import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import { test } from 'node:test';
import { buildMessage, signMessageForDomain } from '../src/mailer.js';
import { createDkimKeyPair } from '../src/dkim.js';

test('signs messages with a verifiable DKIM relaxed body hash', () => {
  const keys = createDkimKeyPair();
  const raw = buildMessage({
    from: 'noreply@example.com',
    to: 'user@example.net',
    subject: '中文 DKIM test',
    text: 'Hello   MailHub  \n中文正文\twith spacing',
    baseUrl: 'https://mailhub.test'
  });
  const signed = signMessageForDomain(raw, {
    domain: 'example.com',
    selector: 'mh202607',
    dkimPrivate: keys.privateKey
  });

  const verification = verifyDkimSignature(signed, keys.publicKey);
  assert.equal(verification.bodyHashValid, true);
  assert.equal(verification.signatureValid, true);
});

test('signs deliverability headers and includes DKIM identity metadata', () => {
  const keys = createDkimKeyPair();
  const raw = buildMessage({
    from: 'noreply@example.com',
    to: 'user@example.net',
    subject: 'Deliverability headers',
    text: 'Hello',
    baseUrl: 'https://mailhub.test',
    headers: [
      ['List-Unsubscribe', '<mailto:unsubscribe@example.com>, <https://example.com/unsubscribe/123>'],
      ['List-Unsubscribe-Post', 'List-Unsubscribe=One-Click'],
      ['Feedback-Id', 'mh.u123.d456.e789:MailHub'],
      ['X-Report-Abuse-To', 'abuse@example.com']
    ]
  });
  const signed = signMessageForDomain(raw, {
    domain: 'example.com',
    selector: 'mh202607',
    dkimPrivate: keys.privateKey
  });

  const verification = verifyDkimSignature(signed, keys.publicKey);
  const tags = verification.tags;
  const signedHeaders = tags.h.split(':');

  assert.equal(verification.bodyHashValid, true);
  assert.equal(verification.signatureValid, true);
  assert.equal(tags.i, '@example.com');
  assert.equal(tags.q, 'dns/txt');
  assert.match(tags.t, /^\d+$/);
  assert.ok(signedHeaders.includes('list-unsubscribe'));
  assert.ok(signedHeaders.includes('list-unsubscribe-post'));
  assert.ok(signedHeaders.includes('feedback-id'));
  assert.ok(signedHeaders.includes('x-report-abuse-to'));
});

function verifyDkimSignature(rawMessage, publicKey) {
  const separator = rawMessage.indexOf('\r\n\r\n');
  const headers = parseHeaders(rawMessage.slice(0, separator));
  const body = rawMessage.slice(separator + 4);
  const dkim = headers.find((header) => header.name.toLowerCase() === 'dkim-signature');
  assert.ok(dkim);
  const tags = parseDkimTags(dkim.value);
  const bodyHash = crypto
    .createHash('sha256')
    .update(relaxedBody(body))
    .digest('base64');
  const dkimWithoutSignature = dkim.value.replace(/(^|;\s*)b=[^;]*/i, '$1b=');
  const signedHeaderNames = tags.h.split(':').map((name) => name.trim().toLowerCase()).filter(Boolean);
  const signingInput = [
    ...signedHeaderNames.map((name) => relaxedHeader(findLastHeader(headers, name))),
    relaxedHeader({ name: 'DKIM-Signature', value: dkimWithoutSignature }, '')
  ].join('');
  const signatureValid = crypto
    .createVerify('RSA-SHA256')
    .update(signingInput)
    .verify(dkimPublicPem(publicKey), tags.b, 'base64');
  return {
    bodyHashValid: bodyHash === tags.bh,
    signatureValid,
    tags
  };
}

function parseHeaders(headerBlock) {
  const headers = [];
  for (const line of headerBlock.split('\r\n')) {
    if (/^[\t ]/.test(line) && headers.length) {
      headers[headers.length - 1].value += ` ${line.trim()}`;
      continue;
    }
    const index = line.indexOf(':');
    if (index === -1) continue;
    headers.push({
      name: line.slice(0, index),
      value: line.slice(index + 1)
    });
  }
  return headers;
}

function parseDkimTags(value) {
  const tags = {};
  for (const part of value.split(';')) {
    const index = part.indexOf('=');
    if (index === -1) continue;
    tags[part.slice(0, index).trim()] = part.slice(index + 1).trim();
  }
  return tags;
}

function findLastHeader(headers, name) {
  const found = [...headers].reverse().find((header) => header.name.toLowerCase() === name);
  return found || { name, value: '' };
}

function relaxedHeader(header, suffix = '\r\n') {
  return `${header.name.toLowerCase()}:${header.value.replace(/\s+/g, ' ').trim()}${suffix}`;
}

function relaxedBody(body) {
  const lines = String(body || '')
    .replace(/\r?\n/g, '\r\n')
    .split('\r\n')
    .map((line) => line.replace(/[ \t]+$/g, '').replace(/[ \t]+/g, ' '));
  while (lines.length && lines[lines.length - 1] === '') lines.pop();
  return `${lines.join('\r\n')}\r\n`;
}

function dkimPublicPem(publicKey) {
  return [
    '-----BEGIN PUBLIC KEY-----',
    publicKey.match(/.{1,64}/g).join('\n'),
    '-----END PUBLIC KEY-----',
    ''
  ].join('\n');
}
