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,
  createInboundMailbox,
  createUser,
  createWebmailLoginTicket,
  exchangeWebmailLoginTicket,
  initDatabase,
  replaceInboundMailboxGrants
} from '../src/db.js';
import { startSubmissionServer } from '../src/submission.js';

test('an established Webmail SMTP session rechecks live send permission', async () => {
  initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-submission-webmail-')), 'webmail-submission-secret');
  const owner = createUser({
    username: 'webmail-owner',
    email: 'webmail-owner@example.com',
    password: 'password123'
  });
  const delegate = createUser({
    username: 'webmail-delegate',
    email: 'webmail-delegate@example.com',
    password: 'password123'
  });
  createDomain(owner.id, {
    domain: 'webmail-submit.example',
    selector: 'mh',
    verificationToken: 'verify-webmail-submit',
    dkimPublic: 'public',
    dkimPrivate: 'private',
    senderHost: 'in.webmail-submit.example',
    sendingIp: '192.0.2.10',
    spfExtra: '',
    dmarcPolicy: 'none',
    dmarcRua: ''
  });
  const mailbox = createInboundMailbox(owner.id, {
    address: 'shared@webmail-submit.example',
    password: 'mailbox-password'
  });
  replaceInboundMailboxGrants(mailbox.id, [{
    userId: delegate.id,
    permissions: { receive: true, send: true }
  }]);
  const ticket = createWebmailLoginTicket(delegate.id, mailbox.id, {
    audience: 'https://mail.us.ss5.xyz'
  });
  const login = exchangeWebmailLoginTicket(ticket.ticket, {
    audience: 'https://mail.us.ss5.xyz'
  });
  const [server] = startSubmissionServer({
    enabled: true,
    listeners: [{ port: 0, protocol: 'smtp' }],
    hostname: 'in.webmail-submit.example',
    allowInsecureAuth: true,
    inboundEnabled: true
  });
  await waitForListening(server);
  const client = await connectSmtp(server.address().port);

  try {
    assert.match(await client.readReply(), /^220 /);
    client.write('EHLO roundcube.example');
    assert.match(await client.readReply(), /^250 HELP$/m);
    const auth = Buffer.from(`\u0000${mailbox.address}\u0000${login.credential}`).toString('base64');
    client.write(`AUTH PLAIN ${auth}`);
    assert.match(await client.readReply(), /^235 /);

    replaceInboundMailboxGrants(mailbox.id, [{
      userId: delegate.id,
      permissions: { receive: true, send: false }
    }]);
    client.write(`MAIL FROM:<${mailbox.address}>`);
    assert.match(await client.readReply(), /^530 Authentication no longer valid$/);

    client.write(`AUTH PLAIN ${auth}`);
    assert.match(await client.readReply(), /^535 Authentication failed$/);
  } finally {
    client.close();
    await closeServer(server);
  }
});

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

function closeServer(server) {
  return new Promise((resolve) => server.close(resolve));
}

function connectSmtp(port) {
  return new Promise((resolve, reject) => {
    const socket = net.connect(port, '127.0.0.1');
    socket.setEncoding('utf8');
    let buffer = '';
    const pending = [];
    socket.on('data', (chunk) => {
      buffer += chunk;
      flush();
    });
    socket.once('error', reject);
    socket.once('connect', () => {
      socket.off('error', reject);
      resolve({
        write(line) {
          socket.write(`${line}\r\n`);
        },
        readReply() {
          return new Promise((replyResolve) => {
            pending.push(replyResolve);
            flush();
          });
        },
        close() {
          socket.destroy();
        }
      });
    });

    function flush() {
      if (!pending.length) return;
      const lines = buffer.split('\r\n');
      if (lines.length < 2) return;
      const complete = lines.slice(0, -1);
      const finalIndex = complete.findIndex((line) => /^\d{3} /.test(line));
      if (finalIndex === -1) return;
      const reply = complete.slice(0, finalIndex + 1).join('\n');
      buffer = `${complete.slice(finalIndex + 1).join('\r\n')}${lines.at(-1) ? `\r\n${lines.at(-1)}` : ''}`;
      pending.shift()(reply);
      flush();
    }
  });
}
