/**
 * T004a Tier D — ubo_registrations.owner_national_id HMAC verification suite
 *
 * Proves the (ii) treatment wiring is correct for ubo_registrations.owner_national_id.
 * Covers all properties mandated by the T004a design brief, plus four UBO-specific
 * threads surfaced in the reconciliation analysis:
 *
 *   (a) Round-trip: encrypted blob round-trips correctly; HMAC lookup finds the
 *       correct row when queried with the exact plaintext.
 *   (b) Canonical() consistency: format variations produce the same HMAC → same row.
 *   (c) Collision-avoidance: different owner NIDs produce different HMACs; cross-lookup
 *       returns zero rows (with Rule 18 positive-control interlock).
 *   (d) Per-tenant isolation + Rule 18 interlock: same plaintext under two different
 *       tenantIds produces different HMACs; each tenant finds only its own row.
 *       HOLLOW-GREEN detection: denial pass without positive pass = FAIL.
 *   (e) Treat-(i) columns intact: ownerName and ownerNationality still encrypt and
 *       decrypt correctly (regression check).
 *   (f) Structural: owner_national_id_hmac column exists (text), unique index
 *       ubo_owner_national_id_hmac_unique exists.
 *   (g) Wrong-tenant AES auth-tag rejection: tenant-B key cannot decrypt tenant-A blob.
 *   (h) Thread A coexistence: hashOwnerNationalId() produces "ubo_hash:v1:..." prefix —
 *       distinct from "ENC:v1:..." (encrypted blob) and from raw HMAC hex. The erasure
 *       sentinel and the T004a HMAC are non-overlapping in value space.
 *   (i) Thread D — DSAR decrypt fix: decryptPii(encrypted_blob, ctx) = plaintext;
 *       decryptPii(plaintext, ctx) = plaintext (passthrough). Verifies the dsar-purge-worker
 *       fix logic (decrypt before passing to flagUboForIndividualErasure).
 *   (j) Thread B — buildRedactedFields correctness: hashOwnerNationalId applied to the
 *       decrypted plaintext ≠ hashOwnerNationalId applied to the encrypted blob, proving
 *       that decryptUboRow must decrypt ownerNationalId before buildRedactedFields runs.
 *   (k) Thread C — null tenantId safe-no-op: HMAC lookup with empty/null tenant context
 *       cannot find the row (HMAC computed with "" tenant key differs from real tenant HMAC).
 *
 * Run: npx tsx scripts/t004a-tier-d-ubo-verify.ts
 */

import pg from "pg";
import crypto from "crypto";
import {
  encryptPii,
  decryptPii,
  computeLookupHash,
  canonical,
} from "../server/lib/pii-encryption";

const { Pool } = pg;

// ── Test configuration ────────────────────────────────────────────────────────

const TENANT_A = "ubo-tier-d-verify-tenant-a";
const TENANT_B = "ubo-tier-d-verify-tenant-b";

const NID_1_RAW   = "UG-NIN-UBO-111111";
const NID_1_DASH  = "UG-NIN-UBO-111111";  // same — dash form
const NID_1_SPACE = "UG NIN UBO 111111";  // same — space form
const NID_1_LOWER = "ug-nin-ubo-111111";  // same — lowercase form
const NID_2_RAW   = "UG-NIN-UBO-999999";  // different NID

// Verify canonical() normalises format variants to the same string
if (canonical(NID_1_DASH) !== canonical(NID_1_SPACE) || canonical(NID_1_DASH) !== canonical(NID_1_LOWER)) {
  console.error("SETUP FAIL: canonical() variants not equal — test config error");
  process.exit(1);
}
if (canonical(NID_1_RAW) === canonical(NID_2_RAW)) {
  console.error("SETUP FAIL: NID_1 and NID_2 canonical forms must differ — test config error");
  process.exit(1);
}

// Replicate hashOwnerNationalId() logic from ubo-erasure.ts to avoid importing DB-dependent module.
// Uses EXAMINER_TOKEN_SALT (same env var, same algorithm, same prefix).
const HASH_PREFIX = "ubo_hash:v1:";
function hashOwnerNationalId(rawNationalId: string): string {
  const salt = process.env.EXAMINER_TOKEN_SALT;
  if (!salt) throw new Error("EXAMINER_TOKEN_SALT not set");
  const hmac = crypto.createHmac("sha256", salt).update(rawNationalId).digest("hex");
  return `${HASH_PREFIX}${hmac}`;
}

// ── Result tracking ───────────────────────────────────────────────────────────

type Result = { label: string; pass: boolean; detail?: string };
const results: Result[] = [];

function pass(label: string, detail?: string) {
  results.push({ label, pass: true, detail });
  console.log(`  ✓ ${label}${detail ? ` — ${detail}` : ""}`);
}
function fail(label: string, detail: string) {
  results.push({ label, pass: false, detail });
  console.error(`  ✗ ${label} — ${detail}`);
}

// ── Database helpers ──────────────────────────────────────────────────────────

const newUboRef = () =>
  `UBO-TDVERIFY-${Date.now().toString(36).toUpperCase()}-${crypto.randomBytes(2).toString("hex").toUpperCase()}`;

async function insertTestRow(client: pg.PoolClient, opts: {
  uboRef: string;
  ownerNationalId: string;
  tenantId: string;
}): Promise<void> {
  const piiCtx = { type: "tenant" as const, tenantId: opts.tenantId };
  const encNationalId = encryptPii(opts.ownerNationalId, piiCtx);
  const hmac = computeLookupHash(opts.ownerNationalId, opts.tenantId, "ubo_registrations", "owner_national_id");
  const encOwnerName        = encryptPii("Test Owner Name", piiCtx);
  const encOwnerNationality = encryptPii("Ugandan",         piiCtx);

  await client.query(
    `INSERT INTO ubo_registrations
       (ubo_ref, corporate_customer_ref, corporate_name,
        owner_name, owner_national_id, owner_national_id_hmac, owner_nationality,
        ownership_pct, control_type, pep_status, sanctions_status,
        verified_at, registered_by, tenant_id)
     VALUES ($1, $2, $3, $4, $5, $6, $7, 30, 'shareholder', 0, 0, NOW(), 'test-script', $8)
     ON CONFLICT (ubo_ref) DO NOTHING`,
    [
      opts.uboRef,
      `CORP-TDVERIFY-${opts.tenantId}`,
      "Test Corporate Ltd",
      encOwnerName,
      encNationalId,
      hmac,
      encOwnerNationality,
      opts.tenantId,
    ],
  );
}

async function lookupByHmac(
  client: pg.PoolClient,
  ownerNationalId: string,
  tenantId: string,
): Promise<{
  ubo_ref: string;
  owner_national_id: string;
  owner_national_id_hmac: string | null;
  owner_name: string;
  owner_nationality: string;
  tenant_id: string | null;
} | null> {
  const hmac = computeLookupHash(ownerNationalId, tenantId, "ubo_registrations", "owner_national_id");
  const { rows } = await client.query(
    `SELECT ubo_ref, owner_national_id, owner_national_id_hmac,
            owner_name, owner_nationality, tenant_id
       FROM ubo_registrations
      WHERE owner_national_id_hmac = $1
      LIMIT 1`,
    [hmac],
  );
  return rows[0] ?? null;
}

async function cleanup(client: pg.PoolClient, uboRefs: string[]) {
  if (uboRefs.length === 0) return;
  await client.query(
    `DELETE FROM ubo_registrations WHERE ubo_ref = ANY($1::text[])`,
    [uboRefs],
  );
}

// ── Test runner ───────────────────────────────────────────────────────────────

async function runVerification() {
  const pool = new Pool({ connectionString: process.env.DATABASE_URL });
  const client = await pool.connect();

  const insertedRefs: string[] = [];
  const refA1 = newUboRef();  // tenant A, NID_1
  const refA2 = newUboRef();  // tenant A, NID_2 (collision-avoidance)
  const refB1 = newUboRef();  // tenant B, NID_1 (per-tenant isolation)

  try {
    console.log("\n── T004a Tier D: ubo_registrations.owner_national_id HMAC verification ──\n");

    // ── (f) Structural: column + unique index ────────────────────────────────
    console.log("(f) Structural checks");
    {
      const { rows: colRows } = await client.query<{ udt_name: string }>(
        `SELECT udt_name FROM information_schema.columns
          WHERE table_schema = 'public'
            AND table_name   = 'ubo_registrations'
            AND column_name  = 'owner_national_id_hmac'`,
      );
      if (colRows.length === 0) {
        fail("f1 — owner_national_id_hmac column exists (text)", "column NOT FOUND");
      } else {
        const t = colRows[0].udt_name;
        if (t === "text") {
          pass("f1 — owner_national_id_hmac column exists (text)", `udt_name=${t}`);
        } else {
          fail("f1 — owner_national_id_hmac column type=text", `actual udt_name=${t}`);
        }
      }

      const { rows: idxRows } = await client.query<{ indexname: string; indexdef: string }>(
        `SELECT indexname, indexdef
           FROM pg_indexes
          WHERE tablename = 'ubo_registrations'
            AND indexname = 'ubo_owner_national_id_hmac_unique'`,
      );
      if (idxRows.length === 0) {
        fail("f2 — ubo_owner_national_id_hmac_unique index exists", "index NOT FOUND in pg_indexes");
      } else {
        pass("f2 — ubo_owner_national_id_hmac_unique index exists", `indexdef: ${idxRows[0].indexdef}`);
      }
    }

    // ── Insert test rows ─────────────────────────────────────────────────────
    await insertTestRow(client, { uboRef: refA1, ownerNationalId: NID_1_RAW, tenantId: TENANT_A });
    await insertTestRow(client, { uboRef: refA2, ownerNationalId: NID_2_RAW, tenantId: TENANT_A });
    await insertTestRow(client, { uboRef: refB1, ownerNationalId: NID_1_RAW, tenantId: TENANT_B });
    insertedRefs.push(refA1, refA2, refB1);

    // ── (a) Round-trip: encrypted blob decrypts; HMAC lookup finds correct row ─
    console.log("\n(a) Round-trip + HMAC lookup");
    {
      const row = await lookupByHmac(client, NID_1_RAW, TENANT_A);
      if (!row) {
        fail("a1 — HMAC lookup finds row for NID_1 / tenant A", "returned null");
      } else {
        if (row.ubo_ref !== refA1) {
          fail("a1 — HMAC lookup finds correct row (refA1)", `expected ubo_ref=${refA1} got ${row.ubo_ref}`);
        } else {
          pass("a1 — HMAC lookup finds correct row (refA1)", `ubo_ref=${row.ubo_ref}`);
        }

        // Decrypt blob
        const piiCtx = { type: "tenant" as const, tenantId: TENANT_A };
        let decrypted = "";
        try {
          decrypted = decryptPii(row.owner_national_id, piiCtx);
        } catch (e) {
          fail("a2 — owner_national_id blob decrypts without error", String(e));
        }
        if (decrypted) {
          if (canonical(decrypted) === canonical(NID_1_RAW)) {
            pass("a2 — decrypted owner_national_id canonical-equals plaintext", `decrypted="${decrypted}"`);
          } else {
            fail("a2 — decrypted owner_national_id canonical-equals plaintext",
              `decrypted="${decrypted}" canonical="${canonical(decrypted)}" expected="${canonical(NID_1_RAW)}"`);
          }
        }

        // owner_national_id stored as blob (not plaintext)
        const isNidPlaintext = row.owner_national_id === NID_1_RAW || row.owner_national_id === NID_1_RAW.toUpperCase();
        if (isNidPlaintext) {
          fail("a3 — owner_national_id column is encrypted (not plaintext)",
            `stored value is plaintext "${row.owner_national_id}"`);
        } else {
          pass("a3 — owner_national_id column is encrypted (not plaintext)",
            `blob prefix: ${row.owner_national_id.substring(0, 20)}...`);
        }

        // HMAC is not plaintext
        const storedHmac = row.owner_national_id_hmac ?? "";
        if (storedHmac === NID_1_RAW || storedHmac === NID_1_RAW.toUpperCase()) {
          fail("a4 — owner_national_id_hmac is not plaintext NID", `stored="${storedHmac}"`);
        } else {
          pass("a4 — owner_national_id_hmac is not plaintext NID", `stored=${storedHmac.substring(0, 16)}...`);
        }
      }
    }

    // ── (b) Canonical() consistency ──────────────────────────────────────────
    console.log("\n(b) Canonical() format-variation consistency");
    {
      const rowDash  = await lookupByHmac(client, NID_1_DASH,  TENANT_A);
      const rowSpace = await lookupByHmac(client, NID_1_SPACE, TENANT_A);
      const rowLower = await lookupByHmac(client, NID_1_LOWER, TENANT_A);

      if (rowDash?.ubo_ref === refA1)  pass("b1 — dash form finds same row",      `ubo_ref=${rowDash.ubo_ref}`);
      else fail("b1 — dash form finds same row",  `got ${rowDash?.ubo_ref ?? "null"} expected ${refA1}`);

      if (rowSpace?.ubo_ref === refA1) pass("b2 — space form finds same row",     `ubo_ref=${rowSpace.ubo_ref}`);
      else fail("b2 — space form finds same row", `got ${rowSpace?.ubo_ref ?? "null"} expected ${refA1}`);

      if (rowLower?.ubo_ref === refA1) pass("b3 — lowercase form finds same row", `ubo_ref=${rowLower.ubo_ref}`);
      else fail("b3 — lowercase form finds same row", `got ${rowLower?.ubo_ref ?? "null"} expected ${refA1}`);
    }

    // ── (c) Collision-avoidance ───────────────────────────────────────────────
    console.log("\n(c) Collision-avoidance");
    {
      const rowFor2 = await lookupByHmac(client, NID_2_RAW, TENANT_A);
      if (!rowFor2) {
        fail("c1 (positive control) — NID_2 lookup finds its own row (refA2)", "returned null — HOLLOW-GREEN risk");
      } else if (rowFor2.ubo_ref !== refA2) {
        fail("c1 (positive control) — NID_2 lookup finds refA2", `got ${rowFor2.ubo_ref}`);
      } else {
        pass("c1 (positive control) — NID_2 lookup finds refA2", `ubo_ref=${rowFor2.ubo_ref}`);
      }

      if (rowFor2?.ubo_ref === refA1) {
        fail("c2 — NID_2 lookup does not return NID_1 row", `got refA1 (${refA1}) — HASH COLLISION`);
      } else {
        pass("c2 — NID_2 lookup does not return NID_1 row (no collision)");
      }

      const rowFor1 = await lookupByHmac(client, NID_1_RAW, TENANT_A);
      if (rowFor1?.ubo_ref === refA2) {
        fail("c3 — NID_1 lookup does not return NID_2 row", `got refA2 (${refA2}) — HASH COLLISION`);
      } else {
        pass("c3 — NID_1 lookup does not return NID_2 row (no collision)");
      }
    }

    // ── (d) Per-tenant isolation + Rule 18 interlock ─────────────────────────
    console.log("\n(d) Per-tenant isolation (Rule 18: positive + negative controls)");
    {
      const hashA = computeLookupHash(NID_1_RAW, TENANT_A, "ubo_registrations", "owner_national_id");
      const hashB = computeLookupHash(NID_1_RAW, TENANT_B, "ubo_registrations", "owner_national_id");

      if (hashA === hashB) {
        fail("d1 — per-tenant HMAC isolation (hashes differ across tenants)",
          `SAME HASH for both tenants: "${hashA}" — CONSTRUCTION IS NOT ISOLATING`);
      } else {
        pass("d1 — per-tenant HMAC differs across tenants",
          `hashA≠hashB (hashA[0..8]=${hashA.substring(0, 8)} hashB[0..8]=${hashB.substring(0, 8)})`);
      }

      const rowA = await lookupByHmac(client, NID_1_RAW, TENANT_A);
      const positivePassA = rowA?.ubo_ref === refA1;
      if (positivePassA) {
        pass("d2 (positive control) — tenant A HMAC lookup finds refA1", `ubo_ref=${rowA!.ubo_ref}`);
      } else {
        fail("d2 (positive control) — tenant A HMAC lookup finds refA1",
          `got ${rowA?.ubo_ref ?? "null"} — HOLLOW-GREEN risk if d4 passes`);
      }

      const rowBLookup = await lookupByHmac(client, NID_1_RAW, TENANT_B);
      const positivePassB = rowBLookup?.ubo_ref === refB1;
      if (positivePassB) {
        pass("d3 (positive control) — tenant B HMAC lookup finds refB1", `ubo_ref=${rowBLookup!.ubo_ref}`);
      } else {
        fail("d3 (positive control) — tenant B HMAC lookup finds refB1",
          `got ${rowBLookup?.ubo_ref ?? "null"} — HOLLOW-GREEN risk`);
      }

      const wrongTenantARow = rowBLookup?.ubo_ref === refA1;
      const wrongTenantBRow = rowA?.ubo_ref === refB1;

      if (wrongTenantARow) {
        fail("d4 — tenant B lookup does not return tenant A's row", `got refA1 (${refA1}) — ISOLATION FAILURE`);
      } else {
        pass("d4 — tenant B HMAC lookup does not return tenant A's row");
      }

      if (wrongTenantBRow) {
        fail("d5 — tenant A lookup does not return tenant B's row", `got refB1 (${refB1}) — ISOLATION FAILURE`);
      } else {
        pass("d5 — tenant A HMAC lookup does not return tenant B's row");
      }

      // Rule 18 interlock: denial only meaningful if positive passed
      if (!positivePassA && !wrongTenantARow) {
        fail("d-RULE18 — denial result is HOLLOW-GREEN",
          "d2 positive control failed AND d4 denial passed — wrong-tenant returned null but own-tenant also null; cannot distinguish isolation from broken write");
      } else if (positivePassA && positivePassB && !wrongTenantARow && !wrongTenantBRow) {
        pass("d-RULE18 — Rule 18 interlock satisfied",
          "both positive controls pass AND cross-tenant denials pass — isolation is genuine, not hollow-green");
      }
    }

    // ── (e) Treat-(i) columns intact ─────────────────────────────────────────
    console.log("\n(e) Treat-(i) columns regression check (ownerName, ownerNationality)");
    {
      const { rows } = await client.query<{
        owner_name: string; owner_nationality: string; tenant_id: string | null;
      }>(
        `SELECT owner_name, owner_nationality, tenant_id
           FROM ubo_registrations WHERE ubo_ref = $1`,
        [refA1],
      );
      if (rows.length === 0) {
        fail("e1 — row retrievable for (i) column check", `ubo_ref=${refA1} not found`);
      } else {
        const row = rows[0];
        const piiCtx = { type: "tenant" as const, tenantId: TENANT_A };

        try {
          const dec = decryptPii(row.owner_name, piiCtx);
          if (dec === "Test Owner Name") {
            pass("e1 — owner_name round-trips correctly", `dec="${dec}"`);
          } else {
            fail("e1 — owner_name round-trips correctly", `dec="${dec}" expected "Test Owner Name"`);
          }
        } catch (e) {
          fail("e1 — owner_name decrypts without error", String(e));
        }

        try {
          const dec = decryptPii(row.owner_nationality, piiCtx);
          if (dec === "Ugandan") {
            pass("e2 — owner_nationality round-trips correctly", `dec="${dec}"`);
          } else {
            fail("e2 — owner_nationality round-trips correctly", `dec="${dec}" expected "Ugandan"`);
          }
        } catch (e) {
          fail("e2 — owner_nationality decrypts without error", String(e));
        }
      }
    }

    // ── (g) Wrong-tenant AES auth-tag rejection ───────────────────────────────
    console.log("\n(g) Cross-tenant AES auth-tag rejection (wrong key cannot decrypt)");
    {
      const { rows } = await client.query<{ owner_national_id: string }>(
        `SELECT owner_national_id FROM ubo_registrations WHERE ubo_ref = $1`,
        [refA1],
      );
      if (rows.length === 0) {
        fail("g1 — raw row retrievable for cross-key rejection test", "row not found");
      } else {
        const blob = rows[0].owner_national_id;
        const wrongCtx = { type: "tenant" as const, tenantId: TENANT_B };
        let threw = false;
        try {
          decryptPii(blob, wrongCtx);
        } catch {
          threw = true;
        }
        if (threw) {
          pass("g1 — wrong-tenant key cannot decrypt tenant-A blob (AES-GCM auth-tag rejection)");
        } else {
          fail("g1 — wrong-tenant key cannot decrypt tenant-A blob",
            "decryption succeeded with wrong key — AES-GCM auth-tag not enforced");
        }
      }
    }

    // ── (h) Thread A: hashOwnerNationalId coexistence ────────────────────────
    // hashOwnerNationalId() uses EXAMINER_TOKEN_SALT + "ubo_hash:v1:" prefix.
    // The erasure sentinel and the T004a HMAC are non-overlapping in value space:
    //   - erasure sentinel: "ubo_hash:v1:<hex>" — never starts with "ENC:v1:"
    //   - encrypted blob: "ENC:v1:<base64>"    — never starts with "ubo_hash:v1:"
    //   - HMAC lookup key: raw hex              — no special prefix
    console.log("\n(h) Thread A — hashOwnerNationalId() coexistence (prefix disjointness)");
    {
      const encBlob     = encryptPii(NID_1_RAW, { type: "tenant", tenantId: TENANT_A });
      const erasureSent = hashOwnerNationalId(NID_1_RAW);
      const hmacKey     = computeLookupHash(NID_1_RAW, TENANT_A, "ubo_registrations", "owner_national_id");

      if (erasureSent.startsWith(HASH_PREFIX)) {
        pass("h1 — erasure sentinel starts with 'ubo_hash:v1:' prefix", `sentinel=${erasureSent.substring(0, 24)}...`);
      } else {
        fail("h1 — erasure sentinel starts with 'ubo_hash:v1:' prefix", `actual prefix: ${erasureSent.substring(0, 20)}`);
      }

      if (!erasureSent.startsWith("ENC:v1:")) {
        pass("h2 — erasure sentinel does not start with ENC:v1: (no confusion with encrypted blob)");
      } else {
        fail("h2 — erasure sentinel must NOT start with ENC:v1:", "prefix overlap with encrypted blob format");
      }

      if (encBlob.startsWith("ENC:v1:")) {
        pass("h3 — encrypted blob starts with 'ENC:v1:' prefix", `blob prefix: ${encBlob.substring(0, 20)}`);
      } else {
        fail("h3 — encrypted blob starts with 'ENC:v1:' prefix", `actual prefix: ${encBlob.substring(0, 20)}`);
      }

      const valuesDistinct = new Set([encBlob, erasureSent, hmacKey]).size === 3;
      if (valuesDistinct) {
        pass("h4 — encrypted blob, erasure sentinel, and HMAC key are all distinct values");
      } else {
        fail("h4 — encrypted blob, erasure sentinel, and HMAC key must all be distinct", "at least two are equal");
      }

      // decryptPii passthrough: erasure sentinel must NOT be treated as encrypted blob
      try {
        const sentinelDecrypted = decryptPii(erasureSent, { type: "tenant", tenantId: TENANT_A });
        if (sentinelDecrypted === erasureSent) {
          pass("h5 — decryptPii passthrough: erasure sentinel returned as-is (no ENC: prefix → no decrypt attempt)");
        } else {
          fail("h5 — decryptPii passthrough for erasure sentinel", `expected passthrough, got "${sentinelDecrypted}"`);
        }
      } catch (e) {
        fail("h5 — decryptPii passthrough for erasure sentinel", `threw: ${String(e)}`);
      }
    }

    // ── (i) Thread D — DSAR decrypt fix verification ─────────────────────────
    // Proves the decryptPii logic used by the dsar-purge-worker fix:
    //   decryptPii(encrypted_blob, ctx) = plaintext (restores NID from encrypted DSAR row)
    //   decryptPii(plaintext, ctx)      = plaintext (passthrough — safe for legacy unencrypted rows)
    console.log("\n(i) Thread D — DSAR decrypt fix logic (decryptPii dual-mode)");
    {
      const piiCtx = { type: "tenant" as const, tenantId: TENANT_A };
      const encBlob = encryptPii(NID_1_RAW, piiCtx);

      try {
        const dec = decryptPii(encBlob, piiCtx);
        if (canonical(dec) === canonical(NID_1_RAW)) {
          pass("i1 — decryptPii(encrypted_blob, ctx) = plaintext", `dec="${dec}"`);
        } else {
          fail("i1 — decryptPii(encrypted_blob, ctx) = plaintext", `dec="${dec}" ≠ "${NID_1_RAW}"`);
        }
      } catch (e) {
        fail("i1 — decryptPii(encrypted_blob, ctx) does not throw", String(e));
      }

      // Passthrough: plaintext NID (legacy unencrypted DSAR row) → returned as-is
      try {
        const passthrough = decryptPii(NID_1_RAW, piiCtx);
        if (passthrough === NID_1_RAW) {
          pass("i2 — decryptPii(plaintext, ctx) passthrough (no ENC: prefix → returned as-is)");
        } else {
          fail("i2 — decryptPii(plaintext, ctx) passthrough", `got "${passthrough}" expected "${NID_1_RAW}"`);
        }
      } catch (e) {
        fail("i2 — decryptPii(plaintext, ctx) passthrough does not throw", String(e));
      }
    }

    // ── (j) Thread B — buildRedactedFields correctness ────────────────────────
    // Proves that hashOwnerNationalId must be applied to the DECRYPTED plaintext,
    // not to the encrypted blob. If applied to the blob, the erasure sentinel hash
    // differs from what a lookup-by-sentinel would expect.
    console.log("\n(j) Thread B — buildRedactedFields: decrypt-before-hash is required");
    {
      const piiCtx = { type: "tenant" as const, tenantId: TENANT_A };
      const encBlob = encryptPii(NID_1_RAW, piiCtx);

      const sentinelFromPlaintext  = hashOwnerNationalId(NID_1_RAW);
      const sentinelFromBlob       = hashOwnerNationalId(encBlob);

      if (sentinelFromPlaintext !== sentinelFromBlob) {
        pass("j1 — hashOwnerNationalId(plaintext) ≠ hashOwnerNationalId(encrypted_blob)",
          `plaintext sentinel[0..24]=${sentinelFromPlaintext.substring(0, 24)}, ` +
          `blob sentinel[0..24]=${sentinelFromBlob.substring(0, 24)} — decrypt-before-hash is required`);
      } else {
        fail("j1 — hashOwnerNationalId(plaintext) ≠ hashOwnerNationalId(encrypted_blob)",
          "sentinel is the same regardless of input — the hash function lost its discriminatory power");
      }

      // The correct sentinel (from plaintext) starts with "ubo_hash:v1:"
      if (sentinelFromPlaintext.startsWith(HASH_PREFIX)) {
        pass("j2 — correct erasure sentinel (plaintext-derived) has expected prefix", sentinelFromPlaintext.substring(0, 28));
      } else {
        fail("j2 — correct erasure sentinel prefix", `got ${sentinelFromPlaintext.substring(0, 20)}`);
      }

      // The blob-derived sentinel also has the prefix (but would be the wrong hash)
      // This confirms the prefix test alone can't distinguish correct from incorrect hash.
      if (sentinelFromBlob.startsWith(HASH_PREFIX)) {
        pass("j3 — blob-derived sentinel also has 'ubo_hash:v1:' prefix (prefix alone insufficient — decryption is the gate)");
      } else {
        fail("j3 — blob-derived sentinel prefix", `got ${sentinelFromBlob.substring(0, 20)}`);
      }
    }

    // ── (k) Thread C — null/empty tenantId safe-no-op ────────────────────────
    // After T004a, getUboRegistrationsByNationalId returns [] when tenantId is null/empty.
    // Verified by confirming that the HMAC computed with "" tenant key differs from
    // the real HMAC, so a lookup with empty-string-derived HMAC finds no rows.
    console.log("\n(k) Thread C — null tenantId HMAC safe-no-op");
    {
      const emptyTenantHmac = computeLookupHash(NID_1_RAW, "", "ubo_registrations", "owner_national_id");
      const realTenantHmac  = computeLookupHash(NID_1_RAW, TENANT_A, "ubo_registrations", "owner_national_id");

      if (emptyTenantHmac !== realTenantHmac) {
        pass("k1 — empty-tenant HMAC differs from real-tenant HMAC (null tenantId lookup cannot find rows)");
      } else {
        fail("k1 — empty-tenant HMAC must differ from real-tenant HMAC",
          "same hash — null tenant lookup could accidentally match real rows");
      }

      const { rows: emptyRows } = await client.query(
        `SELECT ubo_ref FROM ubo_registrations WHERE owner_national_id_hmac = $1 LIMIT 1`,
        [emptyTenantHmac],
      );
      if (emptyRows.length === 0) {
        pass("k2 — empty-tenant HMAC lookup finds zero rows (null tenantId path returns empty)");
      } else {
        fail("k2 — empty-tenant HMAC lookup must find zero rows",
          `found ${emptyRows.length} row(s) — empty-tenant HMAC should not match any real row`);
      }
    }

  } finally {
    try {
      await cleanup(client, insertedRefs);
      console.log(`\n  [cleanup] removed ${insertedRefs.length} test rows`);
    } catch (e) {
      console.error(`  [cleanup] WARN: cleanup failed: ${e}`);
    }
    client.release();
    await pool.end();
  }

  // ── Summary ──────────────────────────────────────────────────────────────
  const total   = results.length;
  const passing = results.filter(r => r.pass).length;
  const failing = results.filter(r => !r.pass);

  console.log(`\n──────────────────────────────────────────────────────`);
  console.log(`T004a Tier D — ubo_registrations.owner_national_id: ${passing}/${total} PASS`);

  if (failing.length > 0) {
    console.log(`\nFAILED checks:`);
    for (const f of failing) {
      console.error(`  ✗ ${f.label}: ${f.detail}`);
    }
    process.exit(1);
  } else {
    console.log(`All ${total} checks PASS — treatment-(ii) HMAC wiring for ubo_registrations.owner_national_id verified.`);
    process.exit(0);
  }
}

runVerification().catch(err => {
  console.error("Unexpected error:", err);
  process.exit(1);
});
