// scripts/verify-fu021-sanctions-population-retirement.ts
// FU-021 (2026-05-08) — extras-6 sanctions-list refresh + recall-test retirement verification.
// 11 assertions across function-body, audit-log, route, source-static, UI-static, and DB layers.
// Disposition completion of substrate gap named in FU-019's retirement disclosure (extras-6
// sanctions_lists registry populated from locally-stamped metadata rather than real
// upstream-pulled list data; recall-test outcome metrics hardcoded constants rather than
// results of test execution). Routes are admin-gated, so route-layer assertions use
// source-static checks against the 410 handler bodies (single sync expressions with literals;
// no execution path where source says 410 but runtime returns something else).
//
// Source-static checks (assertions 6-8) apply comment-stripping before regex match —
// the FU-021 header comment in extras-6.ts contains the function names and "FU-021"
// as documentation strings that would false-positive a naive search.
//
// Invocation:  npx tsx scripts/verify-fu021-sanctions-population-retirement.ts
import { db } from "../server/db";
import { auditLogs } from "@shared/schema";
import {
  refreshSanctionsLists,
  runSanctionsScreeningTest,
  listSanctionsLists,
  listSanctionsScreeningTests,
} from "../server/lib/pilot-readiness-extras-6";
import { desc, eq } from "drizzle-orm";
import { readFileSync } from "fs";
import { join } from "path";

const RAN_BY = "verify-fu021-script";
const ROOT = process.cwd();
let pass = 0, fail = 0;
const out = (label: string, ok: boolean, detail = "") => {
  console.log(`${ok ? "PASS" : "FAIL"}  ${label}${detail ? "  — " + detail : ""}`);
  if (ok) pass++; else { fail++; process.exitCode = 1; }
};

// Comment-stripping helper for source-static assertions (precedent: verify-sanctions-retirement.ts,
// verify-segmentation-retirement.ts). Removes block comments and line comments so that
// documentation references to the retired identifiers do not false-positive.
const stripComments = (src: string): string =>
  src
    .replace(/\/\*[\s\S]*?\*\//g, "")
    .split("\n")
    .map((l) => l.replace(/\/\/.*$/, ""))
    .join("\n");

// Stricter helper for caller-site assertions — also strips string literals.
// The 410 response messages in routes.ts and the throw messages in extras-6.ts
// contain documentation text like "the substrate gap retired in refreshSanctionsLists (FU-021)"
// where the identifier is followed by whitespace + "(", which a naive call-syntax regex
// matches even after comment-stripping. Strip simple double/single-quoted string contents
// line-by-line. Template literals are not used in the relevant files.
const stripStringsAndComments = (src: string): string =>
  stripComments(src)
    .replace(/"(?:[^"\\\n]|\\.)*"/g, '""')
    .replace(/'(?:[^'\\\n]|\\.)*'/g, "''");

async function main() {
  console.log("─── FU-021 sanctions-population retirement verification ───");

  // ── Function-body layer ──
  // 1. refreshSanctionsLists throws with FU-021 markers
  let threw1 = false; let msg1 = "";
  try { await refreshSanctionsLists(); }
  catch (e: any) { threw1 = true; msg1 = String(e?.message || ""); }
  out("1. refreshSanctionsLists throws with FU-021 markers",
      threw1 && msg1.includes("FU-021 RETIREMENT") && msg1.includes("FOLLOW-UPS.md FU-021"),
      threw1 ? `msg.len=${msg1.length}` : "did not throw");

  // 2. runSanctionsScreeningTest throws with FU-021 markers
  let threw2 = false; let msg2 = "";
  try { await runSanctionsScreeningTest({ ranBy: RAN_BY }); }
  catch (e: any) { threw2 = true; msg2 = String(e?.message || ""); }
  out("2. runSanctionsScreeningTest throws with FU-021 markers",
      threw2 && msg2.includes("FU-021 RETIREMENT") && msg2.includes("FOLLOW-UPS.md FU-021"),
      threw2 ? `msg.len=${msg2.length}` : "did not throw");

  // 3. Both functions wrote audit_logs rows before throwing.
  // Look for the most recent row of each action; both must postdate the start of this run.
  const startTs = new Date(Date.now() - 30000); // 30s window
  const refreshAudit = (await db.select().from(auditLogs)
    .where(eq(auditLogs.action, "EXTRAS6_SANCTIONS_REFRESH_RETIREMENT_INVOKED"))
    .orderBy(desc(auditLogs.timestamp)).limit(1))[0];
  const runTestAudit = (await db.select().from(auditLogs)
    .where(eq(auditLogs.action, "EXTRAS6_SANCTIONS_SCREENING_TEST_RETIREMENT_INVOKED"))
    .orderBy(desc(auditLogs.timestamp)).limit(1))[0];
  const refreshFresh = !!refreshAudit && new Date(refreshAudit.timestamp) >= startTs;
  const runTestFresh = !!runTestAudit && new Date(runTestAudit.timestamp) >= startTs;
  out("3. Both functions wrote audit-log rows before throwing",
      refreshFresh && runTestFresh,
      `refresh=${refreshFresh ? refreshAudit!.timestamp : "none/stale"} runTest=${runTestFresh ? runTestAudit!.timestamp : "none/stale"}`);

  // ── Route layer (source-static; routes are admin-gated, no test client wired) ──
  const routesSrc = readFileSync(join(ROOT, "server/routes.ts"), "utf8");

  // 4. POST /api/sanctions/refresh handler returns 410 with FU-021 markers
  const refreshIdx = routesSrc.indexOf('app.post("/api/sanctions/refresh"');
  const refreshBlock = refreshIdx >= 0 ? routesSrc.slice(refreshIdx, refreshIdx + 2000) : "";
  const refreshOk = refreshBlock.includes("status(410)")
    && refreshBlock.includes("EXTRAS6_SANCTIONS_REFRESH_RETIRED")
    && /retired 2026-05-08 under FU-021/i.test(refreshBlock)
    && refreshBlock.includes('retiredAt: "2026-05-08"');
  out("4. POST /api/sanctions/refresh handler returns 410 with FU-021 markers",
      refreshOk,
      refreshIdx < 0 ? "handler not found" : `block.len=${refreshBlock.length}`);

  // 5. POST /api/sanctions/run-test handler returns 410 with FU-021 markers
  const runTestIdx = routesSrc.indexOf('app.post("/api/sanctions/run-test"');
  const runTestBlock = runTestIdx >= 0 ? routesSrc.slice(runTestIdx, runTestIdx + 2000) : "";
  const runTestOk = runTestBlock.includes("status(410)")
    && runTestBlock.includes("EXTRAS6_SANCTIONS_SCREENING_TEST_RETIRED")
    && /retired 2026-05-08 under FU-021/i.test(runTestBlock)
    && runTestBlock.includes('retiredAt: "2026-05-08"');
  out("5. POST /api/sanctions/run-test handler returns 410 with FU-021 markers",
      runTestOk,
      runTestIdx < 0 ? "handler not found" : `block.len=${runTestBlock.length}`);

  // ── Source-static layer (re-introduction protection) ──
  // Use stripStringsAndComments for caller checks — the FU-021 header in extras-6.ts contains
  // the function names as documentation, AND the 410 response messages in routes.ts contain
  // strings like "retired in refreshSanctionsLists (FU-021)" where the identifier is followed
  // by whitespace + "(", which a call-syntax regex matches even after comment-stripping alone.
  const extras6CodeOnly = stripStringsAndComments(readFileSync(join(ROOT, "server/lib/pilot-readiness-extras-6.ts"), "utf8"));
  const routesCodeOnly = stripStringsAndComments(routesSrc);

  // 6. No live caller of refreshSanctionsLists. Match `<dot-or-word-boundary>refreshSanctionsLists(`
  // and exclude the function definition line (`export async function refreshSanctionsLists(`).
  const refreshAllMatches = [...extras6CodeOnly.matchAll(/(export\s+async\s+function\s+)?refreshSanctionsLists\s*\(/g)];
  const refreshCallSitesExtras6 = refreshAllMatches.filter((m) => !m[1]).length;
  const refreshCallSitesRoutes = (routesCodeOnly.match(/refreshSanctionsLists\s*\(/g) || []).length;
  out("6. No live caller of refreshSanctionsLists in routes.ts or extras-6.ts (code only)",
      refreshCallSitesRoutes === 0 && refreshCallSitesExtras6 === 0,
      `routes=${refreshCallSitesRoutes} extras6=${refreshCallSitesExtras6}`);

  // 7. No live caller of runSanctionsScreeningTest outside its function definition
  const runTestAllMatches = [...extras6CodeOnly.matchAll(/(export\s+async\s+function\s+)?runSanctionsScreeningTest\s*\(/g)];
  const runTestCallSitesExtras6 = runTestAllMatches.filter((m) => !m[1]).length;
  const runTestCallSitesRoutes = (routesCodeOnly.match(/runSanctionsScreeningTest\s*\(/g) || []).length;
  out("7. No live caller of runSanctionsScreeningTest in routes.ts or extras-6.ts (code only)",
      runTestCallSitesRoutes === 0 && runTestCallSitesExtras6 === 0,
      `routes=${runTestCallSitesRoutes} extras6=${runTestCallSitesExtras6}`);

  // 8. SANCTIONS_DEFAULTS constant does not exist anywhere in code
  // (substrate-gap source data must be deleted, not just bypassed)
  const sanctionsDefaultsExtras6 = /\bconst\s+SANCTIONS_DEFAULTS\s*[:=]/.test(extras6CodeOnly);
  const sanctionsDefaultsRoutes = /\bconst\s+SANCTIONS_DEFAULTS\s*[:=]/.test(routesCodeOnly);
  out("8. SANCTIONS_DEFAULTS constant does not exist in code (substrate-gap source deleted)",
      !sanctionsDefaultsExtras6 && !sanctionsDefaultsRoutes,
      `extras6=${sanctionsDefaultsExtras6} routes=${sanctionsDefaultsRoutes}`);

  // ── UI-static layer ──
  const uiSrc = readFileSync(join(ROOT, "client/src/pages/sanctions-pep-screening.tsx"), "utf8");

  // 9. UI page contains the retirement-disclosure card with required markers
  const uiHasCard = uiSrc.includes('data-testid="card-page-retirement-disclosure"');
  const uiHasRetired = /\bretired\b/i.test(uiSrc);
  const uiHasFu021 = uiSrc.includes("FU-021");
  const uiHas410 = uiSrc.includes("410 Gone");
  out("9. UI page contains retirement-disclosure card + 'retired' + 'FU-021' + '410 Gone'",
      uiHasCard && uiHasRetired && uiHasFu021 && uiHas410,
      `card=${uiHasCard} retired=${uiHasRetired} fu021=${uiHasFu021} gone=${uiHas410}`);

  // 10. UI page does not import useMutation, Button, or apiRequest
  // (proves action buttons are removed from code, not just hidden)
  const uiCodeOnly = stripComments(uiSrc);
  const uiHasUseMutation = /\buseMutation\b/.test(uiCodeOnly);
  const uiHasButton = /from\s+["']@\/components\/ui\/button["']/.test(uiCodeOnly) || /\bimport\s+\{[^}]*\bButton\b[^}]*\}/.test(uiCodeOnly);
  const uiHasApiRequest = /\bapiRequest\b/.test(uiCodeOnly);
  out("10. UI page does not import useMutation, Button, or apiRequest",
      !uiHasUseMutation && !uiHasButton && !uiHasApiRequest,
      `useMutation=${uiHasUseMutation} Button=${uiHasButton} apiRequest=${uiHasApiRequest}`);

  // ── DB layer (immutability guarantee) ──
  // 11. listSanctionsLists and listSanctionsScreeningTests both return >= 1 row
  // (retirement did not delete historical rows)
  const listRows = await listSanctionsLists();
  const testRows = await listSanctionsScreeningTests();
  out("11. Historical rows retained — sanctions_lists and sanctions_screening_tests both >= 1",
      listRows.length > 0 && testRows.length > 0,
      `lists=${listRows.length} tests=${testRows.length}`);

  console.log(`\n─── ${pass} passed, ${fail} failed ───`);
  process.exit(fail === 0 ? 0 : 1);
}

main().catch((e) => { console.error("verifier crashed:", e); process.exit(2); });
