/**
 * AEGIS SOAR (AS/SOAR/2026/001) — M2 INVESTIGATION-ROUTE BEHAVIORAL BATTERY.
 * Spec/plan: docs/regulatory/AEGIS_SOAR_M2_INVESTIGATION_PLAN.md (APPROVED).
 * Sub-gates S2–S4 to DEV-VERIFIED, then HOLD at the dev gate.
 *
 * Drives the REAL running dev server (:5000) exactly as an authenticated client
 * would — login → csrf → POST/GET — then reads the durable effect back out-of-band
 * two ways:
 *   • owner pool (DATABASE_URL = BYPASSRLS) for GROUND TRUTH (does the row exist
 *     at all, in ANY tenant — the only honest way to prove a cross-tenant row was
 *     NOT created), and
 *   • withTenantRls(tenant) (the app role with the tenant GUC set) for ROLE-
 *     SWITCHED RLS-isolation reads (tenant A sees its case; tenant B does not).
 *
 * Rule 18 (paired controls): every denial is paired, in the SAME run, with a
 * positive enforcement, so no green is HOLLOW-GREEN.
 * Rule 19 (verified-vs-assumed): each verdict states HOW it was checked (HTTP
 * status + which read-back connection proved the durable effect).
 *
 * M3 FENCE (this battery's central negative discipline): M2 advances a case
 * OPEN → UNDER_INVESTIGATION → PENDING_MLCO_REVIEW and STOPS. The battery proves,
 * RED-THEN-GREEN, that disposition/closure/STR are unreachable from M2:
 *   (red)   a body that injects state/disposition/disposition_reason/
 *           dispositioned_at/str_filed/tenant_id is a 400 (strict DTO);
 *   (green) the identical body WITHOUT the fenced key succeeds — proving the
 *           rejection is the fence, not a generally-broken endpoint;
 *   (red)   no disposing route exists (a CSRF+session-valid POST to a disposing
 *           path 404s while a real route reaches its handler);
 *   (red)   a PENDING_MLCO_REVIEW case cannot be advanced further (terminal);
 *   (green) no case ever becomes DISPOSITIONED and disposition columns stay NULL.
 *
 * PII discipline: free-text note/summary is AES-256-GCM at rest; the battery
 * asserts the raw plaintext appears in NO encrypted_note, NO event_payload, and
 * NO audit-chain details for the case.
 *
 * Side-effect discipline: imports only withTenantRls + pg + bcryptjs + node:crypto.
 * Does NOT import server/index.ts or server/routes.ts (no second server boot). All
 * synthetic seed users/cases/events/evidence are cleaned up in `finally`; the
 * immutable hash-chained audit_chain_entries rows are intentionally preserved.
 */

import pg from "pg";
import bcrypt from "bcryptjs";
import { randomUUID, createHash } from "node:crypto";
import { sql } from "drizzle-orm";
import { withTenantRls } from "../server/db";

const owner = new pg.Pool({ connectionString: process.env.DATABASE_URL });

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

// Per-actor source IPs (RFC 5737 TEST-NET-3). The login limiter is keyed by IP
// (5/15min) and counts persist in the live server across harness runs, so each
// actor gets its OWN IP (1 login each) and the octet rotates per run.
const ipOct = (idx: number) => ((Math.floor(base / 1000) + idx * 7) % 240) + 1;
const RM_XFF = `203.0.113.${ipOct(0)}`;
const ADMIN_XFF = `203.0.113.${ipOct(1)}`;
const AUD_XFF = `203.0.113.${ipOct(2)}`;
const ANON_XFF = `203.0.113.${ipOct(3)}`;

// Unique free-text markers — must NEVER appear raw at rest or in any payload.
const RAW_NOTE = `RAWNOTE-DO-NOT-LEAK-${base}`;
const RAW_SUMMARY = `RAWSUMMARY-DO-NOT-LEAK-${base}`;

const settle = (ms = 500) => new Promise((r) => setTimeout(r, ms));

interface LegResult {
  id: string;
  scenario: string;
  expectation: string;
  observed: string;
  verdict: string;
  pass: boolean;
}
const results: LegResult[] = [];
const rec = (r: LegResult) => results.push(r);
const createdCaseIds = new Set<string>();

// ── owner (BYPASSRLS) ground-truth read-back helpers ─────────────────────────
interface CaseRow {
  id: string;
  tenant_id: string;
  state: string;
  assigned_investigator_id: string | null;
  disposition: string | null;
  disposition_reason: string | null;
  dispositioned_at: Date | null;
  str_filed: boolean;
}
async function caseRow(tenantId: string, id: string): Promise<CaseRow | undefined> {
  const r = await owner.query<CaseRow>(
    "SELECT id, tenant_id, state, assigned_investigator_id, disposition, disposition_reason, dispositioned_at, str_filed FROM fincrime_cases WHERE tenant_id=$1 AND id=$2",
    [tenantId, id],
  );
  return r.rows[0];
}
interface EventRow {
  event_type: string;
  from_state: string | null;
  to_state: string | null;
  event_payload: string;
  payload_hash: string;
  audit_chain_entry_id: number | null;
}
async function eventsFor(tenantId: string, caseId: string): Promise<EventRow[]> {
  const r = await owner.query<EventRow>(
    "SELECT event_type, from_state, to_state, event_payload, payload_hash, audit_chain_entry_id FROM fincrime_case_events WHERE tenant_id=$1 AND case_id=$2 ORDER BY created_at",
    [tenantId, caseId],
  );
  return r.rows;
}
interface EvidenceRow {
  evidence_type: string;
  opaque_ref: string | null;
  encrypted_note: string | null;
  payload_hash: string;
  audit_chain_entry_id: number | null;
}
async function evidenceFor(tenantId: string, caseId: string): Promise<EvidenceRow[]> {
  const r = await owner.query<EvidenceRow>(
    "SELECT evidence_type, opaque_ref, encrypted_note, payload_hash, audit_chain_entry_id FROM fincrime_case_evidence WHERE tenant_id=$1 AND case_id=$2 ORDER BY created_at",
    [tenantId, caseId],
  );
  return r.rows;
}
async function attemptCount(action: string, caseId: string): Promise<number> {
  const r = await owner.query<{ n: string }>(
    "SELECT count(*)::int AS n FROM audit_chain_entries WHERE action=$1 AND resource=$2",
    [action, `fincrime_case:${caseId}`],
  );
  return Number(r.rows[0].n);
}
// role-switched (RLS-subject) visibility: count under the app role with the
// tenant GUC set — proves RLS actually scopes the row, not just an app filter.
async function rlsVisible(tenantId: string, id: string): Promise<number> {
  return withTenantRls(tenantId, async (tdb) => {
    const res: any = await tdb.execute(
      sql`SELECT count(*)::int AS n FROM fincrime_cases WHERE id = ${id}`,
    );
    return Number(res.rows[0].n);
  });
}
// raw-plaintext leak probe across every at-rest surface for a case.
async function leak(tenantId: string, caseId: string, needle: string) {
  const ev = await owner.query<{ encrypted_note: string | null }>(
    "SELECT encrypted_note FROM fincrime_case_evidence WHERE tenant_id=$1 AND case_id=$2",
    [tenantId, caseId],
  );
  const evHit = ev.rows.some((r) => (r.encrypted_note ?? "").includes(needle));
  const evt = await owner.query<{ event_payload: string }>(
    "SELECT event_payload FROM fincrime_case_events WHERE tenant_id=$1 AND case_id=$2",
    [tenantId, caseId],
  );
  const evtHit = evt.rows.some((r) => (r.event_payload ?? "").includes(needle));
  const aud = await owner.query<{ details: string }>(
    "SELECT details FROM audit_chain_entries WHERE resource=$1",
    [`fincrime_case:${caseId}`],
  );
  const audHit = aud.rows.some((r) => (r.details ?? "").includes(needle));
  return { evHit, evtHit, audHit, any: evHit || evtHit || audHit };
}
async function dispositionedAmong(ids: string[]): Promise<number> {
  if (!ids.length) return 0;
  const r = await owner.query<{ n: string }>(
    "SELECT count(*)::int AS n FROM fincrime_cases WHERE id = ANY($1::varchar[]) AND state = 'DISPOSITIONED'",
    [ids],
  );
  return Number(r.rows[0].n);
}
async function withDispositionAmong(ids: string[]): Promise<number> {
  if (!ids.length) return 0;
  const r = await owner.query<{ n: string }>(
    "SELECT count(*)::int AS n FROM fincrime_cases WHERE id = ANY($1::varchar[]) AND (disposition IS NOT NULL OR disposition_reason IS NOT NULL OR dispositioned_at IS NOT NULL OR str_filed = true)",
    [ids],
  );
  return Number(r.rows[0].n);
}
// STR-report owner read-back: a well-formed STR probe rejected at a STATE precondition
// must have created NO fincrime_str_reports row (the write never reached the insert).
async function strReportCount(tenantId: string, caseId: string): Promise<number> {
  const r = await owner.query<{ n: string }>(
    "SELECT count(*)::int AS n FROM fincrime_str_reports WHERE tenant_id=$1 AND case_id=$2",
    [tenantId, caseId],
  );
  return Number(r.rows[0].n);
}

// ── synthetic OPEN-case fixture (precondition setup, not the system under test) ─
async function seedOpenCase(tenantId: string, tag: string): Promise<string> {
  const id = randomUUID();
  await owner.query(
    `INSERT INTO fincrime_cases (id, tenant_id, source_type, source_ref, source_signal_hash, subject_ref)
     VALUES ($1, $2, 'MANUAL_REFERRAL', $3, $4, $5)`,
    [
      id,
      tenantId,
      `ext-${tag}-${base}`,
      createHash("sha256").update(`${tag}-${base}-${randomUUID()}`).digest("hex"),
      `opaque:m2-${tag}-${base}`,
    ],
  );
  createdCaseIds.add(id);
  return id;
}

// ── HTTP cookie jar + login/call helpers ─────────────────────────────────────
class Jar {
  private cookies = new Map<string, string>();
  ingest(res: Response) {
    const setCookies =
      (res.headers as unknown as { getSetCookie?: () => string[] }).getSetCookie?.() ?? [];
    for (const sc of setCookies) {
      const pair = sc.split(";")[0];
      const idx = pair.indexOf("=");
      if (idx > 0) this.cookies.set(pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
    }
  }
  header(): string {
    return [...this.cookies.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
  }
}

interface Session {
  jar: Jar;
  csrf: string;
  xff: string;
  userId: string;
}

async function getCsrf(jar: Jar, xff: string): Promise<string> {
  const res = await fetch(`${SERVER}/api/csrf-token`, {
    headers: { cookie: jar.header(), "x-forwarded-for": xff },
  });
  jar.ingest(res);
  return ((await res.json().catch(() => ({}))) as any)?.csrfToken;
}

async function login(seed: Seed, xff: string): Promise<Session> {
  const jar = new Jar();
  const res = await fetch(`${SERVER}/api/auth/login`, {
    method: "POST",
    headers: { "content-type": "application/json", "x-forwarded-for": xff },
    body: JSON.stringify({ username: seed.username, password: seed.password }),
  });
  jar.ingest(res);
  const j: any = await res.json().catch(() => ({}));
  if (res.status !== 200 || j?.success !== true) {
    throw new Error(`login(${seed.username}) failed HTTP ${res.status} ${JSON.stringify(j).slice(0, 160)}`);
  }
  const csrf = await getCsrf(jar, xff);
  return { jar, csrf, xff, userId: seed.id };
}

async function call(
  method: "GET" | "POST",
  path: string,
  jar: Jar | null,
  csrf: string | null,
  body: unknown,
  xff: string,
): Promise<{ status: number; json: any }> {
  const headers: Record<string, string> = { "x-forwarded-for": xff };
  if (body !== undefined) headers["content-type"] = "application/json";
  if (jar) headers.cookie = jar.header();
  if (csrf) headers["x-csrf-token"] = csrf;
  const res = await fetch(`${SERVER}${path}`, {
    method,
    headers,
    body: body !== undefined ? JSON.stringify(body) : undefined,
  });
  const json: any = await res.json().catch(() => ({}));
  return { status: res.status, json };
}
const post = (s: Session, path: string, body: unknown) =>
  call("POST", path, s.jar, s.csrf, body, s.xff);
const get = (s: Session, path: string) => call("GET", path, s.jar, s.csrf, undefined, s.xff);

// ── seeded synthetic users (cleaned up in finally) ───────────────────────────
interface Seed {
  id: string;
  username: string;
  password: string;
  role: "admin" | "risk_manager" | "auditor";
}
function mkSeed(role: Seed["role"], tag: string): Seed {
  return {
    id: randomUUID(),
    username: `fc-m2-${tag}-${base}`,
    password: `Fc!M2-${tag}-${base}`,
    role,
  };
}
const adminSeed = mkSeed("admin", "admin");
const rmSeed = mkSeed("risk_manager", "rm");
const audSeed = mkSeed("auditor", "aud");
const rm2Seed = mkSeed("risk_manager", "rm2"); // valid assignee, never logs in
const bUserSeed = mkSeed("risk_manager", "buser"); // tenant-B only; "none" in A
const loginSeeds = [adminSeed, rmSeed, audSeed];
const allSeeds = [adminSeed, rmSeed, audSeed, rm2Seed, bUserSeed];

async function seedUser(s: Seed, tenantId: string): Promise<void> {
  await owner.query(
    `INSERT INTO users (id, username, password, role, full_name, is_active)
     VALUES ($1, $2, $3, $4, $5, true)`,
    [s.id, s.username, bcrypt.hashSync(s.password, 10), s.role, `FINCRIME M2 BATTERY ${s.role} (test, delete ok)`],
  );
  await owner.query(
    `INSERT INTO user_tenant_roles (user_id, tenant_id, role, is_active, is_primary)
     VALUES ($1, $2, $3, true, true)`,
    [s.id, tenantId, s.role],
  );
}

// ─────────────────────────────────────────────────────────────────────────────
async function main() {
  console.log("=".repeat(82));
  console.log("AEGIS SOAR M2 Investigation — routes behavioral battery (S2–S4 dev gate)");
  console.log("spec AS/SOAR/2026/001 | env: DEV |", new Date().toISOString());
  console.log("=".repeat(82));

  try {
    await seedUser(adminSeed, TENANT_A);
    await seedUser(rmSeed, TENANT_A);
    await seedUser(audSeed, TENANT_A);
    await seedUser(rm2Seed, TENANT_A);
    await seedUser(bUserSeed, TENANT_B);

    const admin = await login(adminSeed, ADMIN_XFF);
    const rm = await login(rmSeed, RM_XFF);
    const aud = await login(audSeed, AUD_XFF);

    // ── LEG 1a — POSITIVE: risk_manager assigns an OPEN case ──────────────────
    const c1 = await seedOpenCase(TENANT_A, "assign-pos");
    const r1a = await post(rm, `/api/fincrime/cases/${c1}/assign`, { assigneeUserId: rm.userId });
    await settle();
    const row1a = await caseRow(TENANT_A, c1);
    const ev1a = await eventsFor(TENANT_A, c1);
    const at1a = await attemptCount("FINCRIME_CASE_ASSIGN_ATTEMPT", c1);
    const assigned = ev1a.filter((e) => e.event_type === "CASE_ASSIGNED");
    const pass1a =
      r1a.status === 200 &&
      r1a.json?.state === "UNDER_INVESTIGATION" &&
      row1a?.state === "UNDER_INVESTIGATION" &&
      row1a?.assigned_investigator_id === rm.userId &&
      row1a?.disposition === null &&
      row1a?.disposition_reason === null &&
      row1a?.dispositioned_at === null &&
      row1a?.str_filed === false &&
      assigned.length === 1 &&
      assigned[0].from_state === "OPEN" &&
      assigned[0].to_state === "UNDER_INVESTIGATION" &&
      /^[0-9a-f]{64}$/.test(assigned[0].payload_hash) &&
      typeof assigned[0].audit_chain_entry_id === "number" &&
      at1a >= 1;
    rec({
      id: "1a",
      scenario: "POSITIVE risk_manager assigns an OPEN case (OPEN→UNDER_INVESTIGATION)",
      expectation:
        "HTTP 200 state UNDER_INVESTIGATION; durable case advanced + assignee set; disposition cols NULL; exactly one CASE_ASSIGNED event (OPEN→UNDER_INVESTIGATION, 64-hex payload_hash, audit FK); ≥1 ASSIGN_ATTEMPT audit row",
      observed: `HTTP ${r1a.status}; state=${row1a?.state}; assignee=${row1a?.assigned_investigator_id === rm.userId}; disp=${row1a?.disposition}/${row1a?.disposition_reason}/${row1a?.dispositioned_at}; str=${row1a?.str_filed}; CASE_ASSIGNED=${assigned.length}(${assigned[0]?.from_state}->${assigned[0]?.to_state} fk=${assigned[0]?.audit_chain_entry_id}); attemptAudits=${at1a}`,
      verdict: pass1a
        ? "PASS — functionally-verified (HTTP 200 + owner read-back of case+event+audit)"
        : "FAIL",
      pass: pass1a,
    });

    // ── LEG 1b — DENIAL: auditor cannot mutate (paired with 1a) ───────────────
    const c1b = await seedOpenCase(TENANT_A, "auditor-deny");
    const r1b = await post(aud, `/api/fincrime/cases/${c1b}/assign`, { assigneeUserId: rm.userId });
    await settle();
    const row1b = await caseRow(TENANT_A, c1b);
    const ev1b = await eventsFor(TENANT_A, c1b);
    const pass1b = r1b.status === 403 && row1b?.state === "OPEN" && ev1b.length === 0;
    rec({
      id: "1b",
      scenario: "DENIAL authenticated auditor attempts assign (role not admin/risk_manager)",
      expectation: "HTTP 403; case unchanged (still OPEN); ZERO events (paired positive: 1a risk_manager allowed)",
      observed: `HTTP ${r1b.status}; state=${row1b?.state}; events=${ev1b.length}`,
      verdict: pass1b
        ? "PASS — functionally-verified (HTTP 403 + owner read-back proves no mutation)"
        : "FAIL",
      pass: pass1b,
    });

    // ── LEG 1c — DENIAL: unauthenticated (valid CSRF, no session) ─────────────
    const c1c = await seedOpenCase(TENANT_A, "unauth-deny");
    const anonJar = new Jar();
    const anonCsrf = await getCsrf(anonJar, ANON_XFF);
    const r1c = await call("POST", `/api/fincrime/cases/${c1c}/assign`, anonJar, anonCsrf, { assigneeUserId: rm.userId }, ANON_XFF);
    await settle();
    const row1c = await caseRow(TENANT_A, c1c);
    const pass1c = (r1c.status === 401 || r1c.status === 403) && row1c?.state === "OPEN";
    rec({
      id: "1c",
      scenario: "DENIAL unauthenticated assign (CSRF satisfied, no session)",
      expectation: "HTTP 401/403; case unchanged (paired positive: 1a authenticated)",
      observed: `HTTP ${r1c.status}; state=${row1c?.state}`,
      verdict: pass1c
        ? "PASS — functionally-verified (rejected unauthenticated + owner read-back proves no mutation)"
        : "FAIL",
      pass: pass1c,
    });

    // ── LEG 1d — POSITIVE: admin starts an investigation WITH a free-text note ─
    const c1d = await seedOpenCase(TENANT_A, "start-pos");
    const r1d = await post(admin, `/api/fincrime/cases/${c1d}/start`, { note: RAW_NOTE });
    await settle();
    const row1d = await caseRow(TENANT_A, c1d);
    const ev1d = await eventsFor(TENANT_A, c1d);
    const evid1d = await evidenceFor(TENANT_A, c1d);
    const at1d = await attemptCount("FINCRIME_INVESTIGATION_START_ATTEMPT", c1d);
    const started = ev1d.filter((e) => e.event_type === "INVESTIGATION_STARTED");
    const evAdded = ev1d.filter((e) => e.event_type === "EVIDENCE_ADDED");
    const lk1d = await leak(TENANT_A, c1d, RAW_NOTE);
    const pass1d =
      r1d.status === 200 &&
      r1d.json?.state === "UNDER_INVESTIGATION" &&
      typeof r1d.json?.evidenceId === "string" &&
      row1d?.state === "UNDER_INVESTIGATION" &&
      started.length === 1 &&
      started[0].from_state === "OPEN" &&
      started[0].to_state === "UNDER_INVESTIGATION" &&
      evAdded.length === 1 &&
      evid1d.length === 1 &&
      evid1d[0].evidence_type === "INVESTIGATION_NOTE" &&
      evid1d[0].encrypted_note !== null &&
      !evid1d[0].encrypted_note!.includes(RAW_NOTE) &&
      /^[0-9a-f]{64}$/.test(evid1d[0].payload_hash) &&
      typeof evid1d[0].audit_chain_entry_id === "number" &&
      at1d >= 1 &&
      lk1d.any === false;
    rec({
      id: "1d",
      scenario: "POSITIVE admin starts investigation with a free-text note (transition + encrypted evidence + audit)",
      expectation:
        "HTTP 200 UNDER_INVESTIGATION + evidenceId; one INVESTIGATION_STARTED + one EVIDENCE_ADDED event; one encrypted INVESTIGATION_NOTE evidence row (encrypted_note set, raw note ABSENT); ≥1 START_ATTEMPT audit; raw note appears in NO at-rest surface",
      observed: `HTTP ${r1d.status}; state=${row1d?.state}; STARTED=${started.length}; EVIDENCE_ADDED=${evAdded.length}; evidence=${evid1d.length}(${evid1d[0]?.evidence_type} enc=${evid1d[0]?.encrypted_note !== null}); attemptAudits=${at1d}; leak{ev=${lk1d.evHit},evt=${lk1d.evtHit},aud=${lk1d.audHit}}`,
      verdict: pass1d
        ? "PASS — functionally-verified (HTTP 200 + owner read-back: transition+event+encrypted evidence; raw-note leak probe negative on all 3 surfaces)"
        : "FAIL",
      pass: pass1d,
    });

    // ── LEG 2a — M3 FENCE (assign): inject state → 400 (red); clean → 200 (green)
    const c2 = await seedOpenCase(TENANT_A, "fence-assign");
    const r2red = await post(rm, `/api/fincrime/cases/${c2}/assign`, { assigneeUserId: rm.userId, state: "DISPOSITIONED" });
    const rowMid = await caseRow(TENANT_A, c2);
    const r2green = await post(rm, `/api/fincrime/cases/${c2}/assign`, { assigneeUserId: rm.userId });
    await settle();
    const row2 = await caseRow(TENANT_A, c2);
    const pass2a =
      r2red.status === 400 &&
      rowMid?.state === "OPEN" && // rejected body changed nothing
      r2green.status === 200 &&
      row2?.state === "UNDER_INVESTIGATION" &&
      row2?.disposition === null;
    rec({
      id: "2a",
      scenario: "M3-FENCE assign — body injects state=DISPOSITIONED (red) then clean assign (green)",
      expectation: "RED: HTTP 400 (strict DTO rejects 'state'), case still OPEN; GREEN: clean assign HTTP 200 → UNDER_INVESTIGATION, disposition still NULL",
      observed: `red HTTP ${r2red.status} (midState=${rowMid?.state}); green HTTP ${r2green.status} (state=${row2?.state}, disp=${row2?.disposition})`,
      verdict: pass2a
        ? "PASS — functionally-verified (fenced key 400 leaves OPEN; identical clean body succeeds — rejection IS the fence)"
        : "FAIL",
      pass: pass2a,
    });

    // ── LEG 2b — M3 FENCE (start/evidence): every fenced key → 400; clean → ok ─
    const c2b = await seedOpenCase(TENANT_A, "fence-start");
    const startFenced = [
      { note: "x", disposition: "CLEARED" },
      { note: "x", disposition_reason: "because" },
      { note: "x", dispositioned_at: new Date().toISOString() },
      { note: "x", str_filed: true },
      { note: "x", state: "PENDING_MLCO_REVIEW" },
    ];
    const startFencedStatuses: number[] = [];
    for (const b of startFenced) {
      const r = await post(rm, `/api/fincrime/cases/${c2b}/start`, b);
      startFencedStatuses.push(r.status);
    }
    const rowAfterFenced = await caseRow(TENANT_A, c2b);
    const r2bGreen = await post(rm, `/api/fincrime/cases/${c2b}/start`, { note: "clean-start" });
    await settle();
    const row2b = await caseRow(TENANT_A, c2b);
    // evidence fenced
    const c2c = await seedOpenCase(TENANT_A, "fence-evidence");
    const r2cRed = await post(rm, `/api/fincrime/cases/${c2c}/evidence`, { evidenceType: "DOCUMENT_REF", opaqueRef: `opaque:doc-${base}`, state: "DISPOSITIONED" });
    const r2cGreen = await post(rm, `/api/fincrime/cases/${c2c}/evidence`, { evidenceType: "DOCUMENT_REF", opaqueRef: `opaque:doc-${base}` });
    await settle();
    const evid2c = await evidenceFor(TENANT_A, c2c);
    const pass2b =
      startFencedStatuses.every((s) => s === 400) &&
      rowAfterFenced?.state === "OPEN" &&
      r2bGreen.status === 200 &&
      row2b?.state === "UNDER_INVESTIGATION" &&
      r2cRed.status === 400 &&
      r2cGreen.status === 201 &&
      evid2c.length === 1; // only the green evidence landed
    rec({
      id: "2b",
      scenario: "M3-FENCE start/evidence — disposition/state/str_filed keys (red) vs clean body (green)",
      expectation: "every fenced start body 400 (case stays OPEN); clean start 200; evidence fenced 400 then clean 201 with exactly one durable evidence row",
      observed: `startFenced=[${startFencedStatuses.join(",")}] midState=${rowAfterFenced?.state}; cleanStart HTTP ${r2bGreen.status}->${row2b?.state}; evidence red ${r2cRed.status}/green ${r2cGreen.status} rows=${evid2c.length}`,
      verdict: pass2b
        ? "PASS — functionally-verified (all fenced keys 400 + clean bodies succeed; owner read-back confirms only clean evidence landed)"
        : "FAIL",
      pass: pass2b,
    });

    // ── LEG 2d — M3 FENCE (escalate): inject disposition → 400; case unchanged ─
    // c2 is UNDER_INVESTIGATION from 2a; escalate is its only legal next edge.
    const r2dRed = await post(rm, `/api/fincrime/cases/${c2}/escalate-mlco`, { summary: "y", disposition: "STR_RECOMMENDED" });
    await settle();
    const row2d = await caseRow(TENANT_A, c2);
    const pass2d = r2dRed.status === 400 && row2d?.state === "UNDER_INVESTIGATION" && row2d?.disposition === null;
    rec({
      id: "2d",
      scenario: "M3-FENCE escalate — body injects disposition=STR_RECOMMENDED (red)",
      expectation: "HTTP 400 (strict DTO rejects 'disposition'); case stays UNDER_INVESTIGATION, disposition NULL (paired green escalate is leg 4a)",
      observed: `red HTTP ${r2dRed.status}; state=${row2d?.state}; disp=${row2d?.disposition}`,
      verdict: pass2d
        ? "PASS — functionally-verified (escalate cannot smuggle a disposition; owner read-back confirms NULL)"
        : "FAIL",
      pass: pass2d,
    });

    // ── LEG 3 — M3 FENCE (post-M3 lockstep refresh) ───────────────────────────
    // ⚠ POST-M3 EXCEPTION to the "ORIGINAL battery is NEVER edited" invariant: the
    // original "no disposing route exists" assertion became a FALSE-RED once the M3
    // two-eyes STR disposal route (POST .../str) shipped (FU-070/071, prod-live).
    // Refreshed IN LOCKSTEP with the harness clone + the equivalence manifest so the
    // two batteries stay equivalent and the gate is honest-green (architect-ruled,
    // AS/PLATFORM/2026/003). The fence is NOT weakened: the four non-STR disposal
    // verbs must still be ABSENT (404); the shipped STR route is reachable but must
    // REJECT an M2-context disposal probe at its DTO (400, AEGIS_ERR_100 /
    // fincrime_str_prepare); and owner read-back must prove the probe caused NO
    // disposition effect (case not DISPOSITIONED, disposition cols NULL, str_filed
    // false). The real M2 assign route still reaches its handler (409).
    const absentDisposingPaths = ["dispose", "close", "disposition", "file-str"];
    const disposeStatuses: Record<string, number> = {};
    for (const p of absentDisposingPaths) {
      const r = await post(rm, `/api/fincrime/cases/${c1}/${p}`, { foo: "bar" });
      disposeStatuses[p] = r.status;
    }
    // (a) DTO fence — a malformed STR body is rejected at the schema (400, AEGIS_ERR_100).
    const strProbe = await post(rm, `/api/fincrime/cases/${c1}/str`, { foo: "bar" });
    disposeStatuses["str"] = strProbe.status;
    // (b) STATE fence — a WELL-FORMED STR body (passes prepareStrSchema) on c1, which is
    //     UNDER_INVESTIGATION (NOT PENDING_MLCO_REVIEW), must be rejected by the disposal
    //     STATE precondition (storage.prepareStr → StrGuardError("STATE_CONFLICT") → 409
    //     AEGIS_ERR_104/state_conflict). This is the load-bearing fence: it proves the
    //     shipped two-eyes STR disposal route cannot be entered from an M2-context case,
    //     not merely that a garbage body is DTO-rejected. Paired controls (below): the
    //     well-formed probe created ZERO fincrime_str_reports rows and its narrative is in
    //     no at-rest surface — the write provably never happened.
    const strWellFormed = await post(rm, `/api/fincrime/cases/${c1}/str`, { narrative: RAW_SUMMARY });
    const realRoute = await post(rm, `/api/fincrime/cases/${c1}/assign`, { assigneeUserId: rm.userId }); // c1 already UNDER_INVESTIGATION → 409 (handler reached)
    const row3 = await caseRow(TENANT_A, c1);
    const strRows3 = await strReportCount(TENANT_A, c1);
    const lk3 = await leak(TENANT_A, c1, RAW_SUMMARY);
    const nonStrAbsent404 = absentDisposingPaths.every((p) => disposeStatuses[p] === 404);
    const strReachableRejected400 =
      strProbe.status === 400 &&
      strProbe.json?.error?.code === "AEGIS_ERR_100" &&
      strProbe.json?.error?.details?.validation === "fincrime_str_prepare";
    const strStateFenced409 =
      strWellFormed.status === 409 &&
      strWellFormed.json?.error?.code === "AEGIS_ERR_104" &&
      strWellFormed.json?.error?.details?.reason === "state_conflict";
    const noStrRow = strRows3 === 0;
    const rejectedNarrativeNotPersisted = lk3.any === false;
    const noDispositionMutation =
      row3?.state !== "DISPOSITIONED" &&
      row3?.disposition === null &&
      row3?.disposition_reason === null &&
      row3?.dispositioned_at === null &&
      row3?.str_filed === false;
    const pass3 =
      nonStrAbsent404 &&
      strReachableRejected400 &&
      strStateFenced409 &&
      noStrRow &&
      rejectedNarrativeNotPersisted &&
      noDispositionMutation &&
      realRoute.status !== 404;
    rec({
      id: "3",
      scenario: "M3-FENCE (post-M3) — non-STR disposal verbs absent; shipped STR route DTO-fences a malformed body AND STATE-fences a well-formed M2-context disposal; zero STR rows; no disposition effect",
      expectation: "dispose/close/disposition/file-str → 404 (absent); str{malformed} → 400 (AEGIS_ERR_100/fincrime_str_prepare); str{well-formed narrative} → 409 (AEGIS_ERR_104/state_conflict; c1 UNDER_INVESTIGATION≠PENDING_MLCO_REVIEW); zero fincrime_str_reports rows; rejected narrative in no at-rest surface; case not DISPOSITIONED, disposition cols NULL, str_filed false; assign reachable (409)",
      observed: `dispose=${JSON.stringify(disposeStatuses)}; strWellFormed HTTP ${strWellFormed.status}(${strWellFormed.json?.error?.code}/${strWellFormed.json?.error?.details?.reason}); strRows=${strRows3}; narrativePersisted=${lk3.any}; realAssign HTTP ${realRoute.status}; state=${row3?.state}; disp=${row3?.disposition}; strFiled=${row3?.str_filed}`,
      verdict: pass3
        ? "PASS — functionally-verified (non-STR verbs 404; STR DTO-fenced 400 AND STATE-fenced 409/state_conflict; owner read-back: zero STR rows, narrative not persisted, no disposition; real route 409)"
        : "FAIL",
      pass: pass3,
    });

    // ── LEG 4a — POSITIVE: escalate UNDER_INVESTIGATION → PENDING_MLCO_REVIEW ──
    const c4 = await seedOpenCase(TENANT_A, "escalate");
    await post(rm, `/api/fincrime/cases/${c4}/assign`, { assigneeUserId: rm.userId }); // → UNDER_INVESTIGATION
    const r4a = await post(rm, `/api/fincrime/cases/${c4}/escalate-mlco`, { summary: RAW_SUMMARY });
    await settle();
    const row4a = await caseRow(TENANT_A, c4);
    const ev4a = await eventsFor(TENANT_A, c4);
    const evid4a = await evidenceFor(TENANT_A, c4);
    const at4a = await attemptCount("FINCRIME_ESCALATE_MLCO_ATTEMPT", c4);
    const escEvents = ev4a.filter((e) => e.event_type === "ESCALATED_TO_MLCO_REVIEW");
    const noteEvid = evid4a.filter((e) => e.evidence_type === "INVESTIGATION_NOTE");
    const lk4a = await leak(TENANT_A, c4, RAW_SUMMARY);
    const pass4a =
      r4a.status === 200 &&
      r4a.json?.state === "PENDING_MLCO_REVIEW" &&
      typeof r4a.json?.evidenceId === "string" &&
      row4a?.state === "PENDING_MLCO_REVIEW" &&
      row4a?.disposition === null &&
      escEvents.length === 1 &&
      escEvents[0].from_state === "UNDER_INVESTIGATION" &&
      escEvents[0].to_state === "PENDING_MLCO_REVIEW" &&
      noteEvid.length === 1 &&
      noteEvid[0].encrypted_note !== null &&
      !noteEvid[0].encrypted_note!.includes(RAW_SUMMARY) &&
      at4a >= 1 &&
      lk4a.any === false;
    rec({
      id: "4a",
      scenario: "POSITIVE escalate-mlco (UNDER_INVESTIGATION→PENDING_MLCO_REVIEW) with required encrypted summary",
      expectation:
        "HTTP 200 PENDING_MLCO_REVIEW + evidenceId; disposition NULL (terminal, NOT disposed); one ESCALATED_TO_MLCO_REVIEW event; one encrypted INVESTIGATION_NOTE summary (raw ABSENT); ≥1 ESCALATE_ATTEMPT audit; raw summary in NO at-rest surface",
      observed: `HTTP ${r4a.status}; state=${row4a?.state}; disp=${row4a?.disposition}; ESCALATED=${escEvents.length}(${escEvents[0]?.from_state}->${escEvents[0]?.to_state}); noteEvidence=${noteEvid.length}(enc=${noteEvid[0]?.encrypted_note !== null}); attemptAudits=${at4a}; leak{ev=${lk4a.evHit},evt=${lk4a.evtHit},aud=${lk4a.audHit}}`,
      verdict: pass4a
        ? "PASS — functionally-verified (HTTP 200 + owner read-back: terminal transition, NULL disposition, encrypted summary, leak probe negative)"
        : "FAIL",
      pass: pass4a,
    });

    // ── LEG 4b — M3 FENCE: PENDING_MLCO_REVIEW is terminal for M2 ──────────────
    const t1 = await post(rm, `/api/fincrime/cases/${c4}/assign`, { assigneeUserId: rm.userId });
    const t2 = await post(rm, `/api/fincrime/cases/${c4}/start`, { note: "x" });
    const t3 = await post(rm, `/api/fincrime/cases/${c4}/escalate-mlco`, { summary: "again" });
    const t4 = await post(rm, `/api/fincrime/cases/${c4}/evidence`, { evidenceType: "KYT_REF", opaqueRef: `opaque:k-${base}` });
    await settle();
    const row4b = await caseRow(TENANT_A, c4);
    const evid4b = await evidenceFor(TENANT_A, c4);
    const pass4b =
      t1.status === 409 &&
      t2.status === 409 &&
      t3.status === 409 &&
      t4.status === 409 && // evidence: state not OPEN/UNDER_INVESTIGATION → 409 state_not_eligible
      row4b?.state === "PENDING_MLCO_REVIEW" &&
      row4b?.disposition === null &&
      evid4b.length === noteEvid.length; // no new evidence landed after escalate
    rec({
      id: "4b",
      scenario: "M3-FENCE terminal — PENDING_MLCO_REVIEW case cannot be advanced/dispositioned by ANY M2 action",
      expectation: "assign/start/escalate all 409 (CAS wrong from-state); evidence 409 (state not eligible); case stays PENDING_MLCO_REVIEW, disposition NULL, no new evidence",
      observed: `assign=${t1.status} start=${t2.status} escalate=${t3.status} evidence=${t4.status}; state=${row4b?.state}; disp=${row4b?.disposition}; evidenceUnchanged=${evid4b.length === noteEvid.length}`,
      verdict: pass4b
        ? "PASS — functionally-verified (every onward M2 action 409 + owner read-back: state/disposition unchanged — M2 cannot reach disposition)"
        : "FAIL",
      pass: pass4b,
    });

    // ── LEG 5a — POSITIVE: add opaque-only evidence to an OPEN case ────────────
    const c5 = await seedOpenCase(TENANT_A, "evidence-pos");
    const r5a = await post(rm, `/api/fincrime/cases/${c5}/evidence`, { evidenceType: "TRANSACTION_REF", opaqueRef: `opaque:txn-${base}` });
    await settle();
    const row5a = await caseRow(TENANT_A, c5);
    const evid5a = await evidenceFor(TENANT_A, c5);
    const evAdded5a = (await eventsFor(TENANT_A, c5)).filter((e) => e.event_type === "EVIDENCE_ADDED");
    const at5a = await attemptCount("FINCRIME_EVIDENCE_ADD_ATTEMPT", c5);
    const pass5a =
      r5a.status === 201 &&
      typeof r5a.json?.evidenceId === "string" &&
      row5a?.state === "OPEN" && // evidence add does NOT transition
      evid5a.length === 1 &&
      evid5a[0].evidence_type === "TRANSACTION_REF" &&
      evid5a[0].opaque_ref === `opaque:txn-${base}` &&
      evid5a[0].encrypted_note === null &&
      typeof evid5a[0].audit_chain_entry_id === "number" &&
      evAdded5a.length === 1 &&
      at5a >= 1;
    rec({
      id: "5a",
      scenario: "POSITIVE add opaque-only evidence (no state change)",
      expectation: "HTTP 201 + evidenceId; case stays OPEN; one TRANSACTION_REF evidence row (opaque_ref set, encrypted_note NULL, audit FK); one EVIDENCE_ADDED event; ≥1 ADD_ATTEMPT audit",
      observed: `HTTP ${r5a.status}; state=${row5a?.state}; evidence=${evid5a.length}(${evid5a[0]?.evidence_type} opaque=${evid5a[0]?.opaque_ref === `opaque:txn-${base}`}); EVIDENCE_ADDED=${evAdded5a.length}; attemptAudits=${at5a}`,
      verdict: pass5a
        ? "PASS — functionally-verified (HTTP 201 + owner read-back of evidence row + event + audit; case state unchanged)"
        : "FAIL",
      pass: pass5a,
    });

    // ── LEG 5b — DENIAL: malformed evidence (paired with 5a) ──────────────────
    const r5bNoContent = await post(rm, `/api/fincrime/cases/${c5}/evidence`, { evidenceType: "TRANSACTION_REF" }); // refine: needs ≥1 content
    const r5bBadType = await post(rm, `/api/fincrime/cases/${c5}/evidence`, { evidenceType: "NOT_A_TYPE", opaqueRef: `opaque:x-${base}` });
    const r5bPiiRef = await post(rm, `/api/fincrime/cases/${c5}/evidence`, { evidenceType: "DOCUMENT_REF", opaqueRef: "john.doe@example.com" });
    await settle();
    const evid5b = await evidenceFor(TENANT_A, c5);
    const pass5b =
      r5bNoContent.status === 400 &&
      r5bBadType.status === 400 &&
      r5bPiiRef.status === 400 &&
      evid5b.length === 1; // unchanged from 5a — no malformed row landed
    rec({
      id: "5b",
      scenario: "DENIAL malformed evidence — no content / bad type / raw-PII opaqueRef",
      expectation: "all three HTTP 400; evidence row count unchanged at 1 (paired positive: 5a opaque evidence accepted)",
      observed: `noContent=${r5bNoContent.status} badType=${r5bBadType.status} piiRef=${r5bPiiRef.status}; evidenceRows=${evid5b.length}`,
      verdict: pass5b
        ? "PASS — functionally-verified (all malformed 400 + owner read-back: no malformed evidence landed)"
        : "FAIL",
      pass: pass5b,
    });

    // ── LEG 6a — tenant-from-session: A-actor cannot assign a B case ──────────
    const cB = await seedOpenCase(TENANT_B, "xtenant");
    const cAok = await seedOpenCase(TENANT_A, "xtenant-control");
    const r6aDeny = await post(rm, `/api/fincrime/cases/${cB}/assign`, { assigneeUserId: rm.userId });
    const r6aOk = await post(rm, `/api/fincrime/cases/${cAok}/assign`, { assigneeUserId: rm.userId });
    await settle();
    const rowB = await caseRow(TENANT_B, cB);
    const evB = await eventsFor(TENANT_B, cB);
    const rowAok = await caseRow(TENANT_A, cAok);
    const seenA = await rlsVisible(TENANT_A, cB);
    const seenB = await rlsVisible(TENANT_B, cB);
    const pass6a =
      r6aDeny.status === 409 && // CAS finds no such case in actor's (A) tenant
      rowB?.state === "OPEN" &&
      evB.length === 0 &&
      r6aOk.status === 200 &&
      rowAok?.state === "UNDER_INVESTIGATION" &&
      seenA === 0 &&
      seenB === 1;
    rec({
      id: "6a",
      scenario: "DENIAL tenant-from-session — A-authed actor assigns a TENANT_B case id",
      expectation: "HTTP 409 (no matching case in the actor's session tenant); B case untouched (OPEN, 0 events); paired A-case assign 200; RLS: B case invisible under withTenantRls(A)=0, visible under (B)=1",
      observed: `denyB HTTP ${r6aDeny.status} (Bstate=${rowB?.state}, Bevents=${evB.length}); controlA HTTP ${r6aOk.status} (state=${rowAok?.state}); rlsVisible A=${seenA} B=${seenB}`,
      verdict: pass6a
        ? "PASS — functionally-verified (tenant derives from session not path; owner+RLS read-back prove the B case is untouched and RLS-isolated)"
        : "FAIL",
      pass: pass6a,
    });

    // ── LEG 6b — cross-tenant evidence add → 404 (paired with 5a positive) ────
    const r6b = await post(rm, `/api/fincrime/cases/${cB}/evidence`, { evidenceType: "KYT_REF", opaqueRef: `opaque:xt-${base}` });
    await settle();
    const evidB = await evidenceFor(TENANT_B, cB);
    const pass6b = r6b.status === 404 && evidB.length === 0;
    rec({
      id: "6b",
      scenario: "DENIAL cross-tenant evidence — A-authed actor adds evidence to a TENANT_B case",
      expectation: "HTTP 404 (case not found in the actor's tenant); ZERO evidence rows on the B case (paired positive: 5a)",
      observed: `HTTP ${r6b.status}; B evidence rows=${evidB.length}`,
      verdict: pass6b
        ? "PASS — functionally-verified (404 + owner read-back proves no evidence landed cross-tenant)"
        : "FAIL",
      pass: pass6b,
    });

    // ── LEG 6c — payload cannot assert tenant (strict DTO) ────────────────────
    const c6c = await seedOpenCase(TENANT_A, "tenant-assert");
    const r6cCamel = await post(rm, `/api/fincrime/cases/${c6c}/assign`, { assigneeUserId: rm.userId, tenantId: TENANT_B });
    const r6cSnake = await post(rm, `/api/fincrime/cases/${c6c}/assign`, { assigneeUserId: rm.userId, tenant_id: TENANT_B });
    await settle();
    const row6c = await caseRow(TENANT_A, c6c);
    const pass6c = r6cCamel.status === 400 && r6cSnake.status === 400 && row6c?.state === "OPEN";
    rec({
      id: "6c",
      scenario: "DENIAL payload-asserted tenant — body carries tenantId/tenant_id = B",
      expectation: "HTTP 400 on both (strict DTO rejects the unknown tenant key); case unchanged (OPEN) — tenant is never client-supplied",
      observed: `tenantId=${r6cCamel.status} tenant_id=${r6cSnake.status}; state=${row6c?.state}`,
      verdict: pass6c
        ? "PASS — functionally-verified (payload cannot assert tenant; owner read-back confirms no mutation)"
        : "FAIL",
      pass: pass6c,
    });

    // ── LEG 7 — assignee no-oracle validation (identical 400 for all bad cases) ─
    const c7valid = await seedOpenCase(TENANT_A, "assignee-valid");
    const r7valid = await post(rm, `/api/fincrime/cases/${c7valid}/assign`, { assigneeUserId: rm2Seed.id });
    const c7a = await seedOpenCase(TENANT_A, "assignee-missing");
    const r7missing = await post(rm, `/api/fincrime/cases/${c7a}/assign`, { assigneeUserId: randomUUID() });
    const c7b = await seedOpenCase(TENANT_A, "assignee-wrongrole");
    const r7wrongRole = await post(rm, `/api/fincrime/cases/${c7b}/assign`, { assigneeUserId: audSeed.id });
    const c7c = await seedOpenCase(TENANT_A, "assignee-xtenant");
    const r7xtenant = await post(rm, `/api/fincrime/cases/${c7c}/assign`, { assigneeUserId: bUserSeed.id });
    await settle();
    const row7valid = await caseRow(TENANT_A, c7valid);
    const badStatuses = [r7missing.status, r7wrongRole.status, r7xtenant.status];
    const badBodies = [r7missing.json?.error?.code, r7wrongRole.json?.error?.code, r7xtenant.json?.error?.code];
    const identicalShape = new Set(badBodies).size === 1;
    const pass7 =
      r7valid.status === 200 &&
      row7valid?.state === "UNDER_INVESTIGATION" &&
      row7valid?.assigned_investigator_id === rm2Seed.id &&
      badStatuses.every((s) => s === 400) &&
      identicalShape;
    rec({
      id: "7",
      scenario: "ASSIGNEE no-oracle — valid assignee accepted; non-existent / wrong-role / cross-tenant ALL identical 400",
      expectation: "valid eligible assignee HTTP 200 (case assigned to them); the three ineligible cases ALL return HTTP 400 with an IDENTICAL error shape (no existence/role oracle)",
      observed: `valid HTTP ${r7valid.status} (assignee=${row7valid?.assigned_investigator_id === rm2Seed.id}); bad=[${badStatuses.join(",")}]; codes=[${badBodies.join(",")}] identical=${identicalShape}`,
      verdict: pass7
        ? "PASS — functionally-verified (HTTP 200 valid + owner read-back; three distinct ineligibility reasons collapse to one indistinguishable 400)"
        : "FAIL",
      pass: pass7,
    });

    // ── LEG 8 — CAS concurrency: two concurrent assigns ⇒ exactly one 409 ──────
    const c8 = await seedOpenCase(TENANT_A, "concurrency");
    const [c8r1, c8r2] = await Promise.all([
      post(rm, `/api/fincrime/cases/${c8}/assign`, { assigneeUserId: rm.userId }),
      post(rm, `/api/fincrime/cases/${c8}/assign`, { assigneeUserId: rm2Seed.id }),
    ]);
    await settle();
    const row8 = await caseRow(TENANT_A, c8);
    const ev8 = (await eventsFor(TENANT_A, c8)).filter((e) => e.event_type === "CASE_ASSIGNED");
    const at8 = await attemptCount("FINCRIME_CASE_ASSIGN_ATTEMPT", c8);
    const statuses = [c8r1.status, c8r2.status].sort();
    const oneWonOne409 = statuses[0] === 200 && statuses[1] === 409;
    const pass8 =
      oneWonOne409 &&
      row8?.state === "UNDER_INVESTIGATION" &&
      ev8.length === 1; // EFFECT exactly-once via CAS — exactly one transition event
    rec({
      id: "8",
      scenario: "CAS concurrency — two concurrent assigns on one OPEN case",
      expectation: "exactly one HTTP 200 and one HTTP 409; durable case UNDER_INVESTIGATION with EXACTLY ONE CASE_ASSIGNED event (effect-once via the compare-and-set)",
      observed: `statuses=[${statuses.join(",")}]; state=${row8?.state}; CASE_ASSIGNED events=${ev8.length}; attemptAudits=${at8}`,
      verdict: pass8
        ? `PASS — functionally-verified (HTTP {one 200, one 409} + owner read-back: exactly one transition event). HONEST CAVEAT (Rule 19): attempt-before-effect means ≥1 ASSIGN_ATTEMPT audit rows (observed ${at8}); the EFFECT, not the attempt, is exactly-once.`
        : "FAIL",
      pass: pass8,
    });

    // ── LEG 9a — read route: auditor CAN read; projection omits disposition ────
    const r9a = await get(aud, `/api/fincrime/cases?queue=all-active`);
    const ourIds = [...createdCaseIds];
    const returnedOurs = (r9a.json?.cases ?? []).filter((c: any) => ourIds.includes(c.id));
    const noDispositionLeak = (r9a.json?.cases ?? []).every(
      (c: any) => !("disposition" in c) && !("dispositionReason" in c) && !("strFiled" in c),
    );
    const pass9a =
      r9a.status === 200 &&
      Array.isArray(r9a.json?.cases) &&
      returnedOurs.length > 0 &&
      noDispositionLeak;
    rec({
      id: "9a",
      scenario: "READ route — auditor reads the case queue; projection omits disposition columns",
      expectation: "HTTP 200; auditor (read-only role) IS authorized to list; payload carries NO disposition/dispositionReason/strFiled keys (paired denial: 1b auditor cannot mutate)",
      observed: `HTTP ${r9a.status}; cases=${(r9a.json?.cases ?? []).length}; ofOurs=${returnedOurs.length}; dispositionOmitted=${noDispositionLeak}`,
      verdict: pass9a
        ? "PASS — functionally-verified (HTTP 200 list for auditor; explicit projection check confirms disposition columns are not exposed)"
        : "FAIL",
      pass: pass9a,
    });

    // ── LEG 9b — read route: bad queue → 400; valid queue → 200 ───────────────
    const r9bBad = await get(rm, `/api/fincrime/cases?queue=bogus-queue`);
    const r9bGood = await get(rm, `/api/fincrime/cases?queue=open`);
    const pass9b = r9bBad.status === 400 && r9bGood.status === 200;
    rec({
      id: "9b",
      scenario: "READ route — unknown queue rejected; valid queue accepted",
      expectation: "queue=bogus-queue HTTP 400 (closed enum); queue=open HTTP 200 (paired control)",
      observed: `bad HTTP ${r9bBad.status}; good HTTP ${r9bGood.status}`,
      verdict: pass9b
        ? "PASS — functionally-verified (closed-enum queue selector rejects unknown, accepts valid)"
        : "FAIL",
      pass: pass9b,
    });

    // ── LEG 9c — read route: queue filters segregate states correctly ─────────
    const ourSet = new Set(createdCaseIds);
    const openQ = await get(rm, `/api/fincrime/cases?queue=open`);
    const pendingQ = await get(rm, `/api/fincrime/cases?queue=pending-mlco-review`);
    const mineQ = await get(rm, `/api/fincrime/cases?queue=mine`);
    const openOurs = (openQ.json?.cases ?? []).filter((c: any) => ourSet.has(c.id));
    const pendingOurs = (pendingQ.json?.cases ?? []).filter((c: any) => ourSet.has(c.id));
    const mineOurs = (mineQ.json?.cases ?? []).filter((c: any) => ourSet.has(c.id));
    const openAllOpen = openOurs.every((c: any) => c.state === "OPEN");
    const pendingHasC4 = pendingOurs.some((c: any) => c.id === c4) && pendingOurs.every((c: any) => c.state === "PENDING_MLCO_REVIEW");
    const mineAllRm = mineOurs.every((c: any) => c.assignedInvestigatorId === rm.userId) && mineOurs.some((c: any) => c.id === c1);
    const pass9c = openQ.status === 200 && pendingQ.status === 200 && mineQ.status === 200 && openAllOpen && pendingHasC4 && mineAllRm;
    rec({
      id: "9c",
      scenario: "READ route — open / pending-mlco-review / mine queues segregate by state and assignee",
      expectation: "open returns only OPEN; pending-mlco-review returns only PENDING_MLCO_REVIEW (incl. escalated c4); mine returns only cases assigned to the actor (incl. c1)",
      observed: `open ours=${openOurs.length} allOpen=${openAllOpen}; pending ours=${pendingOurs.length} hasC4&allPending=${pendingHasC4}; mine ours=${mineOurs.length} allRm&hasC1=${mineAllRm}`,
      verdict: pass9c
        ? "PASS — functionally-verified (each queue's durable rows match its state/assignee predicate)"
        : "FAIL",
      pass: pass9c,
    });

    // ── LEG 11a — DETERMINISTIC TOCTOU proof (denial): evidence-add BLOCKS on
    // the case row lock; while it waits the case is escalated to PENDING; the
    // unblocked add re-reads under its own FOR UPDATE, sees the terminal state,
    // and returns 409 — evidence can NEVER land on an escalated case.
    // We simulate the competing escalate with an owner-held row lock + UPDATE so
    // the ordering is FORCED (not timing-dependent): the discriminator is whether
    // the evidence request BLOCKS on the row lock. Pre-fix (plain SELECT) it would
    // NOT block → return 201 during the hold → fail this leg. Post-fix (FOR UPDATE)
    // it blocks, then 409s. This is the bug-distinguishing observable.
    const c11a = await seedOpenCase(TENANT_A, "race-deny");
    await post(rm, `/api/fincrime/cases/${c11a}/assign`, { assigneeUserId: rm.userId }); // → UNDER_INVESTIGATION
    const lockClientA = await owner.connect();
    let blockedA = false;
    let resA: { status: number; json: any } = { status: 0, json: {} };
    try {
      await lockClientA.query("BEGIN");
      await lockClientA.query("SELECT id FROM fincrime_cases WHERE tenant_id=$1 AND id=$2 FOR UPDATE", [TENANT_A, c11a]);
      let returnedA = false;
      const pA = post(rm, `/api/fincrime/cases/${c11a}/evidence`, { evidenceType: "DOCUMENT_REF", opaqueRef: `opaque:doc-raceA-${base}` });
      pA.then(() => { returnedA = true; });
      await settle(1200);
      blockedA = !returnedA; // evidence still blocked behind the held row lock
      // competing escalate "wins" the row first: move to terminal, then release.
      await lockClientA.query("UPDATE fincrime_cases SET state='PENDING_MLCO_REVIEW', updated_at=NOW() WHERE tenant_id=$1 AND id=$2", [TENANT_A, c11a]);
      await lockClientA.query("COMMIT");
      resA = await pA;
    } finally {
      lockClientA.release();
    }
    await settle();
    const row11a = await caseRow(TENANT_A, c11a);
    const docRefA = (await evidenceFor(TENANT_A, c11a)).filter((e) => e.evidence_type === "DOCUMENT_REF");
    const pass11a = blockedA && resA.status === 409 && docRefA.length === 0 && row11a?.state === "PENDING_MLCO_REVIEW";
    rec({
      id: "11a",
      scenario: "TOCTOU race (denial) — evidence-add concurrent with escalation to PENDING_MLCO_REVIEW",
      expectation: "evidence-add BLOCKS on the case row lock during the hold; after the case is escalated and the lock releases, the add re-reads the terminal state and returns 409; ZERO DOCUMENT_REF evidence rows; case PENDING_MLCO_REVIEW",
      observed: `blockedOnRowLock=${blockedA}; evidence HTTP ${resA.status}; docRefRows=${docRefA.length}; state=${row11a?.state}`,
      verdict: pass11a
        ? "PASS — functionally-verified (FOR UPDATE forced serialization: evidence blocked behind the row lock, then 409 on the now-terminal case; owner read-back confirms no evidence landed). Bug-distinguishing: a plain SELECT would not block and would 201 mid-hold."
        : "FAIL",
      pass: pass11a,
    });

    // ── LEG 11b — DETERMINISTIC TOCTOU proof (positive, paired with 11a): SAME
    // blocking mechanism, but the case is NOT escalated during the wait — the
    // unblocked add re-reads UNDER_INVESTIGATION and SUCCEEDS (201). The only
    // difference from 11a is whether the case was escalated while the add waited,
    // proving the 409 in 11a is the state gate firing — not a generally-broken add.
    const c11b = await seedOpenCase(TENANT_A, "race-allow");
    await post(rm, `/api/fincrime/cases/${c11b}/assign`, { assigneeUserId: rm.userId }); // → UNDER_INVESTIGATION
    const lockClientB = await owner.connect();
    let blockedB = false;
    let resB: { status: number; json: any } = { status: 0, json: {} };
    try {
      await lockClientB.query("BEGIN");
      await lockClientB.query("SELECT id FROM fincrime_cases WHERE tenant_id=$1 AND id=$2 FOR UPDATE", [TENANT_A, c11b]);
      let returnedB = false;
      const pB = post(rm, `/api/fincrime/cases/${c11b}/evidence`, { evidenceType: "DOCUMENT_REF", opaqueRef: `opaque:doc-raceB-${base}` });
      pB.then(() => { returnedB = true; });
      await settle(1200);
      blockedB = !returnedB;
      // release WITHOUT changing state — case stays UNDER_INVESTIGATION.
      await lockClientB.query("COMMIT");
      resB = await pB;
    } finally {
      lockClientB.release();
    }
    await settle();
    const row11b = await caseRow(TENANT_A, c11b);
    const docRefB = (await evidenceFor(TENANT_A, c11b)).filter((e) => e.evidence_type === "DOCUMENT_REF");
    const pass11b = blockedB && resB.status === 201 && docRefB.length === 1 && row11b?.state === "UNDER_INVESTIGATION";
    rec({
      id: "11b",
      scenario: "TOCTOU race (positive, paired) — evidence-add blocks then SUCCEEDS when the case is NOT escalated during the wait",
      expectation: "evidence-add BLOCKS on the row lock; after release with the case still UNDER_INVESTIGATION, the add returns 201 and lands exactly one DOCUMENT_REF evidence row; case stays UNDER_INVESTIGATION",
      observed: `blockedOnRowLock=${blockedB}; evidence HTTP ${resB.status}; docRefRows=${docRefB.length}; state=${row11b?.state}`,
      verdict: pass11b
        ? "PASS — functionally-verified (same lock-wait as 11a; outcome flips to 201 solely because no escalation occurred — proves 11a's 409 is the state gate, not a broken endpoint)"
        : "FAIL",
      pass: pass11b,
    });

    // ── LEG 12 — REAL-ROUTE concurrency: evidence-add vs escalate-mlco fired
    // together (no harness lock), N iterations. Asserts EVERY outcome is one of
    // the two LEGAL shapes — (A) evidence 201 + 1 DOCUMENT_REF row, or (B)
    // evidence 409 + 0 rows — with escalate always 200 and the case always
    // PENDING_MLCO_REVIEW. An illegal shape (e.g. 201 with the row on a PENDING
    // case, or escalate 409, or wrong final state) fails. Complements the
    // deterministic 11a/11b with the actual two competing route handlers.
    const N = 8;
    const raceOutcomes: string[] = [];
    let leg12ok = true;
    for (let i = 0; i < N; i++) {
      const cR = await seedOpenCase(TENANT_A, `race12-${i}`);
      await post(rm, `/api/fincrime/cases/${cR}/assign`, { assigneeUserId: rm.userId }); // → UNDER_INVESTIGATION
      const [er, sr] = await Promise.all([
        post(rm, `/api/fincrime/cases/${cR}/evidence`, { evidenceType: "DOCUMENT_REF", opaqueRef: `opaque:doc12-${base}-${i}` }),
        post(rm, `/api/fincrime/cases/${cR}/escalate-mlco`, { summary: `race summary ${i}` }),
      ]);
      await settle(120);
      const rowR = await caseRow(TENANT_A, cR);
      const docRef = (await evidenceFor(TENANT_A, cR)).filter((e) => e.evidence_type === "DOCUMENT_REF");
      const legalA = er.status === 201 && docRef.length === 1; // evidence serialized BEFORE escalate
      const legalB = er.status === 409 && docRef.length === 0; // escalate won; add saw terminal
      const ok = sr.status === 200 && rowR?.state === "PENDING_MLCO_REVIEW" && (legalA || legalB);
      if (!ok) leg12ok = false;
      raceOutcomes.push(`ev${er.status}/esc${sr.status}/${rowR?.state}/doc${docRef.length}`);
    }
    const countA = raceOutcomes.filter((o) => o.startsWith("ev201")).length;
    const countB = raceOutcomes.filter((o) => o.startsWith("ev409")).length;
    rec({
      id: "12",
      scenario: `REAL-ROUTE race — evidence-add vs escalate-mlco, ${N} concurrent iterations`,
      expectation: "every iteration is a LEGAL shape: escalate 200, case PENDING_MLCO_REVIEW, and (evidence 201 ⇔ exactly 1 DOCUMENT_REF row) XOR (evidence 409 ⇔ 0 rows); NO evidence row on a case where it landed after PENDING",
      observed: `outcomes=[${raceOutcomes.join(", ")}]; serialized-before(201)=${countA}, lost-to-escalate(409)=${countB}`,
      verdict: leg12ok
        ? "PASS — functionally-verified (all iterations legal; owner read-back confirms evidence-row presence matches the HTTP result and the case is always terminal). Rule-19 note: scheduling decides which legal ordering occurs each run; both are correct and the deterministic gate proof is 11a/11b."
        : "FAIL",
      pass: leg12ok,
    });

    // ── LEG 13 — no-silent-close: NO synthetic case is or ever became disposed ─
    const dispCount = await dispositionedAmong([...createdCaseIds]);
    const withDispCount = await withDispositionAmong([...createdCaseIds]);
    const pass10 = dispCount === 0 && withDispCount === 0;
    rec({
      id: "10",
      scenario: "M3-FENCE global — no synthetic case reached DISPOSITIONED or carries any disposition field",
      expectation: "across ALL cases this battery touched: ZERO in state DISPOSITIONED; ZERO carrying disposition / disposition_reason / dispositioned_at / str_filed",
      observed: `dispositionedCases=${dispCount}; casesWithAnyDispositionField=${withDispCount}; ofTotal=${createdCaseIds.size}`,
      verdict: pass10
        ? "PASS — functionally-verified (owner read-back over every touched case: M2 left the disposition surface entirely NULL — no silent close, no M3 reach)"
        : "FAIL",
      pass: pass10,
    });
  } finally {
    // cleanup: evidence + events first (composite FK → cases), then cases;
    // then UTRs/sessions; deactivate users (hard-delete blocked by audit FK refs).
    // audit_chain_entries are immutable (hash-chained) and intentionally preserved.
    try {
      const ids = [...createdCaseIds];
      if (ids.length) {
        const evd = await owner.query("DELETE FROM fincrime_case_evidence WHERE case_id = ANY($1::varchar[])", [ids]);
        const evt = await owner.query("DELETE FROM fincrime_case_events WHERE case_id = ANY($1::varchar[])", [ids]);
        const cse = await owner.query("DELETE FROM fincrime_cases WHERE id = ANY($1::varchar[])", [ids]);
        console.log(`\ncleanup: deleted ${evd.rowCount} evidence, ${evt.rowCount} event, ${cse.rowCount} case row(s).`);
      }
    } catch (e: any) {
      console.log(`cleanup cases/events/evidence (non-fatal): ${e?.message ?? e}`);
    }
    for (const s of allSeeds) {
      try {
        await owner.query("DELETE FROM user_tenant_roles WHERE user_id = $1", [s.id]);
        await owner.query("DELETE FROM sessions WHERE user_id = $1", [s.id]);
        await owner.query(
          "UPDATE users SET is_active = false, password = gen_random_uuid()::text WHERE id = $1",
          [s.id],
        );
      } catch (e: any) {
        console.log(`cleanup user ${s.username} (non-fatal): ${e?.message ?? e}`);
      }
    }
    console.log("cleanup: synthetic seed users deactivated + credentials neutralized (audit chain preserved).");
    await owner.end().catch(() => {});
  }

  console.log("");
  for (const r of results) {
    console.log("-".repeat(82));
    console.log(`[${r.id}] ${r.scenario}`);
    console.log(`     expect  : ${r.expectation}`);
    console.log(`     observe : ${r.observed}`);
    console.log(`     VERDICT : ${r.verdict}`);
  }
  console.log("-".repeat(82));
  const passed = results.filter((r) => r.pass).length;
  const allPass = results.length > 0 && results.every((r) => r.pass);
  console.log(`ACCEPTANCE: ${passed}/${results.length} checks PASS — ${allPass ? "ALL PASS" : "SEE FAILURES ABOVE"}`);
  process.exit(allPass ? 0 : 1);
}

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