import assert from 'node:assert/strict';
import { mkdtempSync, writeFileSync } from 'node:fs';
import net from 'node:net';
import { tmpdir } from 'node:os';
import path from 'node:path';
import tls from 'node:tls';
import { test } from 'node:test';

import {
  createDomain,
  createInboundMailbox,
  createUser,
  initDatabase
} from '../src/db.js';
import { readMaildirMessage, scanMaildirMailbox } from '../src/maildir-store.js';
import { resolveSubmissionTracking, startSubmissionServer } from '../src/submission.js';

const testCert = `-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUKFK3kjPk5eJZ2nsmLbEjfQZp5lQwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDcwODA2MjYzMloXDTI2MDgw
NzA2MjYzMlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAuUOQ0ewpBEjMNEBcyN79o8ujqetpWh7wbLcc5+aiNUaa
OhmGitZaHuNzLBG/vhd44QJjBmdRV4lG8/t8NCFMczpHzmW5f8ovY+5MqeOMDXDX
zeX+LjyraYTM/NxuZirUOQNjE5oq5T8TirTpp1ZycF/aYz9kyq/BlIR3bMySHV+M
39o6U1aS71tpP4NthnwOtxWs9Md5axfw5d4PcNEbEx42XfcQxnaZkmIm3O0uQ9z/
E5K9TwMPsIAYVWKUff57thwB27cj/SBEJoPCAcWvIenbPddLGuIm+T9LHoGiMmfd
OSW9Hx+1fKCAsIA9JuUkMCCRWAnI6aft+gI+GByFVQIDAQABo1MwUTAdBgNVHQ4E
FgQUEBUJl3WRWbTyqk1gF9T0tpPQGx8wHwYDVR0jBBgwFoAUEBUJl3WRWbTyqk1g
F9T0tpPQGx8wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOsr+
ZBjxA+3ynvE0Ay0UHVNKWmkkxVwX9uiYdfP/KZSD8HGuEZ22c9Z4EmfLg4XxXnaK
Up2A3vg/r1TA4LcHSCzCpYgUsT1mNs84jFlkg7qcSr/To3AodQIbU6290nVIHCoA
LLLpDLOMm7HkrbqRRQnLJBz296PZgHZDZ5W680C+QEZhw3BlY6hixdvHikd9xPFv
jBVQrMyBu0kqlhGY3ZsqhMfapn1x/pmCl+kd6KXj5jOn1gSqig4hOh8mGa7xq7D8
/okZ4boyv18n95yjo2YkhpuTidocBDsSoK9U4UQrVl4ID+zydh2vVpvYbCemmBc9
EZVyHdaIa3874ry3Kw==
-----END CERTIFICATE-----`;

const testKey = `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5Q5DR7CkESMw0
QFzI3v2jy6Op62laHvBstxzn5qI1Rpo6GYaK1loe43MsEb++F3jhAmMGZ1FXiUbz
+3w0IUxzOkfOZbl/yi9j7kyp44wNcNfN5f4uPKtphMz83G5mKtQ5A2MTmirlPxOK
tOmnVnJwX9pjP2TKr8GUhHdszJIdX4zf2jpTVpLvW2k/g22GfA63Faz0x3lrF/Dl
3g9w0RsTHjZd9xDGdpmSYibc7S5D3P8Tkr1PAw+wgBhVYpR9/nu2HAHbtyP9IEQm
g8IBxa8h6ds910sa4ib5P0segaIyZ905Jb0fH7V8oICwgD0m5SQwIJFYCcjpp+36
Aj4YHIVVAgMBAAECggEAEPWDqgfHiG4ejVDv/W5WQxp22KzlV7vZz+XiRkM5SY21
PAjOpWJyXP3+ssPMLNtKtDRK+tDV5CC7p0V37PC4ZhrpbFYpCS7psCjmCS48iZVx
F9bMMhDZMx9WQXZvP7h/dVmfRfHYp+QihpHBitExyCIqJGgu8pPssUsAAKxoWrpW
z2VvWXBvIBtvWTJNjBduKgoEAWpqdJU/YlXyDyHKOtGpQEksR4A0CWeMEa4BYgog
MYou8TSiKuIb4M5BKdiHBSO7RiTn6YJ3o7BoGnMVztOPQS6HjMZbAc9oevM2CXQI
4gy/BgqSRz4FFbGDp5ZscRdEgf4sxvmXiIzWQ0jqSQKBgQDg35FCOmMTIBujh0s1
E0D8wmX/Gt6D63Tn1ORFn0OrOSV3qywb9xuQ3ZpSkQwX5+AwjRIcvB1HUTml5xJb
cIxKom/DmCzsHl3Pi16F5U7kRLV0h++Wi4z5Inqdf1jchcPiVwPMZFd0sRwechdr
47PUJokwDb4YQuKGzMJDB/4uWQKBgQDS6G59Gc2dS04BXpSjPcY75JzQ8PIQdOgn
oGdSOhr26hMHIuquHcxTmgLQVDJ1zdNMW6o+btYmGbq3kC25T0JtFn/xVN4kGGSt
askSeC3wuoScRmQbC4ibriXtDI0rVVoW+q9PPxqjnCYumgAru8JPk//sF1OrOcgf
fg4NQeNHXQKBgBReK5oED9U7o0U8i+NSyyAZu4NPu7fXK8+TyNlFg6uOkYY1Stl9
mFGWwNOZr666oePD41AW+c/r23zbYR5HI23fKKBeuDLqzTRvOzCFhI/IaWcUqO7J
1Qr7xH9feXz/4K4vk3h/3iwDnrhjPRvYlAEzPFnN5NnttPbgtPe9UZiBAoGADHoN
lz8Ah+6dhb03o9SStpZWpJGscEbKORXnJtkjITpFt+Vb5sMChGuXAQYKGif7+Qdv
MdRSvNxGzHcuDUlgD54GIZu4rH/47L1leb88UIJoN+p+H492HGeX/McCTu70rmlU
F6cPB2DEbXtUyUGJRDXoLOpQW8/GQ/6sDCK6tE0CgYEAqYZuCqT+mcuZLwwUzQD+
6Yucas5RNEXuBP+2/VjKLjUoNRmI1G3EvLVXtlhw5PFxXqUnNFoaQZ0LEqeNfhrh
pBFgyA7DS+r2vSQNq52yjyyI6Jhe7dIc6I79e3uqSYQ0ODWm5z8IYzmQ64R/yeqb
2trVak9Z2+qu6bsdcDk6TEE=
-----END PRIVATE KEY-----`;

test('smtp listeners accept both plain SMTP and implicit TLS clients', async () => {
  const dataDir = mkdtempSync(path.join(tmpdir(), 'mailhub-submission-test-'));
  const certPath = path.join(dataDir, 'cert.pem');
  const keyPath = path.join(dataDir, 'key.pem');
  writeFileSync(certPath, testCert);
  writeFileSync(keyPath, testKey);
  const [server] = startSubmissionServer({
    enabled: true,
    listeners: [{ port: 0, protocol: 'smtp' }],
    hostname: 'localhost',
    allowInsecureAuth: true,
    tlsCertPath: certPath,
    tlsKeyPath: keyPath
  });
  await waitForListening(server);
  const port = server.address().port;

  try {
    const plainBanner = await readPlainBanner(port);
    assert.match(plainBanner, /^220 localhost MailHub SMTP ready/);

    const tlsBanner = await readImplicitTlsBanner(port);
    assert.match(tlsBanner, /^220 localhost MailHub SMTP ready/);
  } finally {
    await closeServer(server);
  }
});

test('SMTP STARTTLS preserves non-UTF-8 inbound message bytes', async () => {
  const dataDir = mkdtempSync(path.join(tmpdir(), 'mailhub-submission-starttls-'));
  const maildirRoot = path.join(dataDir, 'maildir');
  const certPath = path.join(dataDir, 'cert.pem');
  const keyPath = path.join(dataDir, 'key.pem');
  writeFileSync(certPath, testCert);
  writeFileSync(keyPath, testKey);
  initDatabase(dataDir, 'submission-starttls-secret');
  const user = createUser({
    username: 'submission-starttls',
    email: 'submission-starttls@example.com',
    password: 'password123'
  });
  createDomain(user.id, {
    domain: 'starttls.example',
    selector: 'mh',
    verificationToken: 'verify',
    dkimPublic: 'public',
    dkimPrivate: 'private',
    senderHost: 'mail.starttls.example',
    sendingIp: '192.0.2.72',
    spfExtra: '',
    dmarcPolicy: 'none',
    dmarcRua: ''
  });
  const mailbox = createInboundMailbox(user.id, {
    address: 'inbox@starttls.example',
    password: 'mailbox-password'
  });
  const [server] = startSubmissionServer({
    enabled: true,
    listeners: [{ port: 0, protocol: 'smtp' }],
    hostname: 'localhost',
    allowInsecureAuth: false,
    inboundEnabled: true,
    maildirEnabled: true,
    maildirRoot,
    tlsCertPath: certPath,
    tlsKeyPath: keyPath
  });
  await waitForListening(server);

  const rawMessage = Buffer.concat([
    Buffer.from([
      'From: sender@example.net',
      `To: ${mailbox.address}`,
      'Subject: STARTTLS bytes',
      'Content-Type: text/plain; charset=ISO-8859-1',
      '',
      'caf'
    ].join('\r\n'), 'ascii'),
    Buffer.from([0xe9, 0x0d, 0x0a])
  ]);

  try {
    await sendStartTlsMessage(server.address().port, mailbox.address, rawMessage);
    const [entry] = await scanMaildirMailbox({ root: maildirRoot, address: mailbox.address });
    assert.ok(entry);
    const stored = (await readMaildirMessage(entry)).bytes;
    assert.equal(stored.includes(Buffer.from([0x63, 0x61, 0x66, 0xe9])), true);
    assert.equal(stored.includes(Buffer.from('café', 'utf8')), false);
  } finally {
    await closeServer(server);
  }
});

test('submission tracking honors defaults and explicit control headers', () => {
  assert.deepEqual(resolveSubmissionTracking('Subject: A\r\n\r\nBody', false), {
    enabled: false,
    opens: false,
    clicks: false,
    explicit: false
  });
  assert.deepEqual(resolveSubmissionTracking('X-MailHub-Track: opens\r\n\r\nBody', false), {
    enabled: true,
    opens: true,
    clicks: false,
    explicit: true
  });
  assert.deepEqual(resolveSubmissionTracking('X-MailHub-Track: clicks, opens\r\n\r\nBody', false), {
    enabled: true,
    opens: true,
    clicks: true,
    explicit: true
  });
  assert.deepEqual(resolveSubmissionTracking('X-MailHub-Track: off\r\n\r\nBody', true), {
    enabled: false,
    opens: false,
    clicks: false,
    explicit: true
  });
});

function waitForListening(server) {
  if (server.listening) return Promise.resolve();
  return new Promise((resolve) => server.once('listening', resolve));
}

function closeServer(server) {
  return new Promise((resolve, reject) => {
    server.close((error) => error ? reject(error) : resolve());
  });
}

function readPlainBanner(port) {
  return new Promise((resolve, reject) => {
    const socket = net.createConnection({ host: '127.0.0.1', port });
    socket.setTimeout(3000);
    socket.once('data', (chunk) => {
      socket.end();
      resolve(chunk.toString('utf8').trim());
    });
    socket.once('error', reject);
    socket.once('timeout', () => reject(new Error('plain SMTP banner timed out')));
  });
}

function readImplicitTlsBanner(port) {
  return new Promise((resolve, reject) => {
    const socket = tls.connect({
      host: '127.0.0.1',
      port,
      rejectUnauthorized: false,
      servername: 'localhost'
    });
    socket.setTimeout(3000);
    socket.once('secureConnect', () => {
      socket.once('data', (chunk) => {
        socket.end();
        resolve(chunk.toString('utf8').trim());
      });
    });
    socket.once('error', reject);
    socket.once('timeout', () => reject(new Error('implicit TLS SMTP banner timed out')));
  });
}

async function sendStartTlsMessage(port, recipient, rawMessage) {
  const plainSocket = net.createConnection({ host: '127.0.0.1', port });
  plainSocket.setTimeout(3000);
  await waitForSocketEvent(plainSocket, 'connect');
  await readSmtpResponse(plainSocket, 220);
  await smtpCommand(plainSocket, 'EHLO sender.example.net', 250);
  await smtpCommand(plainSocket, 'STARTTLS', 220);

  const secureSocket = tls.connect({
    socket: plainSocket,
    rejectUnauthorized: false,
    servername: 'localhost'
  });
  secureSocket.setTimeout(3000);
  await waitForSocketEvent(secureSocket, 'secureConnect');
  try {
    await smtpCommand(secureSocket, 'EHLO sender.example.net', 250);
    await smtpCommand(secureSocket, 'MAIL FROM:<sender@example.net>', 250);
    await smtpCommand(secureSocket, `RCPT TO:<${recipient}>`, 250);
    await smtpCommand(secureSocket, 'DATA', 354);
    secureSocket.write(rawMessage);
    if (!rawMessage.subarray(-2).equals(Buffer.from('\r\n'))) secureSocket.write('\r\n');
    secureSocket.write('.\r\n');
    await readSmtpResponse(secureSocket, 250);
    await smtpCommand(secureSocket, 'QUIT', 221);
  } finally {
    secureSocket.destroy();
  }
}

async function smtpCommand(socket, command, expectedCode) {
  socket.write(`${command}\r\n`);
  return readSmtpResponse(socket, expectedCode);
}

function readSmtpResponse(socket, expectedCode) {
  return new Promise((resolve, reject) => {
    let buffer = '';
    const cleanup = () => {
      socket.off('data', onData);
      socket.off('error', onError);
      socket.off('timeout', onTimeout);
    };
    const onError = (error) => {
      cleanup();
      reject(error);
    };
    const onTimeout = () => onError(new Error('SMTP response timed out'));
    const onData = (chunk) => {
      buffer += Buffer.from(chunk).toString('latin1');
      const lines = buffer.split(/\r?\n/);
      const terminal = lines.find((line) => new RegExp(`^${expectedCode} `).test(line));
      if (!terminal) return;
      cleanup();
      resolve(buffer);
    };
    socket.on('data', onData);
    socket.once('error', onError);
    socket.once('timeout', onTimeout);
  });
}

function waitForSocketEvent(socket, event) {
  return new Promise((resolve, reject) => {
    const cleanup = () => {
      socket.off(event, onEvent);
      socket.off('error', onError);
      socket.off('timeout', onTimeout);
    };
    const onEvent = () => {
      cleanup();
      resolve();
    };
    const onError = (error) => {
      cleanup();
      reject(error);
    };
    const onTimeout = () => onError(new Error(`${event} timed out`));
    socket.once(event, onEvent);
    socket.once('error', onError);
    socket.once('timeout', onTimeout);
  });
}
