import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
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,
  createSendEvent,
  createTrackingLink,
  createUser,
  getSendEvent,
  initDatabase
} from '../src/db.js';
import {
  createTrackingToken,
  encryptTrackingTarget,
  hashTrackingToken
} from '../src/tracking.js';

test('serves neutral open pixels and only records valid GET requests', async () => {
  const fixture = await trackingServerFixture();
  try {
    const head = await fetch(`${fixture.baseUrl}/t/o/${fixture.openToken}.gif`, {
      method: 'HEAD',
      headers: { 'User-Agent': 'Mozilla/5.0' }
    });
    assert.equal(head.status, 200);
    assert.match(head.headers.get('content-type') || '', /image\/gif/i);
    assert.match(head.headers.get('cache-control') || '', /no-store/i);
    assert.equal(getSendEvent(fixture.userId, fixture.eventId).tracking.events.length, 0);

    const open = await fetch(`${fixture.baseUrl}/t/o/${fixture.openToken}.gif`, {
      headers: {
        'User-Agent': 'Mozilla/5.0',
        'X-Forwarded-For': '203.0.113.7'
      }
    });
    assert.equal(open.status, 200);
    assert.match(open.headers.get('content-type') || '', /image\/gif/i);
    assert.ok((await open.arrayBuffer()).byteLength > 0);
    const detail = getSendEvent(fixture.userId, fixture.eventId);
    assert.equal(detail.tracking.summary.totalOpens, 1);
    assert.equal(detail.tracking.events[0].source, 'direct');

    const unknown = await fetch(`${fixture.baseUrl}/t/o/${createTrackingToken()}.gif`);
    assert.equal(unknown.status, 200);
    assert.equal(await unknown.arrayBuffer().then((value) => value.byteLength), 42);
    const malformed = await fetch(`${fixture.baseUrl}/t/o/bad.gif`, { redirect: 'manual' });
    assert.equal(malformed.status, 200);
    assert.match(malformed.headers.get('content-type') || '', /image\/gif/i);
    assert.equal(malformed.headers.get('location'), null);
  } finally {
    fixture.child.kill('SIGTERM');
    await waitForExit(fixture.child, 1000);
  }
});

test('redirects opaque click tokens and rejects unknown or unreadable targets', async () => {
  const fixture = await trackingServerFixture();
  try {
    const click = await fetch(`${fixture.baseUrl}/t/c/${fixture.clickToken}`, {
      redirect: 'manual',
      headers: { 'User-Agent': 'Mozilla/5.0' }
    });
    assert.equal(click.status, 302);
    assert.equal(click.headers.get('location'), fixture.target);
    assert.match(click.headers.get('cache-control') || '', /no-store/i);
    assert.equal(click.headers.get('referrer-policy'), 'no-referrer');
    const detail = getSendEvent(fixture.userId, fixture.eventId);
    assert.equal(detail.tracking.summary.totalClicks, 1);
    assert.equal(detail.tracking.events.find((event) => event.eventType === 'click').targetOrigin, 'https://example.net');

    const unknown = await fetch(`${fixture.baseUrl}/t/c/${createTrackingToken()}`, { redirect: 'manual' });
    assert.equal(unknown.status, 404);
    assert.equal(unknown.headers.get('location'), null);
    const malformed = await fetch(`${fixture.baseUrl}/t/c/bad?u=https://attacker.example`, { redirect: 'manual' });
    assert.equal(malformed.status, 404);
    assert.equal(malformed.headers.get('location'), null);

    const unreadable = await fetch(`${fixture.baseUrl}/t/c/${fixture.unreadableClickToken}`, { redirect: 'manual' });
    assert.equal(unreadable.status, 410);
    assert.equal(unreadable.headers.get('location'), null);
  } finally {
    fixture.child.kill('SIGTERM');
    await waitForExit(fixture.child, 1000);
  }
});

async function trackingServerFixture() {
  const dataDir = mkdtempSync(path.join(tmpdir(), 'mailhub-tracking-server-'));
  initDatabase(dataDir, 'session-secret');
  const user = createUser({ username: `tracking-${Date.now()}`, email: `tracking-${Date.now()}@example.com`, password: 'password123' });
  const domain = createDomain(user.id, domainFixture(`tracking-${Date.now()}.example`));
  const openToken = createTrackingToken();
  const eventId = createSendEvent({
    userId: user.id,
    domainId: domain.id,
    sender: `sender@${domain.domain}`,
    recipients: ['reader@example.net'],
    subject: 'Tracking endpoint',
    status: 'sent',
    trackingToken: openToken,
    trackingOpens: true,
    trackingClicks: true
  });
  const target = 'https://example.net/reset?token=private#account';
  const clickToken = createTrackingToken();
  createTrackingLink(user.id, eventId, {
    token: clickToken,
    targetCiphertext: encryptTrackingTarget(target, 'tracking-secret'),
    targetFingerprint: hashTrackingToken(target),
    targetOrigin: 'https://example.net'
  });
  const unreadableClickToken = createTrackingToken();
  createTrackingLink(user.id, eventId, {
    token: unreadableClickToken,
    targetCiphertext: encryptTrackingTarget('https://example.net/old', 'old-secret'),
    targetFingerprint: hashTrackingToken('https://example.net/old'),
    targetOrigin: 'https://example.net'
  });

  const port = await freePort();
  const child = spawn(process.execPath, ['src/server.js'], {
    cwd: process.cwd(),
    env: {
      ...process.env,
      PORT: String(port),
      DATA_DIR: dataDir,
      ADMIN_PASSWORD: 'password123',
      SESSION_SECRET: 'session-secret',
      TRACKING_SECRET: 'tracking-secret',
      TRUST_PROXY: 'true',
      SUBMISSION_ENABLED: 'false',
      IMAP_ENABLED: 'false',
      POP3_ENABLED: 'false',
      WEBHOOK_WORKER_ENABLED: '0',
      DNS_AUTO_CHECK_ENABLED: 'false',
      DELIVERY_TRACKING_ENABLED: 'false'
    },
    stdio: ['ignore', 'pipe', 'pipe']
  });
  await waitForOutput(child, 'MailHub listening');
  return {
    child,
    baseUrl: `http://127.0.0.1:${port}`,
    userId: user.id,
    eventId,
    openToken,
    clickToken,
    unreadableClickToken,
    target
  };
}

function domainFixture(domain) {
  return {
    domain,
    selector: 'mh',
    verificationToken: 'verify',
    dkimPublic: 'public',
    dkimPrivate: 'private',
    senderHost: `mail.${domain}`,
    sendingIp: '192.0.2.10',
    spfExtra: '',
    dmarcPolicy: 'none',
    dmarcRua: ''
  };
}

function freePort() {
  return new Promise((resolve, reject) => {
    const server = net.createServer();
    server.listen(0, '127.0.0.1', () => {
      const { port } = server.address();
      server.close((error) => (error ? reject(error) : resolve(port)));
    });
    server.on('error', reject);
  });
}

function waitForOutput(child, value, timeoutMs = 8000) {
  return new Promise((resolve, reject) => {
    let buffer = '';
    const timer = setTimeout(() => reject(new Error(`Timed out waiting for ${value}\n${buffer}`)), timeoutMs);
    const onData = (chunk) => {
      buffer += String(chunk);
      if (!buffer.includes(value)) return;
      clearTimeout(timer);
      child.stdout?.off('data', onData);
      child.stderr?.off('data', onData);
      resolve();
    };
    child.stdout?.on('data', onData);
    child.stderr?.on('data', onData);
  });
}

function waitForExit(child, timeoutMs) {
  return new Promise((resolve) => {
    if (child.exitCode != null) return resolve(true);
    const timer = setTimeout(() => resolve(false), timeoutMs);
    child.once('exit', () => {
      clearTimeout(timer);
      resolve(true);
    });
  });
}
