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

import {
  createDomain,
  createApiToken,
  createInboundMailbox,
  createInboundMessage,
  createUser,
  deleteDomain,
  getInboundMailboxByAddress,
  getInboundMailboxForSender,
  getInboundMessageMaildirStorage,
  getInboundMessage,
  initDatabase,
  listAdminInboundMailboxAccess,
  listDomains,
  listInboundMailboxFolders,
  listInboundMailboxDomains,
  listInboundMailboxes,
  listInboundMessages,
  markInboundMessageRead,
  replaceInboundMailboxGrants,
  resolveInboundRecipient,
  searchInboundMessages,
  transferDomain,
  updateDomain,
  updateInboundMailbox,
  upsertImportedInboundMailbox,
  verifySmtpCredential
} from '../src/db.js';

test('users can create inbound mailboxes and read received messages', () => {
  initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-inbound-db-')), 'inbound-secret');
  const user = createUser({ username: 'inbound-user', email: 'inbound-user@example.com', password: 'password123' });
  createDomain(user.id, {
    domain: 'inbound.example',
    selector: 'mh',
    verificationToken: 'verify',
    dkimPublic: 'public',
    dkimPrivate: 'private',
    senderHost: 'mail.inbound.example',
    sendingIp: '192.0.2.10',
    spfExtra: '',
    dmarcPolicy: 'none',
    dmarcRua: ''
  });

  const mailbox = createInboundMailbox(user.id, {
    address: 'Support@Inbound.Example',
    displayName: 'Support',
    password: 'mailbox-pass-123',
    aliases: 'help desk',
    forwardTo: 'archive@example.net, ops@example.net',
    keepForwarded: false,
    quotaMb: 512
  });
  assert.equal(mailbox.address, 'support@inbound.example');
  assert.equal(mailbox.displayName, 'Support');
  assert.equal(mailbox.passwordSet, true);
  assert.deepEqual(mailbox.aliases, ['help', 'desk']);
  assert.deepEqual(mailbox.forwardTo, ['archive@example.net', 'ops@example.net']);
  assert.equal(mailbox.keepForwarded, false);
  assert.equal(mailbox.quotaMb, 512);
  assert.equal(mailbox.messageCount, 0);
  assert.equal(mailbox.unreadCount, 0);

  const resolved = getInboundMailboxByAddress('SUPPORT@INBOUND.EXAMPLE');
  assert.equal(resolved.id, mailbox.id);
  assert.equal(resolved.userId, user.id);
  assert.equal(verifySmtpCredential('support@inbound.example', 'mailbox-pass-123').user.id, user.id);
  assert.equal(verifySmtpCredential('support@inbound.example', 'wrong-password'), null);
  assert.equal(resolveInboundRecipient('help@inbound.example').mailbox.id, mailbox.id);

  const message = createInboundMessage(resolved, {
    sender: 'alice@example.net',
    recipients: ['support@inbound.example'],
    subject: 'Hello inbound',
    messageId: '<hello@example.net>',
    rawMessage: 'From: alice@example.net\r\nTo: support@inbound.example\r\nSubject: Hello inbound\r\n\r\nHello MailHub',
    textBody: 'Hello MailHub',
    htmlBody: ''
  });
  assert.equal(message.mailboxId, mailbox.id);
  assert.equal(message.read, false);

  const mailboxes = listInboundMailboxes(user.id);
  assert.equal(mailboxes.length, 1);
  assert.equal(mailboxes[0].messageCount, 1);
  assert.equal(mailboxes[0].unreadCount, 1);

  const messages = listInboundMessages(user.id);
  assert.equal(messages.length, 1);
  assert.equal(messages[0].subject, 'Hello inbound');
  assert.equal(messages[0].preview, 'Hello MailHub');
  assert.equal('rawMessage' in messages[0], false);

  const detail = getInboundMessage(user.id, message.id);
  assert.equal(detail.rawMessage.includes('Hello MailHub'), true);
  assert.equal(detail.textBody, 'Hello MailHub');

  const readMessage = markInboundMessageRead(user.id, message.id, true);
  assert.equal(readMessage.read, true);
  assert.equal(listInboundMailboxes(user.id)[0].unreadCount, 0);
  assert.equal(markInboundMessageRead(999999, message.id, true), null);
});

test('mailbox grants isolate owner, view, receive, send, token selection, and transfer access', () => {
  initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-inbound-grants-')), 'inbound-secret');
  const owner = createUser({ username: 'grant-owner', email: 'grant-owner@example.com', password: 'password123' });
  const viewer = createUser({ username: 'grant-viewer', email: 'grant-viewer@example.com', password: 'password123' });
  const receiver = createUser({ username: 'grant-receiver', email: 'grant-receiver@example.com', password: 'password123' });
  const sender = createUser({ username: 'grant-sender', email: 'grant-sender@example.com', password: 'password123' });
  const domain = createDomain(owner.id, {
    domain: 'grants.example',
    selector: 'mh',
    verificationToken: 'verify-grants',
    dkimPublic: 'public',
    dkimPrivate: 'private',
    senderHost: 'mail.grants.example',
    sendingIp: '192.0.2.31',
    spfExtra: '',
    dmarcPolicy: 'none',
    dmarcRua: ''
  });
  const mailbox = createInboundMailbox(owner.id, {
    address: 'support@grants.example',
    aliases: ['help']
  });
  const message = createInboundMessage(mailbox, {
    sender: 'alice@example.net',
    recipients: [mailbox.address],
    subject: 'Mailbox grant message',
    rawMessage: 'Subject: Mailbox grant message\r\n\r\nShared body',
    textBody: 'Shared body'
  });

  const accessEntry = replaceInboundMailboxGrants(mailbox.id, [
    { userId: viewer.id, view: true },
    { userId: receiver.id, permissions: ['receive'] },
    { userId: sender.id, permissions: { send: true } }
  ]);
  assert.equal(accessEntry.mailbox.id, mailbox.id);
  assert.equal(accessEntry.owner.id, owner.id);
  assert.deepEqual(
    new Map(accessEntry.grants.map((grant) => [grant.user.id, grant.permissions])),
    new Map([
      [viewer.id, { view: true, receive: false, send: false }],
      [receiver.id, { view: true, receive: true, send: false }],
      [sender.id, { view: true, receive: false, send: true }]
    ])
  );
  assert.equal(listAdminInboundMailboxAccess().find((entry) => entry.mailbox.id === mailbox.id).owner.id, owner.id);
  assert.deepEqual(listInboundMailboxes(owner.id, { permission: 'view' })[0].access, {
    type: 'owner',
    permissions: { view: true, receive: true, send: true }
  });

  assert.deepEqual(listInboundMailboxes(viewer.id), []);
  const [viewOnlyMailbox] = listInboundMailboxes(viewer.id, { permission: 'view' });
  assert.deepEqual(viewOnlyMailbox.access, {
    type: 'assigned',
    permissions: { view: true, receive: false, send: false }
  });
  assert.equal(viewOnlyMailbox.ownerUserId, owner.id);
  assert.equal(viewOnlyMailbox.messageCount, null);
  assert.equal(viewOnlyMailbox.unreadCount, null);
  assert.equal(viewOnlyMailbox.lastMessageAt, null);
  assert.equal(getInboundMessage(viewer.id, message.id, { permission: 'receive' }), null);
  assert.equal(listInboundMailboxFolders(viewer.id, mailbox.id, { permission: 'receive' }), null);
  assert.equal(getInboundMessageMaildirStorage(viewer.id, message.id, { permission: 'receive' }), null);
  assert.equal(markInboundMessageRead(viewer.id, message.id, true, { permission: 'receive' }), null);

  assert.equal(searchInboundMessages(receiver.id, { pageSize: 100 }).total, 0);
  const [receivedMailbox] = listInboundMailboxes(receiver.id, { permission: 'view' });
  assert.equal(receivedMailbox.access.permissions.receive, true);
  assert.equal(receivedMailbox.messageCount, 1);
  assert.equal(receivedMailbox.unreadCount, 1);
  assert.equal(searchInboundMessages(receiver.id, { pageSize: 100 }, { permission: 'receive' }).total, 1);
  assert.equal(getInboundMessage(receiver.id, message.id, { permission: 'receive' }).subject, 'Mailbox grant message');
  assert.ok(listInboundMailboxFolders(receiver.id, mailbox.id, { permission: 'receive' }));
  assert.equal(getInboundMessageMaildirStorage(receiver.id, message.id, { permission: 'receive' }).id, message.id);
  assert.equal(markInboundMessageRead(receiver.id, message.id, true, { permission: 'receive' }).read, true);

  assert.equal(getInboundMailboxForSender(receiver.id, mailbox.address), null);
  assert.equal(getInboundMailboxForSender(sender.id, mailbox.address).id, mailbox.id);
  assert.equal(getInboundMailboxForSender(sender.id, 'help@grants.example').id, mailbox.id);
  assert.equal(getInboundMailboxForSender(sender.id, mailbox.address, { ownerOnly: true }), null);
  assert.equal(getInboundMailboxForSender(sender.id, mailbox.address, { mailboxIds: [] }), null);
  assert.equal(getInboundMailboxForSender(sender.id, mailbox.address, { mailboxIds: [mailbox.id] }).id, mailbox.id);

  const selectedToken = createApiToken(viewer.id, 'granted mailbox', {
    scopes: ['mailboxes:read'],
    mailboxAccess: 'selected',
    mailboxIds: [mailbox.id]
  });
  assert.equal(selectedToken.mailboxAccess, 'selected');
  assert.deepEqual(selectedToken.mailboxIds, [mailbox.id]);
  assert.equal(listInboundMailboxes(viewer.id, {
    permission: 'view',
    mailboxIds: selectedToken.mailboxIds
  }).length, 1);
  assert.equal(searchInboundMessages(viewer.id, { pageSize: 100 }, {
    permission: 'receive',
    mailboxIds: selectedToken.mailboxIds
  }).total, 0);
  const receiveToken = createApiToken(receiver.id, 'received mailbox', {
    scopes: ['messages:read'],
    mailboxAccess: 'selected',
    mailboxIds: [mailbox.id]
  });
  assert.equal(searchInboundMessages(receiver.id, { pageSize: 100 }, {
    permission: 'receive',
    mailboxIds: receiveToken.mailboxIds
  }).total, 1);

  assert.throws(
    () => replaceInboundMailboxGrants(mailbox.id, [{ userId: viewer.id, view: false, receive: false, send: false }]),
    /至少需要一项权限/
  );
  assert.throws(
    () => replaceInboundMailboxGrants(mailbox.id, [{ userId: owner.id, view: true }]),
    /所有者无需额外授权/
  );
  assert.throws(
    () => replaceInboundMailboxGrants(mailbox.id, [{ userId: 999999, view: true }]),
    /用户不存在/
  );
  assert.throws(
    () => replaceInboundMailboxGrants(mailbox.id, [
      { userId: viewer.id, view: true },
      { userId: viewer.id, receive: true }
    ]),
    /不能重复/
  );
  assert.equal(listAdminInboundMailboxAccess().find((entry) => entry.mailbox.id === mailbox.id).grants.length, 3);

  replaceInboundMailboxGrants(mailbox.id, []);
  assert.equal(listInboundMailboxes(viewer.id, {
    permission: 'view',
    mailboxIds: selectedToken.mailboxIds
  }).length, 0);
  assert.equal(getInboundMailboxForSender(sender.id, mailbox.address), null);

  replaceInboundMailboxGrants(mailbox.id, [{ userId: sender.id, send: true }]);
  transferDomain({ actorUserId: owner.id, domainId: domain.id, targetUserId: sender.id });
  const transferred = listAdminInboundMailboxAccess().find((entry) => entry.mailbox.id === mailbox.id);
  assert.equal(transferred.owner.id, sender.id);
  assert.deepEqual(transferred.grants, []);
});

test('domains can route unknown inbound recipients to catch-all targets', () => {
  initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-inbound-catchall-')), 'inbound-secret');
  const user = createUser({ username: 'catch-user', email: 'catch@example.com', password: 'password123' });
  const domain = createDomain(user.id, {
    domain: 'catch.example',
    selector: 'mh',
    verificationToken: 'verify',
    dkimPublic: 'public',
    dkimPrivate: 'private',
    senderHost: 'mail.catch.example',
    sendingIp: '192.0.2.20',
    spfExtra: '',
    dmarcPolicy: 'none',
    dmarcRua: ''
  });
  const mailbox = createInboundMailbox(user.id, { address: 'share@catch.example' });

  const updated = updateDomain(domain.id, user.id, { catchAllAddress: 'share@catch.example' });
  assert.equal(updated.catchAllAddress, 'share@catch.example');
  const localRoute = resolveInboundRecipient('missing@catch.example');
  assert.equal(localRoute.catchAll, true);
  assert.equal(localRoute.mailbox.id, mailbox.id);
  assert.equal(localRoute.recipient, 'missing@catch.example');

  updateDomain(domain.id, user.id, { catchAllAddress: '/dev/null' });
  const dropRoute = resolveInboundRecipient('drop@catch.example');
  assert.equal(dropRoute.drop, true);
  assert.equal(dropRoute.mailbox, null);

  updateDomain(domain.id, user.id, { catchAllAddress: 'external@example.net' });
  const forwardRoute = resolveInboundRecipient('forward@catch.example');
  assert.deepEqual(forwardRoute.forwardTo, ['external@example.net']);
  assert.equal(forwardRoute.mailbox, null);
});

test('domain owners can share mailbox creation without sharing domain management', () => {
  initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-inbound-db-scope-')), 'inbound-secret');
  const owner = createUser({ username: 'owner-user', email: 'owner@example.com', password: 'password123' });
  const other = createUser({ username: 'other-user', email: 'other@example.com', password: 'password123' });
  const domain = createDomain(owner.id, {
    domain: 'owned.example',
    selector: 'mh',
    verificationToken: 'verify',
    dkimPublic: 'public',
    dkimPrivate: 'private',
    senderHost: 'mail.owned.example',
    sendingIp: '192.0.2.11',
    spfExtra: '',
    dmarcPolicy: 'none',
    dmarcRua: ''
  });

  assert.throws(
    () => createInboundMailbox(other.id, { address: 'support@owned.example' }),
    /收信域名不存在/
  );
  assert.equal(listInboundMailboxDomains(other.id).some((item) => item.domain === 'owned.example'), false);

  const shared = updateDomain(domain.id, owner.id, { mailboxSignupEnabled: true });
  assert.equal(shared.mailboxSignupEnabled, true);
  assert.equal(listDomains(other.id).some((item) => item.domain === 'owned.example'), false);

  const sharedDomains = listInboundMailboxDomains(other.id);
  assert.deepEqual(
    sharedDomains.map((item) => item.domain),
    ['owned.example']
  );
  assert.deepEqual(Object.keys(sharedDomains[0]).sort(), ['domain', 'id', 'mailboxSignupEnabled', 'userId']);

  const mailbox = createInboundMailbox(other.id, {
    address: 'support@owned.example',
    password: 'mailbox-pass-123'
  });
  assert.equal(mailbox.userId, other.id);
  assert.equal(mailbox.domainId, domain.id);
  assert.equal(verifySmtpCredential('support@owned.example', 'mailbox-pass-123').user.id, other.id);

  const message = createInboundMessage(mailbox, {
    sender: 'sender@example.net',
    recipients: ['support@owned.example'],
    subject: 'Shared domain',
    rawMessage: 'From: sender@example.net\r\nTo: support@owned.example\r\nSubject: Shared domain\r\n\r\nHello',
    textBody: 'Hello'
  });
  assert.equal(listInboundMailboxes(other.id).length, 1);
  assert.equal(listInboundMessages(other.id)[0].id, message.id);
  assert.equal(listInboundMessages(owner.id).length, 0);

  assert.throws(
    () => deleteDomain(domain.id, owner.id),
    /该域名仍有关联收信邮箱/
  );
  assert.equal(listDomains(owner.id)[0].id, domain.id);
  assert.equal(getInboundMailboxByAddress(mailbox.address).id, mailbox.id);
  assert.equal(getInboundMessage(other.id, message.id).id, message.id);
});

test('mailbox addresses and aliases share one namespace per domain', () => {
  initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-inbound-namespace-')), 'inbound-secret');
  const owner = createUser({ username: 'namespace-owner', email: 'namespace-owner@example.com', password: 'password123' });
  const other = createUser({ username: 'namespace-user', email: 'namespace-user@example.com', password: 'password123' });
  const domain = createDomain(owner.id, {
    domain: 'namespace.example',
    selector: 'mh',
    verificationToken: 'verify',
    dkimPublic: 'public',
    dkimPrivate: 'private',
    senderHost: 'mail.namespace.example',
    sendingIp: '192.0.2.12',
    spfExtra: '',
    dmarcPolicy: 'none',
    dmarcRua: ''
  });
  updateDomain(domain.id, owner.id, { mailboxSignupEnabled: true });

  const primary = createInboundMailbox(owner.id, {
    address: 'primary@namespace.example',
    aliases: ['billing', 'legacy']
  });
  assert.throws(
    () => createInboundMailbox(other.id, { address: 'billing@namespace.example' }),
    /billing@namespace\.example 已被其他邮箱占用/
  );
  assert.throws(
    () => createInboundMailbox(other.id, {
      address: 'second@namespace.example',
      aliases: ['primary']
    }),
    /primary@namespace\.example 已被其他邮箱占用/
  );

  const second = createInboundMailbox(other.id, {
    address: 'second@namespace.example',
    aliases: ['support']
  });
  assert.deepEqual(updateInboundMailbox(other.id, second.id, { aliases: ['support'] }).aliases, ['support']);
  assert.throws(
    () => updateInboundMailbox(other.id, second.id, { aliases: ['legacy'] }),
    /legacy@namespace\.example 已被其他邮箱占用/
  );

  assert.throws(
    () => upsertImportedInboundMailbox(owner.id, { address: 'legacy@namespace.example' }),
    /legacy@namespace\.example 已被其他邮箱占用/
  );
  const imported = upsertImportedInboundMailbox(owner.id, {
    address: 'archive@namespace.example',
    aliases: ['old-archive']
  });
  assert.throws(
    () => upsertImportedInboundMailbox(owner.id, {
      address: imported.address,
      aliases: ['support']
    }),
    /support@namespace\.example 已被其他邮箱占用/
  );

  assert.equal(resolveInboundRecipient('billing@namespace.example').mailbox.id, primary.id);
  assert.equal(resolveInboundRecipient('support@namespace.example').mailbox.id, second.id);
});
