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

import {
  createDomain,
  createUser,
  getInboundMailbox,
  initDatabase,
  upsertImportedInboundMailbox
} from '../src/db.js';
import { startMailboxAccessServers } from '../src/mail-access.js';
import { startSubmissionServer } from '../src/submission.js';

const legacyPassword = 'Vesta-Legacy-Pass!2026';
const legacyPasswordHash = '{MD5}$1$MhVesta1$tmBXF7EjU15TTV.VWRFfl1';

test('Vesta legacy mailbox password authenticates over IMAP, POP3 and SMTP then upgrades without plaintext', async () => {
  const database = initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-vesta-auth-')), 'vesta-auth-secret');
  const user = createUser({
    username: 'vesta-auth-user',
    email: 'vesta-auth-user@example.test',
    password: 'account-password'
  });
  createDomain(user.id, {
    domain: 'vesta-auth.example',
    selector: 'mh',
    verificationToken: 'verify',
    dkimPublic: 'public',
    dkimPrivate: 'private',
    senderHost: 'mail.vesta-auth.example',
    sendingIp: '192.0.2.40',
    spfExtra: '',
    dmarcPolicy: 'none',
    dmarcRua: ''
  });
  const mailbox = upsertImportedInboundMailbox(user.id, {
    address: 'legacy@vesta-auth.example',
    passwordHash: legacyPasswordHash
  });

  const imported = database
    .prepare('SELECT password_hash, password_secret FROM inbound_mailboxes WHERE id = ?')
    .get(mailbox.id);
  assert.equal(imported.password_hash, legacyPasswordHash);
  assert.equal(imported.password_secret, '');

  const publicMailbox = getInboundMailbox(mailbox.id, user.id);
  assert.equal(Object.hasOwn(publicMailbox, 'password'), false);
  assert.equal(Object.hasOwn(publicMailbox, 'passwordHash'), false);
  assert.equal(Object.hasOwn(publicMailbox, 'passwordSecret'), false);
  assert.equal(JSON.stringify(publicMailbox).includes(legacyPassword), false);

  const [imapServer, pop3Server] = startMailboxAccessServers({
    hostname: 'mail.vesta-auth.example',
    imapEnabled: true,
    imapListeners: [{ port: 0, protocol: 'imap' }],
    pop3Enabled: true,
    pop3Listeners: [{ port: 0, protocol: 'pop3' }],
    allowInsecureAuth: true
  });
  const [smtpServer] = startSubmissionServer({
    enabled: true,
    listeners: [{ port: 0, protocol: 'smtp' }],
    hostname: 'mail.vesta-auth.example',
    allowInsecureAuth: true,
    inboundEnabled: true
  });
  const servers = [imapServer, pop3Server, smtpServer];
  await Promise.all(servers.map(waitForListening));

  let imapClient;
  let pop3Client;
  let smtpClient;
  try {
    imapClient = await connectClient(imapServer.address().port);
    await imapClient.readUntil(/\* OK .* IMAP ready\r\n/);
    const imapLogin = await imapClient.command(
      `A1 LOGIN "${mailbox.address}" "${legacyPassword}"`,
      /A1 (?:OK|NO)/
    );
    assert.match(imapLogin, /A1 OK LOGIN completed/);

    const upgraded = database
      .prepare('SELECT password_hash, password_secret FROM inbound_mailboxes WHERE id = ?')
      .get(mailbox.id);
    assert.match(upgraded.password_hash, /^scrypt\$/);
    assert.notEqual(upgraded.password_hash, legacyPasswordHash);
    assert.equal(upgraded.password_secret, '');
    assert.equal(JSON.stringify(upgraded).includes(legacyPassword), false);

    await imapClient.command('A2 LOGOUT', /A2 OK/);
    imapClient.close();
    imapClient = null;

    pop3Client = await connectClient(pop3Server.address().port);
    await pop3Client.readUntil(/\+OK .* POP3 ready\r\n/);
    assert.match(
      await pop3Client.command(`USER ${mailbox.address}`, /\+OK|\-ERR/),
      /\+OK User accepted/
    );
    assert.match(
      await pop3Client.command(`PASS ${legacyPassword}`, /\+OK|\-ERR/),
      /\+OK Mailbox locked and ready/
    );
    await pop3Client.command('QUIT', /\+OK Bye/);
    pop3Client.close();
    pop3Client = null;

    smtpClient = await connectClient(smtpServer.address().port);
    await smtpClient.readUntil(/^220 .* ready\r\n/m);
    const ehlo = await smtpClient.command('EHLO client.example', /250 HELP\r\n/);
    assert.match(ehlo, /250-AUTH PLAIN LOGIN/);
    const auth = Buffer.from(`\u0000${mailbox.address}\u0000${legacyPassword}`).toString('base64');
    assert.match(
      await smtpClient.command(`AUTH PLAIN ${auth}`, /235 |535 /),
      /235 Authentication successful/
    );
    await smtpClient.command('QUIT', /221 Bye/);
    smtpClient.close();
    smtpClient = null;

    const persisted = getInboundMailbox(mailbox.id, user.id, { includeHash: true, includeSecret: true });
    assert.match(persisted.passwordHash, /^scrypt\$/);
    assert.equal(persisted.passwordSecret, '');
    assert.equal(persisted.passwordRecoverable, false);
    assert.equal(Object.hasOwn(getInboundMailbox(mailbox.id, user.id), 'passwordHash'), false);
  } finally {
    imapClient?.close();
    pop3Client?.close();
    smtpClient?.close();
    await Promise.all(servers.map(closeServer));
  }
});

function connectClient(port) {
  return new Promise((resolve, reject) => {
    const socket = net.createConnection({ host: '127.0.0.1', port });
    socket.setTimeout(5000);
    let buffer = '';
    const waiters = [];

    socket.on('data', (chunk) => {
      buffer += chunk.toString('utf8');
      for (const waiter of [...waiters]) {
        if (!waiter.pattern.test(buffer)) continue;
        waiters.splice(waiters.indexOf(waiter), 1);
        const output = buffer;
        buffer = '';
        clearTimeout(waiter.timer);
        waiter.resolve(output);
      }
    });
    socket.once('connect', () => resolve({
      command(command, pattern) {
        socket.write(`${command}\r\n`);
        return this.readUntil(pattern);
      },
      readUntil(pattern) {
        if (pattern.test(buffer)) {
          const output = buffer;
          buffer = '';
          return Promise.resolve(output);
        }
        return new Promise((waitResolve, waitReject) => {
          const waiter = { pattern, resolve: waitResolve, timer: null };
          waiter.timer = setTimeout(() => {
            waiters.splice(waiters.indexOf(waiter), 1);
            waitReject(new Error(`Timed out waiting for ${pattern}; buffered response: ${buffer}`));
          }, 5000);
          waiters.push(waiter);
        });
      },
      close() {
        socket.destroy();
      }
    }));
    socket.once('error', reject);
    socket.once('timeout', () => reject(new Error('Mail protocol client timed out')));
  });
}

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());
  });
}
