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

import {
  handleInternalWebmailSso,
  isSameOriginRequest,
  loadWebmailSsoConfig
} from '../src/webmail-sso.js';

const secret = 'ab'.repeat(32);
const audience = 'https://mail.us.ss5.xyz';
const ticket = `mht_${'a'.repeat(43)}`;
const credential = `mhw_${'b'.repeat(43)}`;

test('Webmail SSO configuration requires HTTPS and a strong file-backed secret', () => {
  const secretFile = writeSecret(secret);
  const config = loadWebmailSsoConfig({
    WEBMAIL_SSO_URL: `${audience}/?_task=mail`,
    WEBMAIL_SSO_SECRET_FILE: secretFile,
    WEBMAIL_SSO_TICKET_TTL_SECONDS: '45',
    WEBMAIL_SSO_CREDENTIAL_TTL_SECONDS: '3600'
  });
  assert.equal(config.enabled, true);
  assert.equal(config.url, `${audience}/`);
  assert.equal(config.audience, audience);
  assert.equal(config.ticketTtlSeconds, 45);
  assert.equal(config.credentialTtlSeconds, 3600);

  assert.throws(
    () => loadWebmailSsoConfig({ WEBMAIL_SSO_URL: audience }),
    /configured together/
  );
  assert.throws(
    () => loadWebmailSsoConfig({
      WEBMAIL_SSO_URL: 'http://webmail.example.com/',
      WEBMAIL_SSO_SECRET_FILE: secretFile
    }),
    /must use HTTPS/
  );
  assert.throws(
    () => loadWebmailSsoConfig({
      WEBMAIL_SSO_URL: audience,
      WEBMAIL_SSO_SECRET_FILE: writeSecret('short')
    }),
    /64-512 hexadecimal/
  );
  assert.throws(
    () => loadWebmailSsoConfig({
      WEBMAIL_SSO_URL: audience,
      WEBMAIL_SSO_SECRET_FILE: writeSecret('z'.repeat(64))
    }),
    /64-512 hexadecimal/
  );
});

test('Webmail ticket creation origin check accepts only the configured or proxied application origin', () => {
  const request = {
    headers: {
      origin: 'https://mail-send.ss5.xyz',
      host: 'app:3000',
      'x-forwarded-host': 'mail-send.ss5.xyz',
      'x-forwarded-proto': 'https'
    },
    socket: { encrypted: false }
  };
  assert.equal(isSameOriginRequest(request, 'https://mail-send.ss5.xyz/', { trustProxy: true }), true);
  assert.equal(isSameOriginRequest({ ...request, headers: { ...request.headers, origin: 'https://evil.example' } }, 'https://mail-send.ss5.xyz/', { trustProxy: true }), false);
  assert.equal(isSameOriginRequest({ ...request, headers: { ...request.headers, origin: '' } }, 'https://mail-send.ss5.xyz/', { trustProxy: true }), false);
});

test('Webmail internal exchange is authenticated, audience-bound, single purpose, and private', async () => {
  const exchanges = [];
  const revocations = [];
  const config = loadWebmailSsoConfig({
    WEBMAIL_SSO_URL: `${audience}/`,
    WEBMAIL_SSO_SECRET_FILE: writeSecret(secret)
  });
  const server = http.createServer((req, res) => {
    const url = new URL(req.url || '/', 'http://mailhub.internal');
    void handleInternalWebmailSso(req, res, url, config, {
      exchangeWebmailLoginTicket(value, options) {
        exchanges.push({ value, options });
        if (exchanges.length > 1) return null;
        return {
          mailbox: { address: 'shared@example.com' },
          credential,
          expiresAt: '2030-01-01T00:00:00.000Z'
        };
      },
      revokeWebmailCredential(value, options) {
        revocations.push({ value, options });
        return true;
      }
    });
  });
  await listen(server);

  try {
    const unauthorized = await request(server, '/internal/webmail-sso/exchange', {
      authorization: 'Bearer wrong-secret',
      body: { ticket, audience }
    });
    assert.equal(unauthorized.status, 401);
    assert.equal(exchanges.length, 0);

    const wrongAudience = await request(server, '/internal/webmail-sso/exchange', {
      body: { ticket, audience: 'https://other.example' }
    });
    assert.equal(wrongAudience.status, 400);
    assert.equal(exchanges.length, 0);

    const exchanged = await request(server, '/internal/webmail-sso/exchange', {
      body: { ticket, audience }
    });
    assert.equal(exchanged.status, 200);
    assert.equal(exchanged.headers['cache-control'], 'no-store');
    assert.deepEqual(exchanged.json, {
      username: 'shared@example.com',
      credential,
      expiresAt: '2030-01-01T00:00:00.000Z'
    });
    assert.deepEqual(exchanges, [{ value: ticket, options: { audience } }]);

    const replayed = await request(server, '/internal/webmail-sso/exchange', {
      body: { ticket, audience }
    });
    assert.equal(replayed.status, 400);
    assert.equal(JSON.stringify(replayed.json).includes(ticket), false);

    const revoked = await request(server, '/internal/webmail-sso/revoke', {
      body: { credential, audience }
    });
    assert.equal(revoked.status, 200);
    assert.deepEqual(revocations, [{ value: credential, options: { audience } }]);
  } finally {
    await close(server);
  }
});

function writeSecret(value) {
  const directory = mkdtempSync(path.join(tmpdir(), 'mailhub-webmail-sso-'));
  const file = path.join(directory, 'secret');
  writeFileSync(file, `${value}\n`, { mode: 0o600 });
  return file;
}

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

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

function request(server, requestPath, {
  authorization = `Bearer ${secret}`,
  body = {}
} = {}) {
  const rawBody = JSON.stringify(body);
  return new Promise((resolve, reject) => {
    const req = http.request({
      host: '127.0.0.1',
      port: server.address().port,
      path: requestPath,
      method: 'POST',
      headers: {
        Authorization: authorization,
        'Content-Type': 'application/json',
        'Content-Length': String(Buffer.byteLength(rawBody))
      }
    }, (res) => {
      const chunks = [];
      res.on('data', (chunk) => chunks.push(chunk));
      res.on('end', () => {
        const raw = Buffer.concat(chunks).toString('utf8');
        resolve({
          status: res.statusCode,
          headers: res.headers,
          json: raw ? JSON.parse(raw) : null
        });
      });
    });
    req.once('error', reject);
    req.end(rawBody);
  });
}
