/**
 * T004a Tier D — kyc_customers.national_id HMAC verification suite
 *
 * Proves the (ii) treatment wiring is correct for kyc_customers.national_id:
 *
 *   (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 (dash, space, case) all produce
 *       the same HMAC → same row found.
 *   (c) Collision-avoidance: two different national IDs produce different hashes;
 *       lookup for ID-A does not return the ID-B row.
 *   (d) Per-tenant isolation: same plaintext national ID under two different
 *       tenantIds produces different HMACs → each tenant finds only its own row.
 *   (e) (i) columns intact: all six treat-(i) columns on kyc_customers still
 *       encrypt and decrypt correctly (regression check).
 *   (f) Unique index present: information_schema confirms kyc_national_id_hmac_unique
 *       index exists on kyc_customers(national_id_hmac).
 *   (g) Rule 18 positive-control interlock: HMAC lookup must find the row (positive)
 *       AND the wrong-tenant lookup must return null (negative). A denial pass without
 *       its paired positive pass is reported as HOLLOW-GREEN (FAIL).
 *
 * No interaction with application code paths (routes, middleware, RLS). Pure
 * database-layer wiring proof using the DATABASE_URL from the dev environment.
 *
 * Run: npx tsx scripts/t004a-tier-d-kyc-verify.ts
 */

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

const { Pool } = pg;

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

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

// Two distinct national IDs — canonical forms must differ.
const NID_1_RAW     = "UG-NIN-1234567";          // canonical: UGNIN1234567
const NID_1_DASH    = "UG-NIN-1234567";           // same — dash form
const NID_1_SPACE   = "UG NIN 1234567";           // same — space form
const NID_1_LOWER   = "ug-nin-1234567";           // same — lowercase form
const NID_2_RAW     = "UG-NIN-9999999";           // canonical: UGNIN9999999 — DIFFERENT

// Verify canonical() normalises all 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);
}

// ── 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 ──────────────────────────────────────────────────────────

async function insertTestRow(client: pg.PoolClient, opts: {
  id: string;
  nationalId: string;
  tenantId: string;
}): Promise<void> {
  const piiCtx = { type: "tenant" as const, tenantId: opts.tenantId };
  const encNationalId = encryptPii(opts.nationalId, piiCtx);
  const hmac = computeLookupHash(opts.nationalId, opts.tenantId, "kyc_customers", "national_id");
  const encFullName    = encryptPii("Test Customer", piiCtx);
  const encDob         = encryptPii("1990-01-01",    piiCtx);
  const encPhone       = encryptPii("+256700000000", piiCtx);

  await client.query(
    `INSERT INTO kyc_customers
       (id, national_id, national_id_hmac, full_name, date_of_birth, phone_number,
        status, risk_tier, tenant_id, created_at, updated_at)
     VALUES ($1, $2, $3, $4, $5, $6, 'PENDING', 'LOW', $7, NOW(), NOW())
     ON CONFLICT (id) DO NOTHING`,
    [opts.id, encNationalId, hmac, encFullName, encDob, encPhone, opts.tenantId],
  );
}

async function lookupByHmac(client: pg.PoolClient, nationalId: string, tenantId: string) {
  const hash = computeLookupHash(nationalId, tenantId, "kyc_customers", "national_id");
  const { rows } = await client.query<{
    id: string; national_id: string; national_id_hmac: string | null;
    full_name: string; date_of_birth: string; phone_number: string; tenant_id: string | null;
  }>(
    `SELECT id, national_id, national_id_hmac, full_name, date_of_birth, phone_number, tenant_id
       FROM kyc_customers WHERE national_id_hmac = $1 LIMIT 1`,
    [hash],
  );
  return rows[0] ?? null;
}

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

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

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

  const insertedIds: string[] = [];
  const idA1 = `kyc-td-a1-${Date.now()}`;
  const idA2 = `kyc-td-a2-${Date.now()}`;
  const idB1 = `kyc-td-b1-${Date.now()}`;

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

    // ── (f) Unique index present ─────────────────────────────────────────────
    console.log("(f) Unique index check");
    {
      const { rows } = await client.query<{ indexname: string; indexdef: string }>(
        `SELECT indexname, indexdef
           FROM pg_indexes
          WHERE tablename = 'kyc_customers'
            AND indexname = 'kyc_national_id_hmac_unique'`,
      );
      if (rows.length === 0) {
        fail("f1 — kyc_national_id_hmac_unique index exists", "index NOT FOUND in pg_indexes");
      } else {
        pass("f1 — kyc_national_id_hmac_unique index exists", `indexdef: ${rows[0].indexdef}`);
      }

      // Verify national_id_hmac column is present with type text
      const { rows: colRows } = await client.query<{ udt_name: string }>(
        `SELECT udt_name FROM information_schema.columns
          WHERE table_schema = 'public'
            AND table_name = 'kyc_customers'
            AND column_name = 'national_id_hmac'`,
      );
      if (colRows.length === 0) {
        fail("f2 — national_id_hmac column exists (text)", "column NOT FOUND");
      } else {
        const t = colRows[0].udt_name;
        if (t === "text") {
          pass("f2 — national_id_hmac column exists (text)", `udt_name=${t}`);
        } else {
          fail("f2 — national_id_hmac column type=text", `actual udt_name=${t}`);
        }
      }

      // Verify the old global unique constraint on national_id is gone
      const { rows: conRows } = await client.query<{ conname: string }>(
        `SELECT conname FROM pg_constraint
          WHERE conrelid = 'kyc_customers'::regclass
            AND contype = 'u'
            AND pg_get_constraintdef(oid) LIKE '%national_id%'
            AND pg_get_constraintdef(oid) NOT LIKE '%national_id_hmac%'`,
      );
      if (conRows.length > 0) {
        fail("f3 — global unique constraint on national_id removed", `still present: ${conRows.map(r => r.conname).join(", ")}`);
      } else {
        pass("f3 — global unique constraint on national_id removed");
      }
    }

    // ── Insert test rows ─────────────────────────────────────────────────────
    // idA1: tenant A, NID_1
    // idA2: tenant A, NID_2  (collision-avoidance test)
    // idB1: tenant B, NID_1  (per-tenant isolation test — same plaintext, different tenant)
    await insertTestRow(client, { id: idA1, nationalId: NID_1_RAW,  tenantId: TENANT_A });
    await insertTestRow(client, { id: idA2, nationalId: NID_2_RAW,  tenantId: TENANT_A });
    await insertTestRow(client, { id: idB1, nationalId: NID_1_RAW,  tenantId: TENANT_B });
    insertedIds.push(idA1, idA2, idB1);

    // ── (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.id !== idA1) {
          fail("a1 — HMAC lookup finds correct row", `expected id=${idA1} got id=${row.id}`);
        } else {
          pass("a1 — HMAC lookup finds correct row (idA1)", `id=${row.id}`);
        }

        // Decrypt the blob
        const piiCtx = { type: "tenant" as const, tenantId: TENANT_A };
        let decrypted: string;
        try {
          decrypted = decryptPii(row.national_id, piiCtx);
        } catch (e) {
          fail("a2 — national_id blob decrypts without error", String(e));
          decrypted = "";
        }

        if (decrypted) {
          if (canonical(decrypted) === canonical(NID_1_RAW)) {
            pass("a2 — decrypted national_id canonical-equals plaintext", `decrypted="${decrypted}"`);
          } else {
            fail("a2 — decrypted national_id canonical-equals plaintext", `decrypted="${decrypted}" canonical="${canonical(decrypted)}" expected="${canonical(NID_1_RAW)}"`);
          }
        }

        // Verify HMAC column value is not plaintext
        const storedHmac = row.national_id_hmac ?? "";
        const isPlaintext = storedHmac === NID_1_RAW || storedHmac === NID_1_RAW.toUpperCase();
        if (isPlaintext) {
          fail("a3 — national_id_hmac is not plaintext national ID", `stored value equals plaintext "${storedHmac}"`);
        } else {
          pass("a3 — national_id_hmac is not plaintext national ID", `stored=${storedHmac.substring(0, 16)}...`);
        }

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

    // ── (b) Canonical() consistency: format variations find same row ──────────
    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?.id === idA1) {
        pass("b1 — dash form finds same row as exact input", `id=${rowDash.id}`);
      } else {
        fail("b1 — dash form finds same row", `got id=${rowDash?.id ?? "null"} expected ${idA1}`);
      }
      if (rowSpace?.id === idA1) {
        pass("b2 — space form finds same row", `id=${rowSpace.id}`);
      } else {
        fail("b2 — space form finds same row", `got id=${rowSpace?.id ?? "null"} expected ${idA1}`);
      }
      if (rowLower?.id === idA1) {
        pass("b3 — lowercase form finds same row", `id=${rowLower.id}`);
      } else {
        fail("b3 — lowercase form finds same row", `got id=${rowLower?.id ?? "null"} expected ${idA1}`);
      }
    }

    // ── (c) Collision-avoidance: different IDs don't cross-match ─────────────
    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 (idA2)", "returned null — HOLLOW-GREEN risk");
      } else if (rowFor2.id !== idA2) {
        fail("c1 (positive control) — NID_2 lookup finds idA2", `got id=${rowFor2.id}`);
      } else {
        pass("c1 (positive control) — NID_2 lookup finds idA2", `id=${rowFor2.id}`);
      }

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

      // NID_1 lookup must NOT return NID_2 row
      const rowFor1 = await lookupByHmac(client, NID_1_RAW, TENANT_A);
      if (rowFor1?.id === idA2) {
        fail("c3 — NID_1 lookup does not return NID_2 row", `got idA2 (${idA2}) — 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)");
    {
      // Positive control: tenant A finds its own NID_1 row
      const rowA = await lookupByHmac(client, NID_1_RAW, TENANT_A);
      const positivePass = rowA?.id === idA1;

      // Negative control: tenant B's HMAC for the same plaintext must NOT return tenant A's row
      const hashB = computeLookupHash(NID_1_RAW, TENANT_B, "kyc_customers", "national_id");
      const hashA = computeLookupHash(NID_1_RAW, TENANT_A, "kyc_customers", "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)})`);
      }

      if (positivePass) {
        pass("d2 (positive control) — tenant A HMAC lookup finds its own row (idA1)", `id=${rowA!.id}`);
      } else {
        fail("d2 (positive control) — tenant A HMAC lookup finds its own row (idA1)", `got id=${rowA?.id ?? "null"} — HOLLOW-GREEN risk if d3 passes`);
      }

      // Tenant A's HMAC queried from tenant B perspective: should find idB1 (tenant B's row),
      // not idA1. That is: the HMAC isolation means looking up NID_1 as tenant A returns tenant
      // A's row, and looking up NID_1 as tenant B returns tenant B's row — never the other.
      const rowBLookup = await lookupByHmac(client, NID_1_RAW, TENANT_B);
      if (!rowBLookup) {
        fail("d3 (positive control) — tenant B HMAC lookup finds its own row (idB1)", "returned null — HOLLOW-GREEN risk");
      } else if (rowBLookup.id === idB1) {
        pass("d3 (positive control) — tenant B HMAC lookup finds idB1", `id=${rowBLookup.id}`);
      } else {
        fail("d3 (positive control) — tenant B HMAC lookup finds idB1", `got id=${rowBLookup.id}`);
      }

      // Cross-check: tenant A's HMAC must NOT return tenant B's row and vice versa
      const wrongTenantARow = rowBLookup?.id === idA1;
      const wrongTenantBRow = rowA?.id === idB1;

      if (wrongTenantARow) {
        fail("d4 — tenant B lookup does not return tenant A's row", `got idA1 (${idA1}) — 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 idB1 (${idB1}) — 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 (!positivePass && !wrongTenantARow) {
        fail("d-RULE18 — denial result is HOLLOW-GREEN", "d2 positive control failed AND d4 denial passed — wrong-tenant lookup returned null but own-tenant lookup also returned null; cannot distinguish isolation from broken write");
      } else if (positivePass && !wrongTenantARow && !wrongTenantBRow && rowBLookup?.id === idB1) {
        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");
    {
      const { rows } = await client.query<{
        full_name: string; date_of_birth: string; phone_number: string; tenant_id: string | null;
      }>(
        `SELECT full_name, date_of_birth, phone_number, tenant_id
           FROM kyc_customers WHERE id = $1`,
        [idA1],
      );
      if (rows.length === 0) {
        fail("e1 — row retrievable for (i) column check", `id=${idA1} not found`);
      } else {
        const row = rows[0];
        const piiCtx = { type: "tenant" as const, tenantId: TENANT_A };

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

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

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

    // ── (g) Wrong-tenant HMAC cannot decrypt correct row ─────────────────────
    console.log("\n(g) Cross-tenant AES auth-tag rejection (wrong key cannot decrypt)");
    {
      // Fetch raw encrypted blob from tenant A's row
      const { rows } = await client.query<{ national_id: string }>(
        `SELECT national_id FROM kyc_customers WHERE id = $1`, [idA1],
      );
      if (rows.length === 0) {
        fail("g1 — raw row retrievable for cross-key rejection test", "row not found");
      } else {
        const blob = rows[0].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");
        }
      }
    }

  } finally {
    // ── Cleanup ──────────────────────────────────────────────────────────────
    try {
      await cleanup(client, insertedIds);
      console.log(`\n  [cleanup] removed ${insertedIds.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 — kyc_customers.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 kyc_customers.national_id verified.`);
    process.exit(0);
  }
}

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