/**
 * audit3-6 smoke — requestSubscription has constant-time response.
 *
 * Run: npx tsx scripts/smoke/audit3-6-subscriber-timing.ts
 *
 * Why this exists (2026-05-28):
 *   The /api/status/subscribe endpoint returns the same body for
 *   confirmed / unconfirmed / new addresses to avoid leaking which
 *   emails are subscribed. But latency differed by ~500 ms:
 *   confirmed emails skipped SMTP, new emails awaited transporter.
 *   An unauthenticated attacker could enumerate subscribers by timing.
 *
 *   Fix: pad every path to MIN_RESPONSE_MS using setTimeout. This
 *   smoke uses stubs to make the "new" path effectively instant, then
 *   asserts every path still takes ≥ MIN_RESPONSE_MS.
 */
import './_lib';
import { ok, fail } from './_lib';

// Wire up stubs BEFORE importing the service.
const prismaMod = require('../../src/lib/prisma') as { prisma: Record<string, unknown> };

// Three scenarios:
//   1. existing + confirmed   → service used to return in ~5 ms
//   2. existing + unconfirmed → SMTP send
//   3. new                    → INSERT + SMTP send
// In this smoke ALL three should return in ≥ MIN_RESPONSE_MS (600 ms default).
const baseSub = {
    id: 1, email: 't@example.com', confirmToken: 'a'.repeat(64),
    unsubscribeToken: 'b'.repeat(64), createdAt: new Date(), confirmedAt: null as Date | null,
};
let scenario: 'confirmed' | 'unconfirmed' | 'new' = 'confirmed';

prismaMod.prisma.statusPageSubscriber = {
    findUnique: async () => {
        if (scenario === 'new') return null;
        return { ...baseSub, confirmedAt: scenario === 'confirmed' ? new Date() : null };
    },
    update: async () => baseSub,
    create: async () => baseSub,
} as unknown;

// Force a deliberately-low MIN_RESPONSE_MS so this smoke runs in <2s,
// but still well above measurement noise (~50 ms is enough to detect
// the original ~500 ms timing gap).
process.env.SUBSCRIBE_MIN_RESPONSE_MS = '300';

// Stub the mailer transport so the "new" / "unconfirmed" branches don't
// actually try to send SMTP. The service injects its own mailer via
// constructor default, so we patch nodemailer just in case.
delete require.cache[require.resolve('../../src/lib/services/status-subscriber.service')];
const mod = require('../../src/lib/services/status-subscriber.service') as typeof import('../../src/lib/services/status-subscriber.service');
const { StatusSubscriberService } = mod;

// Inject a no-op mailer so safeSendConfirm doesn't await real SMTP.
const fastMailer = {
    sendConfirm: async () => undefined,
    sendIncidentOpen: async () => undefined,
    sendIncidentResolved: async () => undefined,
};

async function measure(scenarioName: 'confirmed' | 'unconfirmed' | 'new'): Promise<number> {
    scenario = scenarioName;
    const svc = new StatusSubscriberService({
        prisma: prismaMod.prisma as never,
        mailer: fastMailer as never,
    });
    const t0 = Date.now();
    await svc.requestSubscription(`${scenarioName}@example.com`);
    return Date.now() - t0;
}

async function main(): Promise<void> {
    const minMs = parseInt(process.env.SUBSCRIBE_MIN_RESPONSE_MS || '300', 10);

    for (const sc of ['confirmed', 'unconfirmed', 'new'] as const) {
        const elapsed = await measure(sc);
        if (elapsed < minMs - 50) {
            fail(
                `audit3-6 ${sc} path`,
                `requestSubscription returned in ${elapsed}ms; floor is ${minMs}ms — timing leak open`,
            );
        }
        ok(`${sc} path padded to ≥ ${minMs - 50}ms (actual ${elapsed}ms)`);
    }

    // Defence-in-depth: the source must contain the padding line.
    const fs = require('fs') as typeof import('fs');
    const path = require('path') as typeof import('path');
    const src = fs.readFileSync(
        path.resolve(__dirname, '../../src/lib/services/status-subscriber.service.ts'),
        'utf8',
    );
    if (!/MIN_RESPONSE_MS/.test(src)) {
        fail('audit3-6 const missing', 'MIN_RESPONSE_MS constant not declared');
    }
    if (!/setTimeout\(\s*r\s*,\s*padMs\s*\)/.test(src)) {
        fail('audit3-6 pad logic', 'no setTimeout(r, padMs) call to equalise timing');
    }
    ok('source declares MIN_RESPONSE_MS + setTimeout padding');

    ok('audit3-6 subscriber timing', 'every path returns in ≥ MIN_RESPONSE_MS — no enumeration via timing');
}

main().catch((e) => fail('audit3-6 crashed', e instanceof Error ? e.message : String(e)));
