/**
 * AEGIS SOAR (AS/SOAR/2026/001) — M3 CHUNK 3: goAML Form-B EXPORT battery.
 * Drives M3 chunk 3 (the READ-ONLY goAML Form-B render) to DEV-VERIFIED, then
 * HOLD at the dev gate. NO prod deploy. NO live FIA submission.
 *
 * Built on the AS/PLATFORM/2026/002 proof-harness toolkit (scripts/lib/proof-
 * harness.ts) — real-route driving (login → csrf → GET), owner (BYPASSRLS)
 * GROUND-TRUTH read-back, role-switched RLS-subject reads (rlsCountById), and
 * carry-list user cleanup. Does NOT import server/index.ts or server/routes.ts
 * (no second boot); it DOES import the PURE render module (server/lib/fincrime/
 * goaml-formb.ts has no DB import) only to assert against the SAME deployment
 * config + mandatory-field enumeration the route uses. The immutable hash-chained
 * audit rows are intentionally preserved.
 *
 * THE FOUR BINDING CRITERIA — each PAIRED (Rule 18), each verdict stating HOW it
 * was checked (Rule 19), every value asserted against owner ground-truth or the
 * shared deployment config (never the route's own echo):
 *   1. CORRECT — every rendered field equals its source: stored fact (owner read-
 *      back of case/STR/approval), documented derivation (reportCode/indicators/
 *      entityReference), or deployment-wired config (rentity/reportingPerson).
 *      Paired honesty: fiuRefNumber renders NULL because owner fia_reference IS
 *      NULL (no fabricated FIA acknowledgement).
 *   2. COMPLETE — every mandatory goAML field present (enumerateMandatoryFields);
 *      paired control: the CONDITIONAL fields (fiuRefNumber, rentityBranch) are
 *      excluded from the mandatory set and an empty conditional is NOT an
 *      incompleteness.
 *   3. ONLY-FORM-REQUIRED-PII — the decrypted narrative IS present (in reason, the
 *      disclosure); paired negatives: NO internal user ids, SHA-256 hashes, AES-GCM
 *      ciphertext, source-signal hash, or internal-shaped key name appears anywhere
 *      in the serialized payload, and the raw narrative appears ONLY in reason.
 *   4. TENANT-SCOPED — a sibling tenant reads NOWHERE (route 404 + rlsCountById(B)=0)
 *      while tenant A renders (200 + rlsCountById(A)=1 + owner ground-truth=1) — no
 *      cross-tenant oracle, isolation is RLS not an app filter or a missing row.
 *
 * Plus: the read-side PRECONDITION gate (only DISPOSITIONED/STR_RECOMMENDED with a
 * PREPARED STR renders) and the read-only FENCE (no non-GET write surface; the case
 * row + every child count is byte-identical before/after the export).
 *
 * PII discipline: this battery NEVER prints the decrypted narrative or any secret.
 */

import {
  ProofRun,
  verdict,
  login,
  sessionClient,
  call,
  getCsrf,
  Jar,
  mkSeed,
  seedUser,
  deactivateUsers,
  ownerQuery,
  ownerCount,
  rlsCountById,
  allocateActorIps,
  endOwnerPool,
  randomUUID,
  createHash,
  sleep,
  type RouteResult,
  type Session,
  type Seed,
} from "./lib/proof-harness";
import {
  getGoamlDeploymentConfig,
  enumerateMandatoryFields,
  GOAML_FORMB_MANDATORY_FIELDS,
  type GoamlFormB,
} from "../server/lib/fincrime/goaml-formb";

const SERVER = "http://127.0.0.1:5000";
const TENANT_A = "aegis-sovereign"; // the authed actor's real seeded dev tenant
const TENANT_B = "stanbic-ug-001"; // a DIFFERENT real seeded dev tenant
const base = Date.now();

// Unique free-text marker — the decrypted disclosure; must appear ONLY in reason.
const RAW_NARRATIVE = `RAWNARRATIVE-FORMB-DO-NOT-LEAK-${base}`;

const sha256 = (s: string): string => createHash("sha256").update(s, "utf8").digest("hex");

// Pull the guard reason out of the AegisException envelope:
//   { success:false, error:{ code, details:{ reason }, ... } }
function reasonOf(r: RouteResult): string | undefined {
  const error = (r.json as { error?: { details?: { reason?: unknown } } })?.error;
  const reason = error?.details?.reason;
  return typeof reason === "string" ? reason : undefined;
}

// Two timestamps name the SAME instant (both pg-driven Dates / ISO strings — sound
// because the route's drizzle reads and this harness's owner pool use the same `pg`).
function sameInstant(iso: string | null | undefined, d: Date | null | undefined): boolean {
  if (iso == null && d == null) return true;
  if (iso == null || d == null) return false;
  const t = Date.parse(iso);
  return Number.isFinite(t) && t === new Date(d).getTime();
}

// Recursively collect every object key in the payload (for the structural
// internal-key-name absence check).
function collectKeys(obj: unknown, acc: string[] = []): string[] {
  if (Array.isArray(obj)) {
    for (const v of obj) collectKeys(v, acc);
  } else if (obj && typeof obj === "object") {
    for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
      acc.push(k);
      collectKeys(v, acc);
    }
  }
  return acc;
}

const run = new ProofRun("AEGIS SOAR M3 chunk 3 — goAML Form-B EXPORT (read-only render) battery", "DEV");
const cfg = getGoamlDeploymentConfig();

// ── owner (BYPASSRLS) ground-truth read-back helpers ─────────────────────────
interface CaseFormbRow {
  state: string;
  disposition: string | null;
  disposition_reason: string | null;
  dispositioned_at: Date | null;
  str_filed: boolean;
  subject_ref: string;
  subject_ref_kind: string;
  source_type: string;
  source_signal_hash: string;
  updated_at: Date;
}
async function caseFormbRow(tenantId: string, id: string): Promise<CaseFormbRow | undefined> {
  const rows = await ownerQuery<CaseFormbRow>(
    `SELECT state, disposition, disposition_reason, dispositioned_at, str_filed,
            subject_ref, subject_ref_kind, source_type, source_signal_hash, updated_at
       FROM fincrime_cases WHERE tenant_id=$1 AND id=$2`,
    [tenantId, id],
  );
  return rows[0];
}
interface StrFormbRow {
  id: string;
  status: string;
  created_at: Date;
  fia_reference: string | null;
  filed_at: Date | null;
  acknowledged_at: Date | null;
  prepared_by_role: string;
  prepared_by_user_id: string;
  narrative_hash: string | null;
  encrypted_narrative: string | null;
}
async function strFormbRow(tenantId: string, caseId: string): Promise<StrFormbRow | undefined> {
  const rows = await ownerQuery<StrFormbRow>(
    `SELECT id, status, created_at, fia_reference, filed_at, acknowledged_at,
            prepared_by_role, prepared_by_user_id, narrative_hash, encrypted_narrative
       FROM fincrime_str_reports WHERE tenant_id=$1 AND case_id=$2
       ORDER BY created_at DESC LIMIT 1`,
    [tenantId, caseId],
  );
  return rows[0];
}
interface ApprovalFormbRow {
  approver_role: string;
  approved_at: Date;
  approver_user_id: string;
  report_hash: string;
}
async function approvalFormbRow(tenantId: string, caseId: string): Promise<ApprovalFormbRow | undefined> {
  const rows = await ownerQuery<ApprovalFormbRow>(
    `SELECT approver_role, approved_at, approver_user_id, report_hash
       FROM fincrime_str_approvals WHERE tenant_id=$1 AND case_id=$2
       ORDER BY approved_at DESC LIMIT 1`,
    [tenantId, caseId],
  );
  return rows[0];
}
// child-row counts (for the read-only fence equality)
async function childCounts(tenantId: string, caseId: string): Promise<Record<string, number>> {
  return {
    events: await ownerCount("fincrime_case_events", "tenant_id=$1 AND case_id=$2", [tenantId, caseId]),
    evidence: await ownerCount("fincrime_case_evidence", "tenant_id=$1 AND case_id=$2", [tenantId, caseId]),
    str: await ownerCount("fincrime_str_reports", "tenant_id=$1 AND case_id=$2", [tenantId, caseId]),
    approvals: await ownerCount("fincrime_str_approvals", "tenant_id=$1 AND case_id=$2", [tenantId, caseId]),
  };
}

// ── synthetic case fixtures (precondition setup, not the system under test) ────
const createdCaseIds = new Set<string>();
async function seedCase(tenantId: string, tag: string, state: string): Promise<string> {
  const id = randomUUID();
  await ownerQuery(
    `INSERT INTO fincrime_cases (id, tenant_id, source_type, source_ref, source_signal_hash, subject_ref, state)
     VALUES ($1, $2, 'MANUAL_REFERRAL', $3, $4, $5, $6)`,
    [
      id,
      tenantId,
      `ext-${tag}-${base}`,
      sha256(`${tag}-${base}-${randomUUID()}`),
      `opaque:m3c3-${tag}-${base}-TEST_DELETE_OK`,
      state,
    ],
  );
  createdCaseIds.add(id);
  return id;
}
const seedPending = (t: string, tag: string) => seedCase(t, tag, "PENDING_MLCO_REVIEW");
const seedOpen = (t: string, tag: string) => seedCase(t, tag, "OPEN");

// ── seeded synthetic users (carry-list cleanup in finally) ────────────────────
const ips = allocateActorIps(5, base);
const makerSeed = mkSeed("risk_manager", "m3c3-maker", "rm", base); // STR preparer
const adminSeed = mkSeed("admin", "m3c3-checker", "ad", base); // a DIFFERENT checker
const audSeed = mkSeed("auditor", "m3c3-aud", "au", base); // scoped read allowed
const bSeniorSeed = mkSeed("risk_manager", "m3c3-bsenior", "bs", base); // TENANT_B cross-tenant
const allSeeds: Seed[] = [makerSeed, adminSeed, audSeed, bSeniorSeed];

// drive a fresh case all the way to DISPOSITIONED/STR_RECOMMENDED with a PREPARED
// STR + one two-eyes approval (maker prepares, a DIFFERENT checker finalizes) —
// the exact substrate Form-B renders. Returns {caseId, strReportId}.
async function makeDisposedCase(
  mk: ReturnType<typeof sessionClient>,
  ad: ReturnType<typeof sessionClient>,
  tag: string,
  narrative: string,
): Promise<{ caseId: string; strReportId: string }> {
  const caseId = await seedPending(TENANT_A, tag);
  const p = await mk.post(`/api/fincrime/cases/${caseId}/str`, { narrative });
  if (p.status !== 201) throw new Error(`setup prepare failed HTTP ${p.status} for ${tag}`);
  const strReportId = p.json?.strReportId as string;
  const narrativeHash = p.json?.narrativeHash as string;
  const f = await ad.post(`/api/fincrime/cases/${caseId}/finalize`, { reportHash: narrativeHash, reason: "formb-setup-finalize" });
  if (f.status !== 200) throw new Error(`setup finalize failed HTTP ${f.status} for ${tag}`);
  await sleep();
  return { caseId, strReportId };
}

// raw fetch for an arbitrary HTTP method (the method-fence legs) bound to a session.
async function rawMethod(method: string, path: string, s: Session): Promise<number> {
  const res = await fetch(`${SERVER}${path}`, {
    method,
    headers: { cookie: s.jar.header(), "x-csrf-token": s.csrf, "x-forwarded-for": s.xff },
  });
  return res.status;
}

async function main(): Promise<void> {
  console.log("=".repeat(82));
  console.log("AEGIS SOAR M3 chunk 3 — goAML Form-B EXPORT (read-only render) battery (DEV gate)");
  console.log("spec AS/SOAR/2026/001 | env: DEV |", new Date().toISOString());
  console.log("=".repeat(82));

  try {
    await seedUser(makerSeed, TENANT_A, "FINCRIME M3C3 BATTERY maker (test, delete ok)");
    await seedUser(adminSeed, TENANT_A, "FINCRIME M3C3 BATTERY checker-admin (test, delete ok)");
    await seedUser(audSeed, TENANT_A, "FINCRIME M3C3 BATTERY auditor (test, delete ok)");
    await seedUser(bSeniorSeed, TENANT_B, "FINCRIME M3C3 BATTERY tenant-B senior (test, delete ok)");

    const maker = await login(makerSeed, ips[0], SERVER);
    const admin = await login(adminSeed, ips[1], SERVER);
    const aud = await login(audSeed, ips[2], SERVER);
    const bSenior = await login(bSeniorSeed, ips[3], SERVER);
    const ANON_IP = ips[4];

    const mk = sessionClient(maker, SERVER);
    const ad = sessionClient(admin, SERVER);
    const au = sessionClient(aud, SERVER);
    const bs = sessionClient(bSenior, SERVER);

    const formbPath = (id: string) => `/api/fincrime/cases/${id}/formb`;

    // ── GOLDEN: one disposed, renderable case reused by FB1/FB2/FB3/FB4/FB5/FB7 ─
    const { caseId: gCase, strReportId: gStr } = await makeDisposedCase(mk, ad, "golden", RAW_NARRATIVE);
    const cRow = await caseFormbRow(TENANT_A, gCase);
    const sRow = await strFormbRow(TENANT_A, gCase);
    const aRow = await approvalFormbRow(TENANT_A, gCase);

    // ── LEG FB1 — CORRECT: every rendered field == its source (owner / derivation / config)
    {
      const r = await ad.get(formbPath(gCase));
      const p = r.json as unknown as GoamlFormB;
      const checks: Array<[string, boolean]> = [
        ["http200", r.status === 200],
        // deployment-wired (the registered FIA reporting entity / MLCO account)
        ["rentityId", p?.reportHeader?.rentityId === cfg.rentityId],
        ["rentityBranch", p?.reportHeader?.rentityBranch === cfg.rentityBranch],
        ["submissionCode", p?.reportHeader?.submissionCode === cfg.submissionCode],
        ["rp.id", p?.reportingPerson?.id === cfg.reportingPerson.id],
        ["rp.firstName", p?.reportingPerson?.firstName === cfg.reportingPerson.firstName],
        ["rp.lastName", p?.reportingPerson?.lastName === cfg.reportingPerson.lastName],
        ["rp.role", p?.reportingPerson?.role === cfg.reportingPerson.role],
        // documented derivations
        ["reportCode=STR", p?.reportHeader?.reportCode === "STR"],
        ["entityReference", p?.reportHeader?.entityReference === `AEGIS-STR-${gStr}`],
        ["indicators", JSON.stringify(p?.reportIndicators) === JSON.stringify(["STR_SUSPICIOUS_ACTIVITY", "INTERNAL_FIRST_LINE_REFERRAL"])],
        ["partyType", p?.activity?.reportParty?.partyType === "OPAQUE_EXTERNAL_REFERENCE"],
        ["civilIdDeferred", p?.activity?.reportParty?.civilIdentityResolution === "DEFERRED_SEPARATELY_GATED"],
        ["significanceNonEmpty", typeof p?.activity?.significance === "string" && p.activity.significance.length > 0],
        // stored facts (owner read-back)
        ["submissionDate=dispositionedAt", sameInstant(p?.reportHeader?.submissionDate, cRow?.dispositioned_at)],
        ["reason=decryptedNarrative", p?.reason === RAW_NARRATIVE],
        ["party.reference=subjectRef", p?.activity?.reportParty?.reference === cRow?.subject_ref],
        ["party.referenceKind=subjectRefKind", p?.activity?.reportParty?.referenceKind === cRow?.subject_ref_kind],
        ["activityDate=strCreatedAt", sameInstant(p?.activity?.activityDate, sRow?.created_at)],
        ["filing.status=PREPARED", p?.filingStatus?.status === "PREPARED" && sRow?.status === "PREPARED"],
        ["filing.filed=strFiled", p?.filingStatus?.filed === false && cRow?.str_filed === false],
        // criterion-1 honesty: fiuRefNumber NULL <-> owner fia_reference NULL
        ["fiuRefNumber=null<->fiaRefNull", p?.reportHeader?.fiuRefNumber === null && sRow?.fia_reference === null],
        ["filing.fiaReference=null", p?.filingStatus?.fiaReference === null && sRow?.fia_reference === null],
        ["filing.filedAt=null", p?.filingStatus?.filedAt === null && sRow?.filed_at === null],
        ["filing.acknowledgedAt=null", p?.filingStatus?.acknowledgedAt === null && sRow?.acknowledged_at === null],
        ["gov.disposition=STR_RECOMMENDED", p?.governance?.disposition === "STR_RECOMMENDED" && cRow?.disposition === "STR_RECOMMENDED"],
        ["gov.preparedByRole=risk_manager", p?.governance?.preparedByRole === "risk_manager" && sRow?.prepared_by_role === "risk_manager"],
        ["gov.approvedByRole=admin", p?.governance?.approvedByRole === "admin" && aRow?.approver_role === "admin"],
        ["gov.approvedAt=approval.approvedAt", sameInstant(p?.governance?.approvedAt, aRow?.approved_at)],
      ];
      const failed = checks.filter(([, v]) => !v).map(([k]) => k);
      const pass = r.status === 200 && failed.length === 0;
      run.leg({
        id: "FB1",
        scenario: "CORRECT — every rendered Form-B field equals its source (owner read-back / documented derivation / deployment config)",
        expectation: "HTTP 200; all ~28 field equalities hold: deployment-wired header/reportingPerson == config, derivations (reportCode=STR, entityReference=AEGIS-STR-<strId>, indicators), stored facts == owner case/STR/approval; fiuRefNumber renders NULL BECAUSE owner fia_reference IS NULL (no fabricated ack)",
        observed: `HTTP ${r.status}; reportCode=${p?.reportHeader?.reportCode}; entityRef=${p?.reportHeader?.entityReference === `AEGIS-STR-${gStr}`}; reasonMatches=${p?.reason === RAW_NARRATIVE}; fiuRefNumber=${p?.reportHeader?.fiuRefNumber}; failed=[${failed.join(",")}] (of ${checks.length})`,
        verdict: pass
          ? verdict.verified("HTTP 200 + every field cross-checked against owner ground-truth / documented derivation / shared deployment config; fiuRefNumber null paired to owner fia_reference NULL")
          : verdict.fail(),
        pass,
        signals: { http: r.status, failedCount: failed.length, failed, fiuRefNumber: p?.reportHeader?.fiuRefNumber, reasonMatches: p?.reason === RAW_NARRATIVE },
      });
    }

    // ── LEG FB2 — COMPLETE: every mandatory field present; conditionals excluded
    {
      const r = await ad.get(formbPath(gCase));
      const p = r.json as unknown as GoamlFormB;
      const enumerated = enumerateMandatoryFields(p);
      const missing = enumerated.filter((e) => !e.present).map((e) => e.field);
      const fiuRefConditional = !GOAML_FORMB_MANDATORY_FIELDS.includes("reportHeader.fiuRefNumber") && p?.reportHeader?.fiuRefNumber === null;
      const branchConditional = !GOAML_FORMB_MANDATORY_FIELDS.includes("reportHeader.rentityBranch");
      const pass =
        r.status === 200 &&
        GOAML_FORMB_MANDATORY_FIELDS.length > 0 &&
        missing.length === 0 &&
        fiuRefConditional &&
        branchConditional;
      run.leg({
        id: "FB2",
        scenario: "COMPLETE — enumerateMandatoryFields: every mandatory goAML field present; conditional fields correctly excluded",
        expectation: `all ${GOAML_FORMB_MANDATORY_FIELDS.length} mandatory fields present (non-empty); reportHeader.fiuRefNumber is NOT mandatory and renders NULL by design (a conditional empty is not an incompleteness); reportHeader.rentityBranch is NOT mandatory`,
        observed: `HTTP ${r.status}; mandatory=${GOAML_FORMB_MANDATORY_FIELDS.length}; missing=[${missing.join(",")}]; fiuRefConditional=${fiuRefConditional}; branchConditional=${branchConditional}`,
        verdict: pass
          ? verdict.verified("every mandatory field non-empty in the rendered payload; the conditional fiuRefNumber/rentityBranch are excluded from the mandatory set — completeness is real, not faked by demoting required fields")
          : verdict.fail(),
        pass,
        signals: { http: r.status, mandatoryCount: GOAML_FORMB_MANDATORY_FIELDS.length, missing, fiuRefConditional },
      });
    }

    // ── LEG FB3 — ONLY-FORM-REQUIRED-PII: narrative present (reason) + NO internal leakage
    {
      const r = await ad.get(formbPath(gCase));
      const p = r.json as unknown as GoamlFormB;
      const serialized = JSON.stringify(p);
      // positive: the disclosure is present where required
      const narrativePresent = p?.reason === RAW_NARRATIVE;
      // raw narrative appears ONLY in reason (nowhere else once reason is blanked)
      const withoutReason = JSON.stringify({ ...p, reason: "" });
      const narrativeOnlyInReason = !withoutReason.includes(RAW_NARRATIVE);
      // value-absence: no internal id / hash / ciphertext / source-signal in the payload
      const internals: Array<[string, string | null | undefined]> = [
        ["preparedByUserId", sRow?.prepared_by_user_id],
        ["approverUserId", aRow?.approver_user_id],
        ["narrativeHash", sRow?.narrative_hash],
        ["reportHash", aRow?.report_hash],
        ["sourceSignalHash", cRow?.source_signal_hash],
        ["encryptedNarrativeCiphertext", sRow?.encrypted_narrative],
      ];
      const leaked = internals.filter(([, v]) => typeof v === "string" && v.length > 0 && serialized.includes(v)).map(([k]) => k);
      // structural: no internal-shaped key name anywhere in the payload tree
      const forbiddenKey = /hash|userid|user_id|ciphertext|encrypted|audit/i;
      const badKeys = Array.from(new Set(collectKeys(p))).filter((k) => forbiddenKey.test(k));
      const pass =
        r.status === 200 &&
        narrativePresent &&
        narrativeOnlyInReason &&
        leaked.length === 0 &&
        badKeys.length === 0;
      run.leg({
        id: "FB3",
        scenario: "ONLY-FORM-REQUIRED-PII — the decrypted narrative is present in reason; NO internal id/hash/ciphertext/source-signal value or internal-shaped key name leaks",
        expectation: "reason == decrypted narrative (positive); the raw narrative appears ONLY in reason; the prepared/approver user ids, narrative_hash, report_hash, source_signal_hash, and encrypted_narrative ciphertext appear NOWHERE in the serialized payload; no payload key matches /hash|userId|ciphertext|encrypted|audit/",
        observed: `HTTP ${r.status}; narrativePresent=${narrativePresent}; onlyInReason=${narrativeOnlyInReason}; leakedValues=[${leaked.join(",")}]; badKeys=[${badKeys.join(",")}]`,
        verdict: pass
          ? verdict.verified("narrative present (disclosure) yet confined to reason; all six internal values absent from the serialized payload AND no internal-shaped key name present — PII is EXACTLY form-required, by construction (FormBSource never carries the internals)")
          : verdict.fail(),
        pass,
        signals: { http: r.status, narrativePresent, narrativeOnlyInReason, leaked, badKeys },
      });
    }

    // ── LEG FB4 — AUTHZ: admin + risk_manager + auditor render (200, decrypted); anon rejected
    {
      const rAdmin = await ad.get(formbPath(gCase));
      const rMaker = await mk.get(formbPath(gCase));
      const rAud = await au.get(formbPath(gCase));
      const anonJar = new Jar();
      const anonCsrf = await getCsrf(anonJar, ANON_IP, SERVER);
      const rAnon = await call("GET", formbPath(gCase), anonJar, anonCsrf, undefined, ANON_IP, SERVER);
      const decryptedFor = (r: RouteResult) => (r.json as unknown as GoamlFormB)?.reason === RAW_NARRATIVE;
      const pass =
        rAdmin.status === 200 && decryptedFor(rAdmin) &&
        rMaker.status === 200 && decryptedFor(rMaker) &&
        rAud.status === 200 && decryptedFor(rAud) &&
        (rAnon.status === 401 || rAnon.status === 403);
      run.leg({
        id: "FB4",
        scenario: "AUTHZ — admin, risk_manager and scoped auditor each render (200 + decrypted narrative); unauthenticated rejected",
        expectation: "each of the three authorized roles → 200 with reason == decrypted narrative; anon (valid CSRF, no session) → 401/403 (paired positive: authorized 200s)",
        observed: `admin HTTP ${rAdmin.status} narr=${decryptedFor(rAdmin)}; maker HTTP ${rMaker.status} narr=${decryptedFor(rMaker)}; auditor HTTP ${rAud.status} narr=${decryptedFor(rAud)}; anon HTTP ${rAnon.status}`,
        verdict: pass
          ? verdict.verified("all three authorized roles get 200 + the correctly-decrypted narrative; anon rejected — read access scoped to the same roles GET /str already exposes")
          : verdict.fail(),
        pass,
        signals: { admin: rAdmin.status, maker: rMaker.status, auditor: rAud.status, anon: rAnon.status },
      });
    }

    // ── LEG FB5 — TENANT-SCOPED: sibling tenant renders NOWHERE; tenant A does (RLS)
    {
      const rA = await ad.get(formbPath(gCase)); // tenant-A senior (positive)
      const rB = await bs.get(formbPath(gCase)); // tenant-B senior, tenant-A case (negative)
      const rlsB = await rlsCountById(TENANT_B, "fincrime_cases", gCase);
      const rlsA = await rlsCountById(TENANT_A, "fincrime_cases", gCase);
      const ownerExists = await ownerCount("fincrime_cases", "tenant_id=$1 AND id=$2", [TENANT_A, gCase]);
      const pass =
        rA.status === 200 &&
        rB.status === 404 &&
        reasonOf(rB) === "formb_not_renderable" &&
        rlsB === 0 &&
        rlsA === 1 &&
        ownerExists === 1;
      run.leg({
        id: "FB5",
        scenario: "TENANT-SCOPED — tenant-B senior GET 404 (formb_not_renderable); rlsCountById(B)=0 while rlsCountById(A)=1 and owner ground-truth=1; tenant-A 200",
        expectation: "cross-tenant route read 404 with NO row data (formb_not_renderable, same as a non-existent case — no oracle); the case is invisible under the app role in tenant B (RLS) yet visible in tenant A and present in owner ground truth — isolation is RLS, not an app filter or a missing row",
        observed: `A HTTP ${rA.status}; B HTTP ${rB.status}/${reasonOf(rB)}; rlsB=${rlsB}; rlsA=${rlsA}; ownerExists=${ownerExists}`,
        verdict: pass
          ? verdict.verified("tenant-A 200 paired with tenant-B 404 (formb_not_renderable, indistinguishable from absent — no existence oracle) + role-switched rlsCountById B=0/A=1 + owner=1 — RLS-enforced isolation")
          : verdict.fail(),
        pass,
        signals: { aRoute: rA.status, bRoute: rB.status, bReason: reasonOf(rB), rlsB, rlsA, ownerExists },
      });
    }

    // ── LEG FB6 — PRECONDITION GATE: only DISPOSITIONED/STR_RECOMMENDED+PREPARED renders
    {
      // (a) bogus case id → 404
      const rBogus = await ad.get(formbPath(randomUUID()));
      // (b) OPEN case → 404
      const cOpen = await seedOpen(TENANT_A, "pre-open");
      const rOpen = await ad.get(formbPath(cOpen));
      // (c) PENDING case WITH a PREPARED STR but NOT yet disposed → 404 (has an STR, not disposed)
      const cPend = await seedPending(TENANT_A, "pre-pending-prepared");
      const pPrep = await mk.post(`/api/fincrime/cases/${cPend}/str`, { narrative: "pre-pending narrative" });
      const rPend = await ad.get(formbPath(cPend));
      // positive: the golden disposed case renders
      const rGolden = await ad.get(formbPath(gCase));
      const pass =
        rBogus.status === 404 && reasonOf(rBogus) === "formb_not_renderable" &&
        rOpen.status === 404 && reasonOf(rOpen) === "formb_not_renderable" &&
        pPrep.status === 201 &&
        rPend.status === 404 && reasonOf(rPend) === "formb_not_renderable" &&
        rGolden.status === 200;
      run.leg({
        id: "FB6",
        scenario: "PRECONDITION GATE — bogus 404; OPEN case 404; PENDING-with-PREPARED-STR (not disposed) 404; disposed golden 200",
        expectation: "render is gated on DISPOSITIONED/STR_RECOMMENDED WITH a PREPARED STR: a bogus id, an OPEN case, and a PENDING case that DOES carry a PREPARED STR all 404 formb_not_renderable; only the fully-disposed case 200s — having an STR is not sufficient, the case must be disposed",
        observed: `bogus HTTP ${rBogus.status}/${reasonOf(rBogus)}; open HTTP ${rOpen.status}/${reasonOf(rOpen)}; pendingPrepared(prep ${pPrep.status}) HTTP ${rPend.status}/${reasonOf(rPend)}; golden HTTP ${rGolden.status}`,
        verdict: pass
          ? verdict.verified("all three non-renderable states (incl. a PENDING case WITH a PREPARED STR) return 404 formb_not_renderable while the disposed golden case 200s — the precondition is disposition+PREPARED, proven against a near-miss")
          : verdict.fail(),
        pass,
        signals: { bogus: rBogus.status, open: rOpen.status, pendingPrepared: rPend.status, golden: rGolden.status },
      });
    }

    // ── LEG FB7 — READ-ONLY FENCE: no non-GET write surface; row byte-identical pre/post
    {
      const before = await caseFormbRow(TENANT_A, gCase);
      const countsBefore = await childCounts(TENANT_A, gCase);
      // method fence — POST/PUT/PATCH/DELETE to /formb (valid CSRF, admin session)
      const methods = ["POST", "PUT", "PATCH", "DELETE"];
      const methodStatuses: number[] = [];
      for (const m of methods) methodStatuses.push(await rawMethod(m, formbPath(gCase), admin));
      // the read still works
      const rGet = await ad.get(formbPath(gCase));
      await sleep();
      const after = await caseFormbRow(TENANT_A, gCase);
      const countsAfter = await childCounts(TENANT_A, gCase);
      const noWriteSurface = methodStatuses.every((s) => s === 404 || s === 405) && !methodStatuses.some((s) => s >= 200 && s < 300);
      const rowIdentical =
        !!before && !!after &&
        before.state === after.state &&
        before.disposition === after.disposition &&
        before.disposition_reason === after.disposition_reason &&
        sameInstant(before.dispositioned_at?.toISOString() ?? null, after.dispositioned_at) &&
        before.str_filed === after.str_filed &&
        sameInstant(before.updated_at.toISOString(), after.updated_at);
      const countsIdentical = JSON.stringify(countsBefore) === JSON.stringify(countsAfter);
      const pass = noWriteSurface && rGet.status === 200 && rowIdentical && countsIdentical;
      run.leg({
        id: "FB7",
        scenario: "READ-ONLY FENCE — POST/PUT/PATCH/DELETE to /formb have NO route (404/405, never 2xx); GET 200; case row + every child count byte-identical before/after",
        expectation: "no non-GET method on /formb returns 2xx (all 404/405 — only GET is registered); the GET renders 200; and the durable case row (state/disposition/reason/dispositioned_at/str_filed/updated_at) plus events/evidence/str/approvals counts are unchanged — the export mutates nothing",
        observed: `methods[POST,PUT,PATCH,DELETE]=[${methodStatuses.join(",")}]; GET ${rGet.status}; rowIdentical=${rowIdentical}; countsBefore=${JSON.stringify(countsBefore)} countsAfter=${JSON.stringify(countsAfter)}`,
        verdict: pass
          ? verdict.verified("every non-GET method 404/405 (no write route), GET 200, and owner read-back proves the case row + all child counts are byte-identical across the export — read-only fence holds")
          : verdict.fail(),
        pass,
        signals: { methodStatuses, get: rGet.status, rowIdentical, countsIdentical },
      });
    }
  } finally {
    await deactivateUsers(allSeeds.map((s) => s.id)).catch(() => {});
    console.log(
      "cleanup: synthetic seed users deactivated + credentials neutralized (audit chain preserved). " +
        `synthetic DEV case rows left in place (harmless in dev): ${createdCaseIds.size} cases.`,
    );
    await endOwnerPool();
  }

  run.report();
  await run.writeJson().catch(() => {});
  const { allPass } = run.acceptance();
  process.exit(allPass ? 0 : 1);
}

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