// scripts/verify-sanctions-retirement.ts
// FU-019 (2026-05-07) — sanctions screening latency benchmark retirement verification.
// 11 assertions across function-body, audit-log, route, source-static, and DB layers.
// Schema-vocabulary mismatch retirement (shape 2: substrate-not-built, distinct from
// FU-018 shape 1: substrate-impossible). Strict-(β) standard-application (see
// PLATFORM_BEHAVIOUR_NOTES.md). Refusal-criterion #5: real DB at every step.
//
// Invocation:  npx tsx scripts/verify-sanctions-retirement.ts
import { db } from "../server/db";
import { sanctionsScreeningRuns, auditLogs } from "@shared/schema";
import { runSanctionsScreening, listSanctionsRuns } from "../server/lib/wave13-pilot-readiness";
import { desc, eq, sql } from "drizzle-orm";
import { readFileSync } from "fs";
import { join } from "path";

const TRIGGERED_BY = "verify-sanctions-retirement-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; }
};

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

  // ── Function-body layer (load-bearing diff) ──
  // 1. runSanctionsScreening throws with FU-019 marker
  let threw = false; let msg = "";
  try {
    await runSanctionsScreening({ triggeredBy: TRIGGERED_BY, sampleSize: 100 });
  } catch (e: any) {
    threw = true; msg = String(e?.message || "");
  }
  out("1. runSanctionsScreening throws with FU-019 marker",
      threw && msg.includes("FU-019 RETIREMENT") && msg.includes("FOLLOW-UPS.md FU-019"),
      threw ? `msg.len=${msg.length}` : "did not throw");

  // 2. audit_logs row written on invocation (function-surface, three-channel pattern)
  await new Promise((r) => setTimeout(r, 250));
  const recentAudits = await db.select().from(auditLogs)
    .where(eq(auditLogs.action, "WAVE13_SANCTIONS_RETIREMENT_INVOKED"))
    .orderBy(desc(auditLogs.timestamp))
    .limit(5);
  const fnAudit = recentAudits.find((r) => r.resource === "runSanctionsScreening");
  out("2. audit_logs row written with WAVE13_SANCTIONS_RETIREMENT_INVOKED action (function-surface)",
      !!fnAudit,
      fnAudit ? `id=${fnAudit.id} resource=${fnAudit.resource}` : `none of ${recentAudits.length} recent matched resource=runSanctionsScreening`);

  // ── Route layer ──
  // 3. Route 410 with retiredAt + code + retainedReceipts:36 fields. Static check
  // (no auth-bearing test client wired; route handler is a single sync expression with literals).
  const routesSrc = readFileSync(join(ROOT, "server/routes.ts"), "utf8");
  const routeBlock = routesSrc.slice(routesSrc.indexOf("// 5. Sanctions Latency Benchmark"), routesSrc.indexOf("/api/wave13/sanctions/runs") + 200);
  const has410 = routeBlock.includes("status(410)");
  const hasRetiredAt = routeBlock.includes('retiredAt: "2026-05-07"');
  const hasCode = routeBlock.includes('"WAVE13_SANCTIONS_RETIRED"');
  const hasHistorical = routeBlock.includes("retainedReceipts: 36");
  out("3. POST route returns 410 with retiredAt + code + historical fields",
      has410 && hasRetiredAt && hasCode && hasHistorical,
      `410=${has410} retiredAt=${hasRetiredAt} code=${hasCode} historical=${hasHistorical}`);

  // ── DB layer (immutability) ──
  // 4. sanctions_screening_runs row count is 36 (frozen, no new writes)
  const [{ n }] = await db.select({ n: sql<number>`count(*)::int` }).from(sanctionsScreeningRuns);
  out("4. sanctions_screening_runs row count = 36 (frozen)",
      Number(n) === 36, `count=${n}`);

  // 10. listSanctionsRuns returns same 36 rows (read path preserved)
  const listed = await listSanctionsRuns(100);
  out("10. listSanctionsRuns returns same 36 rows (read path preserved)",
      Array.isArray(listed) && listed.length === 36,
      `listed=${listed.length}`);

  // ── Static-source layer (touch-point coverage) ──
  const w13Src = readFileSync(join(ROOT, "server/lib/wave13-pilot-readiness.ts"), "utf8");
  const extras8Src = readFileSync(join(ROOT, "server/lib/pilot-readiness-extras-8.ts"), "utf8");
  const uiSrc = readFileSync(join(ROOT, "client/src/pages/wave13-pilot-readiness.tsx"), "utf8");
  const finalRoutesSrc = readFileSync(join(ROOT, "server/lib/wave-final-routes.ts"), "utf8");

  // 5. Smoke driver no longer references sanctions-screen as runnable entry
  const smokeHasSancEntry = /\{\s*module:\s*"sanctions-screen"\s*,\s*exec:/.test(w13Src);
  out("5. smoke driver no longer has runnable sanctions-screen entry (13→12 modules)",
      !smokeHasSancEntry, smokeHasSancEntry ? "entry still present" : "removed");

  // 6. Monthly-attestation rollup excludes sanctions
  const summaryHasSanc = /id:\s*"sanctions"/.test(w13Src);
  out("6. wave13Summary monthly-attestation rollup excludes sanctions",
      !summaryHasSanc, summaryHasSanc ? 'id: "sanctions" still present' : "removed");

  // 7. Scoreboard destructure has 22 vars (was 23 after FU-018, now FU-019 removes w13Sanc).
  // Strip line and block comments before checking for w13Sanc as a code identifier
  // (FU-019 banner mentions "w13Sanc removed" so naive grep matches the comment).
  const destructureMatch = extras8Src.match(/const\s+\[\s*(canary[^\]]+)\s*\]\s*=\s*await\s+Promise\.all\(/);
  const varCount = destructureMatch
    ? destructureMatch[1].split(",").map((s) => s.trim()).filter((s) => s.length > 0 && !s.startsWith("//")).length
    : -1;
  const extras8CodeOnly = extras8Src
    .replace(/\/\*[\s\S]*?\*\//g, "")
    .split("\n")
    .map((l) => l.replace(/\/\/.*$/, ""))
    .join("\n");
  const hasW13Sanc = /\bw13Sanc\b/.test(extras8CodeOnly);
  out("7. scoreboard destructure has 22 vars (w13Sanc removed from code, not just comments)",
      varCount === 22 && !hasW13Sanc, `varCount=${varCount} w13SancInCode=${hasW13Sanc}`);

  // 8. Modules list count is 12 wave13-* entries (was 13 with wave13-sanctions)
  const wave13Modules = (extras8Src.match(/module:\s*"wave13-[a-z-]+"/g) || []);
  const hasSancModule = wave13Modules.some((m) => m.includes("wave13-sanctions"));
  out("8. modules list has 12 wave13-* entries (wave13-sanctions removed)",
      wave13Modules.length === 12 && !hasSancModule,
      `wave13ModuleCount=${wave13Modules.length} hasSancModule=${hasSancModule}`);

  // 9. UI disclosure panel renders the retirement date string + key markers
  const uiHasDate = uiSrc.includes("Retired 2026-05-07");
  const uiHasDisclosure = uiSrc.includes('data-testid="text-sanctions-disclosure"');
  const uiHasHistContainer = uiSrc.includes('data-testid="container-sanctions-historical"');
  const uiHasHideRunBtnProp = /hideRunButton\?:\s*boolean/.test(uiSrc);
  out("9. UI disclosure panel renders date + disclosure + historical container + hideRunButton prop",
      uiHasDate && uiHasDisclosure && uiHasHistContainer && uiHasHideRunBtnProp,
      `date=${uiHasDate} disclosure=${uiHasDisclosure} histContainer=${uiHasHistContainer} hideRunBtnProp=${uiHasHideRunBtnProp}`);

  // 11. No stale "13 modules" / "13 active" text in Wave-13 surfaces (audit consistency).
  // Extended scope (Option A, operator-approved 2026-05-07): also checks file-banner at
  // wave13-pilot-readiness.ts:1 and runtime log line at wave-final-routes.ts:367.
  // Permitted exceptions: comments documenting 14→13→12 chronology
  // (i.e. "Was 14"/"(was 14"/"14→13"/"13→12" parentheticals).
  const stripChronology = (s: string) =>
    s.split("\n").filter((l) => !/Was\s+1[34]|\(was\s+1[34]|1[34]\s*→\s*1[23]/i.test(l)).join("\n");
  const w13Stripped = stripChronology(w13Src);
  const extras8Stripped = stripChronology(extras8Src);
  const uiStripped = stripChronology(uiSrc);
  const finalRoutesStripped = stripChronology(finalRoutesSrc);
  // Negative lookbehind on [Ww]ave-? prevents matching the "13" inside "Wave-13" or
  // "wave13" (which appear in legitimate phrases like "all 12 active Wave-13 modules"
  // and "across wave13 modules"). The "13" we want to flag is a standalone count, not
  // the platform-name suffix. extras8 + final-routes patterns unchanged — they already
  // require leading parens that exclude the substring-collision case.
  // (Substring-collision pattern: see PLATFORM_BEHAVIOUR_NOTES.md verifier-substring-
  // collision entry; FU-018 hit it on w13Adv-in-comment, FU-019 on 13-in-Wave-13.)
  const stale13 = [
    ...Array.from(w13Stripped.matchAll(/(?<![Ww]ave-?)\b13\s+(active\s+)?modules/gi)).map((m) => `w13:${m[0]}`),
    ...Array.from(extras8Stripped.matchAll(/Wave-13[^\n]*\(13\s+(active\s+)?modules\)/gi)).map((m) => `extras8:${m[0]}`),
    ...Array.from(uiStripped.matchAll(/(?<![Ww]ave-?)\b13\s+(active\s+)?modules|Running\s+13\s+modules/gi)).map((m) => `ui:${m[0]}`),
    ...Array.from(finalRoutesStripped.matchAll(/\(13\s+modules,/gi)).map((m) => `final-routes:${m[0]}`),
  ];
  out("11. no stale '13 modules' text in Wave-13 surfaces (incl. Option-A extensions: file-banner + log line)",
      stale13.length === 0,
      stale13.length === 0 ? "all updated to 12" : `still ${stale13.length} stale ref(s): ${stale13.join(" | ")}`);

  console.log(`\n─── ${pass}/${pass + fail} assertions passed ───`);
  if (fail > 0) console.log(`${fail} FAILED — see PASS/FAIL log above`);
  process.exit(fail > 0 ? 1 : 0);
}

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