/**
 * audit3-followup smoke — every API route either imports `withAuth`,
 * has explicit auth (`requireAuthContext` / `getServerSession`), or is
 * on the documented public-route allowlist.
 *
 * Run: npx tsx scripts/smoke/audit3-withauth-coverage.ts
 *
 * Why this exists (2026-05-29):
 *   The audit highlighted that a future contributor could ship a new
 *   route file under src/app/api/ without remembering to add an auth
 *   check. The cost is high (silently-public sensitive endpoint) and
 *   no existing test catches it.
 *
 *   This smoke walks every `route.ts` under src/app/api/ and asserts
 *   one of:
 *     - imports `withAuth` from `@/lib/api-helpers/with-auth`, OR
 *     - imports `requireAuthContext` (legacy auth helper, used by the
 *       routes that haven't been migrated to withAuth yet), OR
 *     - imports `getServerSession` and references a session check, OR
 *     - is explicitly on the PUBLIC_ROUTES allowlist below.
 *
 *   Public routes MUST be added to the allowlist with a one-line
 *   justification. PRs that add a new route without one of the above
 *   patterns will fail this smoke and require the author to either
 *   add the auth gate or document the public-route decision.
 */
import './_lib';
import { ok, fail } from './_lib';
import fs from 'fs';
import path from 'path';

const ROOT = path.resolve(__dirname, '../..');
const API_DIR = path.join(ROOT, 'src/app/api');

/**
 * Routes that are PUBLIC BY DESIGN. Each entry MUST have a one-line
 * justification next to it. Adding a route here requires PR review —
 * the smoke fails on undocumented entries.
 */
const PUBLIC_ROUTES: ReadonlyArray<{ path: string; reason: string }> = [
    // Auth flow endpoints — pre-session by definition. The auth logic is INSIDE
    // these routes (verifying password, issuing JWTs); they cannot themselves
    // require a prior session.
    { path: 'src/app/api/auth/2fa-challenge/route.ts', reason: 'pre-session 2FA challenge — pendingToken IS the auth' },
    { path: 'src/app/api/auth/[...nextauth]/route.ts', reason: 'NextAuth catch-all — provider/csrf/callback' },
    { path: 'src/app/api/auth/login/route.ts', reason: 'credential exchange — pre-session by definition' },
    { path: 'src/app/api/executive/auth/route.ts', reason: 'executive_display token exchange — device-locked token IS the auth (login-less display)' },

    // Cron-secret-authenticated. These verify the CRON_SECRET header
    // inside the handler; no NextAuth session.
    { path: 'src/app/api/cron/cleanup/route.ts', reason: 'CRON_SECRET header auth — see requireCronSecret' },
    { path: 'src/app/api/cron/route.ts', reason: 'CRON_SECRET header auth' },
    { path: 'src/app/api/cron/run-checks/route.ts', reason: 'CRON_SECRET header auth' },
    { path: 'src/app/api/health/deep/route.ts', reason: 'CRON_SECRET-gated deep healthcheck' },

    // Truly public, intentionally unauth'd.
    { path: 'src/app/api/csp-report/route.ts', reason: 'CSP violation sink — anonymous browsers POST reports; rate-limited + content-length-bounded' },
    { path: 'src/app/api/health/route.ts', reason: 'orchestrator-safe liveness probe' },
    { path: 'src/app/api/metrics/route.ts', reason: 'Prometheus scrape endpoint — Prometheus auth in front' },
    { path: 'src/app/api/status/route.ts', reason: 'public status page data' },
    { path: 'src/app/api/status/subscribe/route.ts', reason: 'public subscribe form (rate-limited + token-confirm flow)' },
    { path: 'src/app/api/status/confirm/route.ts', reason: 'public email-link confirmation (token IS the auth)' },
    { path: 'src/app/api/status/unsubscribe/route.ts', reason: 'public unsubscribe (token IS the auth, two-step POST flow)' },

    // Webhook receivers — token-in-URL is the credential.
    { path: 'src/app/api/heartbeat/[token]/route.ts', reason: 'dead-man\'s-switch heartbeat — URL token IS the auth' },
    { path: 'src/app/api/webhooks/telegram/route.ts', reason: 'Telegram webhook — verifies X-Telegram-Bot-Api-Secret-Token against TELEGRAM_WEBHOOK_SECRET (fails closed); see authz-mutation-roles smoke' },
];

/** Patterns that count as "this route enforces auth". */
const AUTH_IMPORT_PATTERNS = [
    /from\s+['"]@\/lib\/api-helpers\/with-auth['"]/,
    /import\s*\{[^}]*\bwithAuth\b[^}]*\}/,
    /import\s*\{[^}]*\brequireAuthContext\b[^}]*\}/,
    /import\s*\{[^}]*\bgetServerSession\b[^}]*\}/,
    /import\s*\{[^}]*\brequireCronSecret\b[^}]*\}/,
];

function listRouteFiles(dir: string, out: string[] = []): string[] {
    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
        const full = path.join(dir, entry.name);
        if (entry.isDirectory()) listRouteFiles(full, out);
        else if (entry.name === 'route.ts') out.push(full);
    }
    return out;
}

function normalisePath(absPath: string): string {
    return path.relative(ROOT, absPath).replace(/\\/g, '/');
}

function main(): void {
    const routes = listRouteFiles(API_DIR);
    const publicSet = new Set(PUBLIC_ROUTES.map((r) => r.path));

    // Sanity: every entry in PUBLIC_ROUTES must point at an existing
    // route file. Stale allowlist entries are dead weight.
    for (const entry of PUBLIC_ROUTES) {
        const abs = path.join(ROOT, entry.path);
        if (!fs.existsSync(abs)) {
            fail(
                `audit3-withauth-coverage: stale allowlist entry`,
                `PUBLIC_ROUTES references ${entry.path} which does not exist on disk. Remove the entry.`,
            );
        }
    }
    ok(`PUBLIC_ROUTES allowlist: ${PUBLIC_ROUTES.length} entries, all pointing at existing files`);

    const unguarded: string[] = [];
    let guarded = 0;
    let publiclyAllowed = 0;

    for (const file of routes) {
        const rel = normalisePath(file);
        if (publicSet.has(rel)) {
            publiclyAllowed++;
            continue;
        }
        const src = fs.readFileSync(file, 'utf8');
        const isGuarded = AUTH_IMPORT_PATTERNS.some((re) => re.test(src));
        if (isGuarded) {
            guarded++;
        } else {
            unguarded.push(rel);
        }
    }

    if (unguarded.length > 0) {
        const hint = [
            'Fix options:',
            '  1. Add: import { withAuth } from "@/lib/api-helpers/with-auth" and wrap the handler.',
            '  2. Use the legacy requireAuthContext() helper inside the handler.',
            '  3. If the route is intentionally public, add it to PUBLIC_ROUTES with a one-line justification.',
        ].join('\n');
        fail(
            'audit3-withauth-coverage: routes without auth gate',
            `The following route.ts files do NOT import any recognised auth helper AND are not on the PUBLIC_ROUTES allowlist:\n  - ${unguarded.join('\n  - ')}\n\n${hint}\n`,
        );
    }

    ok(
        `route auth coverage: ${guarded} guarded, ${publiclyAllowed} public-by-design, 0 unguarded (out of ${routes.length} routes)`,
    );

    ok('audit3-withauth-coverage', `every route under src/app/api/ has an explicit auth posture (guard or allowlist)`);
}

main();
