// P5.2 / FU-047 — Tenant-isolation automated regression probe suite.
//
// Anchoring follow-up: FU-038 (2026-05-24). Cross-phase substrate fix landed
// because architect review caught at Phase 3 wiring that the entire UBO storage
// substrate had no `tenantId` parameter and Phase 2 trigger functions inherited
// the gap. Substantively-real-but-practically-dormant until Phase 3 would have
// activated it. The systemic lesson logged: "substrate work that doesn't
// include the multi-tenant thread is substantively incomplete substrate."
//
// This suite is the substrate-level mitigation. For every IStorage method that
// accepts a tenantId parameter, the suite seeds two-tenant fixtures and asserts:
//   1. Read isolation — tenant-A query returns ZERO tenant-B rows.
//   2. Back-compat — no-tenantId call returns rows from BOTH tenants (the
//      pre-FU-038 caller contract).
//   3. CAS-write isolation — UPDATE scoped to tenant-A against a tenant-B
//      uboRef returns no-row (undefined) and leaves the B row untouched.
//   4. Audit-payload tenantId — UBO erasure triggers must include the
//      tenantId in the audit_logs.details JSON (12 writeAudit sites per FU-038).
//
// Run: `npx tsx --test tests/tenant-isolation-probes.test.ts`
// Requires: DATABASE_URL pointed at a dev/test Postgres (the same one the dev
//           server uses — Postgres rows are isolated by the unique tenant-id
//           prefix per-run, so concurrent dev work is unaffected).
//
// Idempotent: suite both opens and closes with a prefix-scoped DELETE so a
// crashed run leaves no residue and a re-run on top of stale residue is clean.

import { test, describe, before, after } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
import { sql } from "drizzle-orm";

import { storage } from "../server/storage.js";
import { db } from "../server/db.js";
import {
  apiKeys,
  ghostCoreEvents,
  ssoConfigs,
  i18nOverrides,
  auditLogs,
} from "../shared/schema.js";
import { uboRegistrations } from "../shared/schema-pilot-extras-7.js";
import {
  processCorporateErasure,
  flagUboForIndividualErasure,
} from "../server/lib/ubo-erasure.js";
// FORWARD seed: UBO national_id is seeded exactly as the real write path stores
// it (T004a) — AES-GCM ciphertext in owner_national_id + a tenant-keyed HMAC
// blind index in owner_national_id_hmac. Never plaintext.
import { encryptPii, computeLookupHash } from "../server/lib/pii-encryption.js";

// Unique per-run prefix avoids collisions with concurrent dev work or stale
// rows from a crashed earlier run.
const RUN_TS = Date.now();
const PREFIX = `tenant-iso-probe-${RUN_TS}`;
const TENANT_A = `${PREFIX}-A`;
const TENANT_B = `${PREFIX}-B`;

// ──────────────────────────────────────────────────────────────────────────────
// Cleanup — prefix-scoped DELETE across all probed tables. Run at start (to
// clear residue from a prior crashed run with a different RUN_TS but the same
// "tenant-iso-probe-" base prefix) and at end (normal cleanup).
// ──────────────────────────────────────────────────────────────────────────────
// Concurrency-safe end-of-suite cleanup: exact PREFIX match only. Two
// concurrent runs hold different RUN_TS values so their prefixes never
// collide; each cleans up only its own fixtures.
async function cleanupThisRun(): Promise<void> {
  const likePat = `${PREFIX}%`;
  await db.execute(sql`DELETE FROM ghost_core_events WHERE tenant_id LIKE ${likePat}`);
  await db.execute(sql`DELETE FROM api_keys WHERE tenant_id LIKE ${likePat}`);
  await db.execute(sql`DELETE FROM sso_configs WHERE tenant_id LIKE ${likePat}`);
  await db.execute(sql`DELETE FROM i18n_overrides WHERE tenant_id LIKE ${likePat}`);
  await db.execute(sql`DELETE FROM ubo_registrations WHERE tenant_id LIKE ${likePat}`);
  await db.execute(sql`DELETE FROM audit_logs WHERE resource LIKE ${"ubo:" + PREFIX + "%"} OR (details IS NOT NULL AND details LIKE ${"%" + PREFIX + "%"})`);
}

// Broad preflight purge: clears residue from crashed prior runs (different
// RUN_TS values) before this run seeds. Documented as preflight maintenance
// per architect MEDIUM-3 (2026-05-28). Concurrent runs accept this is a
// best-effort preflight; if another run is mid-execution its fixtures may
// be cleared, but that is no worse than the original broad-purge behaviour
// and is bounded to other tenant-iso-probe-* prefixes (no production data).
async function cleanupCrashResidue(): Promise<void> {
  await db.execute(sql`DELETE FROM ghost_core_events WHERE tenant_id LIKE 'tenant-iso-probe-%'`);
  await db.execute(sql`DELETE FROM api_keys WHERE tenant_id LIKE 'tenant-iso-probe-%'`);
  await db.execute(sql`DELETE FROM sso_configs WHERE tenant_id LIKE 'tenant-iso-probe-%'`);
  await db.execute(sql`DELETE FROM i18n_overrides WHERE tenant_id LIKE 'tenant-iso-probe-%'`);
  await db.execute(sql`DELETE FROM ubo_registrations WHERE tenant_id LIKE 'tenant-iso-probe-%'`);
  await db.execute(sql`DELETE FROM audit_logs WHERE resource LIKE 'ubo:tenant-iso-probe-%' OR (details IS NOT NULL AND details LIKE '%tenant-iso-probe-%')`);
}

// ──────────────────────────────────────────────────────────────────────────────
// Seed — two-tenant fixtures across all five probed tables.
// ──────────────────────────────────────────────────────────────────────────────
interface SeededRefs {
  uboRefA: string;
  uboRefB: string;
  corporateRefA: string;
  corporateRefB: string;
  natIdA: string;
  natIdB: string;
  apiKeyNameA: string;
  apiKeyNameB: string;
}

async function seedFixtures(): Promise<SeededRefs> {
  const refs: SeededRefs = {
    uboRefA: `${PREFIX}-ubo-A`,
    uboRefB: `${PREFIX}-ubo-B`,
    corporateRefA: `${PREFIX}-corp-A`,
    corporateRefB: `${PREFIX}-corp-B`,
    natIdA: `${PREFIX}-natid-A`,
    natIdB: `${PREFIX}-natid-B`,
    apiKeyNameA: `${PREFIX}-key-A`,
    apiKeyNameB: `${PREFIX}-key-B`,
  };

  // ghost_core_events — 1 row per tenant
  await db.insert(ghostCoreEvents).values([
    { eventType: "CBS_FAILOVER", status: "completed", tenantId: TENANT_A, details: `seeded-by-${PREFIX}` },
    { eventType: "CBS_FAILOVER", status: "completed", tenantId: TENANT_B, details: `seeded-by-${PREFIX}` },
  ]);

  // api_keys — 1 row per tenant
  await db.insert(apiKeys).values([
    { name: refs.apiKeyNameA, keyHash: `hash-${PREFIX}-A`, keyPrefix: "tip_A_", tenantId: TENANT_A },
    { name: refs.apiKeyNameB, keyHash: `hash-${PREFIX}-B`, keyPrefix: "tip_B_", tenantId: TENANT_B },
  ]);

  // sso_configs — 1 row per tenant
  await db.insert(ssoConfigs).values([
    { tenantId: TENANT_A, provider: "saml", entityId: `${PREFIX}-entity-A` },
    { tenantId: TENANT_B, provider: "saml", entityId: `${PREFIX}-entity-B` },
  ]);

  // i18n_overrides — 1 row per tenant, SAME locale to test locale+tenantId AND-clause
  await db.insert(i18nOverrides).values([
    { tenantId: TENANT_A, locale: "en", key: `${PREFIX}-key`, value: "A-value" },
    { tenantId: TENANT_B, locale: "en", key: `${PREFIX}-key`, value: "B-value" },
  ]);

  // ubo_registrations — 1 ACTIVE row per tenant
  await db.insert(uboRegistrations).values([
    {
      uboRef: refs.uboRefA,
      corporateCustomerRef: refs.corporateRefA,
      corporateName: `Corp-A-${PREFIX}`,
      ownerName: `Owner-A-${PREFIX}`,
      ownerNationalId: await encryptPii(refs.natIdA, { type: "tenant", tenantId: TENANT_A }),
      ownerNationalIdHmac: computeLookupHash(refs.natIdA, TENANT_A, "ubo_registrations", "owner_national_id"),
      ownerNationality: "UG",
      ownershipPct: 60,
      controlType: "direct",
      registeredBy: `seed-${PREFIX}`,
      tenantId: TENANT_A,
    },
    {
      uboRef: refs.uboRefB,
      corporateCustomerRef: refs.corporateRefB,
      corporateName: `Corp-B-${PREFIX}`,
      ownerName: `Owner-B-${PREFIX}`,
      ownerNationalId: await encryptPii(refs.natIdB, { type: "tenant", tenantId: TENANT_B }),
      ownerNationalIdHmac: computeLookupHash(refs.natIdB, TENANT_B, "ubo_registrations", "owner_national_id"),
      ownerNationality: "UG",
      ownershipPct: 75,
      controlType: "direct",
      registeredBy: `seed-${PREFIX}`,
      tenantId: TENANT_B,
    },
  ]);

  return refs;
}

let seeded: SeededRefs;

before(async () => {
  // Preflight broad purge: handles crash residue from prior runs with
  // different RUN_TS prefixes. Documented per architect MEDIUM-3.
  await cleanupCrashResidue();
  seeded = await seedFixtures();
});

after(async () => {
  // Concurrency-safe end cleanup: only this run's exact PREFIX.
  await cleanupThisRun();
});

// ──────────────────────────────────────────────────────────────────────────────
// READ-ISOLATION PROBES (7 storage methods × 3 invariants each)
// ──────────────────────────────────────────────────────────────────────────────

describe("READ — ghost_core_events tenant isolation", () => {
  test("getGhostCoreEvents(tenant=A) returns only A's rows", async () => {
    const rows = await storage.getGhostCoreEvents(1000, TENANT_A);
    const probeRows = rows.filter((r) => r.details === `seeded-by-${PREFIX}`);
    assert.equal(probeRows.length, 1, `expected exactly 1 probe row for tenant A, got ${probeRows.length}`);
    assert.equal(probeRows[0].tenantId, TENANT_A);
    const leak = rows.find((r) => r.tenantId === TENANT_B);
    assert.equal(leak, undefined, "tenant-B row leaked into tenant-A query");
  });

  test("getGhostCoreEvents() (no tenant) returns rows from BOTH tenants (back-compat)", async () => {
    const rows = await storage.getGhostCoreEvents(1000);
    const probeRows = rows.filter((r) => r.details === `seeded-by-${PREFIX}`);
    const tenants = new Set(probeRows.map((r) => r.tenantId));
    assert.ok(tenants.has(TENANT_A), "tenant-A probe row missing from unscoped query");
    assert.ok(tenants.has(TENANT_B), "tenant-B probe row missing from unscoped query");
  });
});

describe("READ — api_keys tenant isolation", () => {
  test("getApiKeys(tenant=A) returns only A's rows", async () => {
    const rows = await storage.getApiKeys(TENANT_A);
    const probeRows = rows.filter((r) => r.name.startsWith(PREFIX));
    assert.equal(probeRows.length, 1);
    assert.equal(probeRows[0].tenantId, TENANT_A);
    assert.equal(rows.find((r) => r.tenantId === TENANT_B), undefined);
  });

  test("getApiKeys() (no tenant) returns rows from BOTH tenants (back-compat)", async () => {
    const rows = await storage.getApiKeys();
    const probeRows = rows.filter((r) => r.name.startsWith(PREFIX));
    const tenants = new Set(probeRows.map((r) => r.tenantId));
    assert.ok(tenants.has(TENANT_A));
    assert.ok(tenants.has(TENANT_B));
  });
});

describe("READ — sso_configs tenant isolation", () => {
  test("getSsoConfigs(tenant=A) returns only A's rows", async () => {
    const rows = await storage.getSsoConfigs(TENANT_A);
    const probeRows = rows.filter((r) => r.entityId?.startsWith(PREFIX));
    assert.equal(probeRows.length, 1);
    assert.equal(probeRows[0].tenantId, TENANT_A);
    assert.equal(rows.find((r) => r.tenantId === TENANT_B), undefined);
  });

  test("getSsoConfigs() (no tenant) returns rows from BOTH tenants (back-compat)", async () => {
    const rows = await storage.getSsoConfigs();
    const probeRows = rows.filter((r) => r.entityId?.startsWith(PREFIX));
    const tenants = new Set(probeRows.map((r) => r.tenantId));
    assert.ok(tenants.has(TENANT_A));
    assert.ok(tenants.has(TENANT_B));
  });
});

describe("READ — i18n_overrides tenant isolation", () => {
  test("getI18nOverrides(locale=en, tenant=A) returns only A's row", async () => {
    const rows = await storage.getI18nOverrides("en", TENANT_A);
    const probeRows = rows.filter((r) => r.key === `${PREFIX}-key`);
    assert.equal(probeRows.length, 1);
    assert.equal(probeRows[0].tenantId, TENANT_A);
    assert.equal(probeRows[0].value, "A-value");
    assert.equal(rows.find((r) => r.tenantId === TENANT_B), undefined);
  });

  test("getI18nOverrides(locale=en) (no tenant) returns rows from BOTH tenants (back-compat)", async () => {
    const rows = await storage.getI18nOverrides("en");
    const probeRows = rows.filter((r) => r.key === `${PREFIX}-key`);
    const tenants = new Set(probeRows.map((r) => r.tenantId));
    assert.ok(tenants.has(TENANT_A));
    assert.ok(tenants.has(TENANT_B));
  });
});

describe("READ — ubo_registrations tenant isolation (FU-038 critical surface)", () => {
  test("getUboRegistrationsByCorporate(B-corp-ref, tenant=A) returns ZERO rows (cross-tenant lookup blocked)", async () => {
    const rows = await storage.getUboRegistrationsByCorporate(seeded.corporateRefB, TENANT_A);
    assert.equal(rows.length, 0, "tenant-A scoped query returned tenant-B UBO row — FU-038 substrate violation");
  });

  test("getUboRegistrationsByCorporate(A-corp-ref, tenant=A) returns A's row", async () => {
    const rows = await storage.getUboRegistrationsByCorporate(seeded.corporateRefA, TENANT_A);
    assert.equal(rows.length, 1);
    assert.equal(rows[0].tenantId, TENANT_A);
    assert.equal(rows[0].uboRef, seeded.uboRefA);
  });

  test("getUboRegistrationsByCorporate(B-corp-ref) (no tenant) returns B's row (back-compat unscoped)", async () => {
    const rows = await storage.getUboRegistrationsByCorporate(seeded.corporateRefB);
    assert.equal(rows.length, 1);
    assert.equal(rows[0].tenantId, TENANT_B);
  });

  test("getUboRegistrationsByNationalId(B-natId, tenant=A) returns ZERO rows", async () => {
    const rows = await storage.getUboRegistrationsByNationalId(seeded.natIdB, TENANT_A);
    assert.equal(rows.length, 0);
  });

  test("getUboRegistrationsByNationalId(A-natId, tenant=A) returns A's row", async () => {
    const rows = await storage.getUboRegistrationsByNationalId(seeded.natIdA, TENANT_A);
    assert.equal(rows.length, 1);
    assert.equal(rows[0].tenantId, TENANT_A);
  });

  test("getUboRegistrationsByNationalId(B-natId) (no tenant) returns EMPTY — national_id lookup requires the tenant key (T004a)", async () => {
    // T004a: owner_national_id is encrypted and looked up via a TENANT-KEYED HMAC
    // blind index. Without a tenantId the HMAC cannot be derived, so an unscoped
    // national_id lookup structurally returns nothing — STRONGER isolation than the
    // pre-T004a unscoped back-compat behaviour, not a regression.
    const rows = await storage.getUboRegistrationsByNationalId(seeded.natIdB);
    assert.equal(rows.length, 0);
  });

  test("getUboRegistrationByRef(B-uboRef, tenant=A) returns undefined", async () => {
    const row = await storage.getUboRegistrationByRef(seeded.uboRefB, TENANT_A);
    assert.equal(row, undefined, "tenant-A scoped getByRef returned tenant-B row — FU-038 substrate violation");
  });

  test("getUboRegistrationByRef(A-uboRef, tenant=A) returns A's row", async () => {
    const row = await storage.getUboRegistrationByRef(seeded.uboRefA, TENANT_A);
    assert.ok(row, "tenant-A scoped getByRef returned undefined for A's own row");
    assert.equal(row!.tenantId, TENANT_A);
  });

  test("getUboRegistrationByRef(B-uboRef) (no tenant) returns B's row (back-compat)", async () => {
    const row = await storage.getUboRegistrationByRef(seeded.uboRefB);
    assert.ok(row);
    assert.equal(row!.tenantId, TENANT_B);
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// CAS-WRITE-ISOLATION PROBES (2 storage methods) — the FU-038 substrate-fix surface
// ──────────────────────────────────────────────────────────────────────────────

describe("CAS-WRITE — ubo_registrations cross-tenant write blocked", () => {
  test("updateUboErasureState(B-uboRef, ..., tenant=A) returns undefined and leaves B's row untouched", async () => {
    const result = await storage.updateUboErasureState(
      seeded.uboRefB,
      { individualErasurePending: "cross-tenant-write-attempt" },
      { tenantId: TENANT_A },
    );
    assert.equal(result, undefined, "cross-tenant UPDATE returned a row — FU-038 substrate violation");

    // Verify B's row is genuinely untouched.
    const rowB = await storage.getUboRegistrationByRef(seeded.uboRefB);
    assert.ok(rowB);
    assert.equal(rowB!.individualErasurePending, null, "tenant-B row was mutated by tenant-A scoped UPDATE");
  });

  test("updateUboErasureState(A-uboRef, ..., tenant=A) succeeds (positive control)", async () => {
    const result = await storage.updateUboErasureState(
      seeded.uboRefA,
      { individualErasurePending: `${PREFIX}-positive-control` },
      { tenantId: TENANT_A },
    );
    assert.ok(result, "tenant-A scoped UPDATE on A's own row returned undefined — false-negative would mask isolation regression");
    assert.equal(result!.tenantId, TENANT_A);
    // Reset
    await storage.updateUboErasureState(seeded.uboRefA, { individualErasurePending: null }, { tenantId: TENANT_A });
  });

  test("softEraseUboRow(B-uboRef, ..., tenant=A) returns undefined and leaves B's PII intact", async () => {
    const result = await storage.softEraseUboRow(
      seeded.uboRefB,
      { ownerName: "REDACTED", ownerNationalId: "REDACTED", ownerNationality: "REDACTED" },
      "regulator_order",
      `audit-${PREFIX}-cross-tenant-attempt`,
      { tenantId: TENANT_A },
    );
    assert.equal(result, undefined, "cross-tenant softEraseUboRow returned a row — FU-038 substrate violation");

    // Verify B's PII still in place.
    const rowB = await storage.getUboRegistrationByRef(seeded.uboRefB);
    assert.ok(rowB);
    assert.equal(rowB!.ownerName, `Owner-B-${PREFIX}`, "tenant-B PII was redacted by tenant-A scoped softErase");
    assert.equal(rowB!.ownerNationalId, seeded.natIdB);
    assert.equal(rowB!.erasureState, "ACTIVE", "tenant-B erasure_state moved by tenant-A scoped softErase");
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// CAS-WRITE-ISOLATION with expectedCurrentState combined branch (architect
// MEDIUM-2, 2026-05-28). Production trigger paths in ubo-erasure.ts always
// pass both `expectedCurrentState` and `tenantId` in opts; a branch-specific
// regression that broke only the tenantId predicate in the combined-opts
// path would slip past the simpler probes above. Ordered BEFORE the audit-
// payload probes because those mutate row state irreversibly (A → ERASED,
// B → individualErasurePending set).
// ──────────────────────────────────────────────────────────────────────────────

describe("CAS-WRITE — combined { expectedCurrentState, tenantId } cross-tenant denial", () => {
  test("updateUboErasureState(B-uboRef, ..., {expectedCurrentState:ACTIVE, tenantId:A}) returns undefined", async () => {
    const result = await storage.updateUboErasureState(
      seeded.uboRefB,
      { individualErasurePending: "combined-opts-cross-tenant-attempt" },
      { expectedCurrentState: "ACTIVE", tenantId: TENANT_A },
    );
    assert.equal(result, undefined, "combined-opts cross-tenant UPDATE returned a row — substrate violation in expectedCurrentState+tenantId branch");
    const rowB = await storage.getUboRegistrationByRef(seeded.uboRefB);
    assert.ok(rowB);
    assert.equal(rowB!.individualErasurePending, null, "tenant-B row mutated by combined-opts cross-tenant UPDATE");
  });

  test("updateUboErasureState(A-uboRef, ..., {expectedCurrentState:ACTIVE, tenantId:A}) succeeds (positive control for combined branch)", async () => {
    const result = await storage.updateUboErasureState(
      seeded.uboRefA,
      { individualErasurePending: `${PREFIX}-combined-positive` },
      { expectedCurrentState: "ACTIVE", tenantId: TENANT_A },
    );
    assert.ok(result, "combined-opts same-tenant UPDATE returned undefined — false-negative would mask isolation regression");
    assert.equal(result!.tenantId, TENANT_A);
    await storage.updateUboErasureState(seeded.uboRefA, { individualErasurePending: null }, { tenantId: TENANT_A });
  });

  test("softEraseUboRow(B-uboRef, ..., {expectedCurrentState:ACTIVE, tenantId:A}) returns undefined and leaves B intact", async () => {
    const result = await storage.softEraseUboRow(
      seeded.uboRefB,
      { ownerName: "REDACTED", ownerNationalId: "REDACTED", ownerNationality: "REDACTED" },
      "regulator_order",
      `audit-${PREFIX}-combined-cross-tenant`,
      { expectedCurrentState: "ACTIVE", tenantId: TENANT_A },
    );
    assert.equal(result, undefined, "combined-opts cross-tenant softErase returned a row — substrate violation");
    const rowB = await storage.getUboRegistrationByRef(seeded.uboRefB);
    assert.ok(rowB);
    assert.equal(rowB!.ownerName, `Owner-B-${PREFIX}`);
    assert.equal(rowB!.erasureState, "ACTIVE");
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// AUDIT-PAYLOAD tenantId PROBES (FU-038 12-site writeAudit contract)
// ──────────────────────────────────────────────────────────────────────────────

describe("AUDIT-PAYLOAD — tenantId surfaces in audit_logs.details JSON (FU-038)", () => {
  test("processCorporateErasure emits audit rows whose details JSON contains the tenantId", async () => {
    const result = await processCorporateErasure({
      corporateCustomerRef: seeded.corporateRefA,
      dsarRequestId: `${PREFIX}-dsar-corp`,
      actorUserId: null,
      tenantId: TENANT_A,
    });
    assert.equal(result.eligibleCount, 1, `expected exactly 1 eligible UBO row, got ${result.eligibleCount}`);
    assert.equal(result.erasedCount, 1, `expected exactly 1 erased UBO row, got ${result.erasedCount}`);

    // Pull recent audit rows for this resource and verify tenantId is present
    // in the hash-chained details JSON.
    const rows = await db.execute<{ action: string; details: string | null }>(
      sql`SELECT action, details FROM audit_logs WHERE resource = ${"ubo:" + seeded.uboRefA} ORDER BY timestamp DESC LIMIT 5`,
    );
    const auditRows = (rows as any).rows ?? rows;
    assert.ok(Array.isArray(auditRows) && auditRows.length > 0, "no audit_logs row written for UBO erasure");
    const erasureRow = auditRows.find((r: any) => r.action === "UBO_RECORD_ERASED");
    assert.ok(erasureRow, "UBO_RECORD_ERASED audit row not found");
    const details = JSON.parse(erasureRow.details);
    assert.equal(details.tenantId, TENANT_A, `UBO_RECORD_ERASED audit details missing or wrong tenantId: ${erasureRow.details}`);
  });

  test("flagUboForIndividualErasure emits audit row whose details JSON contains the tenantId", async () => {
    // Use tenant-B's still-active row for this probe (corporate path above
    // already erased A's row).
    const result = await flagUboForIndividualErasure({
      ownerNationalId: seeded.natIdB,
      dsarRequestId: `${PREFIX}-dsar-indiv`,
      actorUserId: null,
      tenantId: TENANT_B,
    });
    assert.ok(result.flaggedCount >= 1, `expected at least 1 flagged UBO row, got ${result.flaggedCount}`);

    const rows = await db.execute<{ action: string; details: string | null }>(
      sql`SELECT action, details FROM audit_logs WHERE resource = ${"ubo:" + seeded.uboRefB} ORDER BY timestamp DESC LIMIT 5`,
    );
    const auditRows = (rows as any).rows ?? rows;
    const flagRow = auditRows.find((r: any) => r.action === "UBO_INDIVIDUAL_ERASURE_FLAGGED");
    assert.ok(flagRow, "UBO_INDIVIDUAL_ERASURE_FLAGGED audit row not found");
    const details = JSON.parse(flagRow.details);
    assert.equal(details.tenantId, TENANT_B, `UBO_INDIVIDUAL_ERASURE_FLAGGED audit details missing or wrong tenantId: ${flagRow.details}`);
  });
});

// ──────────────────────────────────────────────────────────────────────────────
// STRUCTURAL writeAudit-coverage probe (architect HIGH, 2026-05-28). The two
// trigger-exercising probes above verify the live audit-write path for 2 of
// the 12 FU-038 writeAudit sites (UBO_RECORD_ERASED, UBO_INDIVIDUAL_ERASURE_FLAGGED).
// The other 10 sites are race-lost / posthumous / regulator-edge-case paths
// that require concurrent races or pre-existing erased rows to trigger cleanly
// — engineering 10 trigger scenarios is disproportionate to the regression
// target (someone dropping `tenantId` from a writeAudit details payload).
//
// Structural source-scan: parse server/lib/ubo-erasure.ts, locate every
// `writeAudit({` block, and assert each block contains a `tenantId` reference
// in its details object literal. Catches the regression target directly.
// Combined with the 2 live-path probes, this covers the full FU-038 contract.
// ──────────────────────────────────────────────────────────────────────────────

describe("STRUCTURAL — all 12 writeAudit sites in ubo-erasure.ts include tenantId in details (FU-038 full contract)", () => {
  test("12 writeAudit({ blocks exist and each one's details object references tenantId", () => {
    const src = readFileSync(
      path.resolve(process.cwd(), "server/lib/ubo-erasure.ts"),
      "utf-8",
    );

    // Find every `writeAudit({` opening and brace-match to its closing `})`.
    const openings: number[] = [];
    const re = /writeAudit\(\{/g;
    let m: RegExpExecArray | null;
    while ((m = re.exec(src)) !== null) {
      openings.push(m.index);
    }
    assert.equal(
      openings.length,
      12,
      `expected exactly 12 writeAudit({ sites per FU-038, found ${openings.length} — coverage contract drift`,
    );

    // For each opening, brace-match to find the corresponding closing `}`
    // of the details argument, then assert the slice contains `tenantId`.
    // Brace counter starts at 1 (we've seen the opening { of the call args).
    for (const start of openings) {
      const argsStart = start + "writeAudit(".length; // position of `{`
      let depth = 0;
      let end = -1;
      for (let i = argsStart; i < src.length; i++) {
        const ch = src[i];
        if (ch === "{") depth++;
        else if (ch === "}") {
          depth--;
          if (depth === 0) {
            end = i;
            break;
          }
        }
      }
      assert.ok(end > argsStart, `brace-match failed for writeAudit at offset ${start}`);
      const block = src.slice(argsStart, end + 1);
      const actionMatch = /action:\s*["']([A-Z_]+)["']/.exec(block);
      const action = actionMatch ? actionMatch[1] : `<unknown@${start}>`;

      // Architect LOW (2026-05-28): tighten to the details sub-object only.
      // Whole-block /tenantId/ would false-green if tenantId appears in
      // action/resource/userId but is absent from details. Locate the
      // `details:` key, brace-match its `{...}` value, and assert tenantId
      // appears inside that sub-object specifically.
      const detailsKeyRe = /\bdetails\s*:\s*\{/g;
      detailsKeyRe.lastIndex = 0;
      const detailsKeyMatch = detailsKeyRe.exec(block);
      assert.ok(detailsKeyMatch, `writeAudit site for action ${action} has no details: { ... } sub-object`);
      const detailsObjStart = detailsKeyMatch.index + detailsKeyMatch[0].length - 1; // position of `{`
      let dDepth = 0;
      let detailsObjEnd = -1;
      for (let i = detailsObjStart; i < block.length; i++) {
        const ch = block[i];
        if (ch === "{") dDepth++;
        else if (ch === "}") {
          dDepth--;
          if (dDepth === 0) {
            detailsObjEnd = i;
            break;
          }
        }
      }
      assert.ok(detailsObjEnd > detailsObjStart, `brace-match failed for details sub-object of writeAudit action ${action}`);
      const detailsBlock = block.slice(detailsObjStart, detailsObjEnd + 1);
      assert.match(
        detailsBlock,
        /tenantId/,
        `writeAudit site for action ${action} omits tenantId from details sub-object — FU-038 audit-payload contract violation. Details block: ${detailsBlock.slice(0, 200)}...`,
      );
    }
  });
});

describe("CONTRACT — empty/whitespace tenantId behaviour", () => {
  test("getUboRegistrationsByCorporate(ref, '') falls back to unscoped (length===0 skip per storage contract)", async () => {
    // Per server/storage.ts FU-038 comment: when tenantId is undefined OR an
    // empty string, no tenant predicate is added. Callers who pass user-input
    // are responsible for normalisation (DSAR worker boundary does this).
    const rows = await storage.getUboRegistrationsByCorporate(seeded.corporateRefB, "");
    assert.equal(rows.length, 1, "empty-string tenantId should fall back to unscoped; expected B's row");
    assert.equal(rows[0].tenantId, TENANT_B);
  });

  test("getUboRegistrationByRef(uboRef, null) falls back to unscoped (null skip per storage contract)", async () => {
    const row = await storage.getUboRegistrationByRef(seeded.uboRefB, null);
    assert.ok(row);
    assert.equal(row!.tenantId, TENANT_B);
  });
});
