/**
 * FU-056 — UBO-Erasure RLS Gap Fix Verification Suite
 *
 * Proves that the withUboTenantCtx fix (D-2-B per architect Rule 9 PASS 2026-06-05)
 * correctly provides per-operation GUC context for background-worker UBO erasure calls.
 *
 * Five probes covering all mandated verification requirements from the design brief §10:
 *
 *   Probe 1 — POSITIVE storage routing: withTenantRls(tenantA) + getUboRegistrationsByCorporate
 *             with _db threading returns the tenant-A row (visible under own-tenant GUC).
 *             Rule 18 positive-control anchor for Probe 2.
 *
 *   Probe 2 — NEGATIVE isolation (Rule 18 interlock): same row, withTenantRls(tenantB) returns
 *             0 rows. Counted as HOLLOW-GREEN FAIL if Probe 1 did not pass (deny-all produces
 *             same zero without enforcement).
 *
 *   Probe 3 — processCorporateErasure POSITIVE end-to-end: erasedCount > 0 with real tenantId.
 *             Pre-fix, GUC=NULL → storage returns 0 rows → false-success. Post-fix,
 *             withUboTenantCtx sets GUC → row is found and erased.
 *
 *   Probe 4 — Null-tenantId RLS block (honest zero): processCorporateErasure with tenantId=null
 *             → withUboTenantCtx falls through to bare appPool db (no GUC set) → RLS blocks
 *             all reads (current_tenant_id()=NULL) → erasedCount=0 honestly. Proves null path
 *             cannot produce false-success from wrong-tenant rows.
 *
 *   Probe 5 — flagUboForIndividualErasure POSITIVE (Thread D decrypt→flag): flaggedCount > 0
 *             with real tenantId. Proves Thread D end-to-end with withUboTenantCtx.
 *
 * Rule 18 interlock: Probe 2 denial only counts as PASS if Probe 1 positive also passed.
 * A hollow-green (deny-all) state would make Probe 2 pass trivially — the interlock catches it.
 *
 * Run: npx tsx scripts/fu056-ubo-erasure-rls-verify.ts
 */

import pg from "pg";
import crypto from "crypto";
import {
  encryptPii,
  computeLookupHash,
} from "../server/lib/pii-encryption";
import { withTenantRls } from "../server/db";
import { storage } from "../server/storage";
import {
  processCorporateErasure,
  flagUboForIndividualErasure,
} from "../server/lib/ubo-erasure";

const { Pool } = pg;

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

const RUN_ID     = `FU056-${Date.now().toString(36).toUpperCase()}`;
const TENANT_A   = `fu056-verify-tenant-a-${Date.now().toString(36)}`;
const TENANT_B   = `fu056-verify-tenant-b-${Date.now().toString(36)}`;
const CORP_REF   = `CORP-${RUN_ID}`;
// Per-probe NIDs: each probe uses a distinct NID so the HMAC unique constraint
// (ubo_owner_national_id_hmac_unique) is never violated across probes. All probes
// run with rows still present in the table (cleanup is deferred to finally).
const NID_P1     = `FU056-NID-P1-${crypto.randomBytes(3).toString("hex").toUpperCase()}`;
const NID_P3     = `FU056-NID-P3-${crypto.randomBytes(3).toString("hex").toUpperCase()}`;
const NID_P4     = `FU056-NID-P4-${crypto.randomBytes(3).toString("hex").toUpperCase()}`;
const NID_P5     = `FU056-NID-P5-${crypto.randomBytes(3).toString("hex").toUpperCase()}`;

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

type Result = { label: string; pass: boolean; detail?: string };
const results: Result[] = [];
let probe1Passed = false; // Rule 18 interlock anchor

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}`);
}
function hollowGreenFail(label: string, detail: string) {
  results.push({ label, pass: false, detail: `HOLLOW-GREEN (Rule 18 fail): ${detail}` });
  console.error(`  ✗ ${label} — HOLLOW-GREEN FAIL (Rule 18): ${detail}`);
}

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

function newUboRef(): string {
  return `UBO-FU056-${Date.now().toString(36).toUpperCase()}-${crypto.randomBytes(2).toString("hex").toUpperCase()}`;
}

async function insertTestRow(client: pg.PoolClient, opts: {
  uboRef:        string;
  corporateRef:  string;
  ownerNid:      string;
  tenantId:      string;
  erasureState?: string;
}): Promise<void> {
  const piiCtx = { type: "tenant" as const, tenantId: opts.tenantId };
  const encNid      = encryptPii(opts.ownerNid,        piiCtx);
  const encName     = encryptPii("FU056 Test Owner",   piiCtx);
  const encNation   = encryptPii("Ugandan",            piiCtx);
  const hmac        = computeLookupHash(opts.ownerNid, opts.tenantId, "ubo_registrations", "owner_national_id");

  const state = opts.erasureState ?? "ACTIVE";
  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, erasure_state)
     VALUES ($1, $2, $3, $4, $5, $6, $7, 30, 'shareholder', 0, 0, NOW(), 'fu056-verify', $8, $9)
     ON CONFLICT (ubo_ref) DO NOTHING`,
    [
      opts.uboRef,
      opts.corporateRef,
      "FU056 Corporate Ltd",
      encName,
      encNid,
      hmac,
      encNation,
      opts.tenantId,
      state,
    ],
  );
}

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],
  );
  // Also clean test audit_logs rows created by erasure
  await client.query(
    `DELETE FROM audit_logs WHERE resource LIKE 'ubo:UBO-FU056%'`,
  );
}

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

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

  try {
    console.log(`\n── FU-056 UBO Erasure RLS Gap Fix — Verification Suite (runId: ${RUN_ID}) ──\n`);

    // ── Probe 1 + 2: Storage routing + Rule 18 interlock ─────────────────────
    console.log("Probes 1–2: RLS routing via withTenantRls + _db threading (Rule 18 interlock)");
    {
      const refP1 = newUboRef();
      inserted.push(refP1);
      await insertTestRow(client, { uboRef: refP1, corporateRef: CORP_REF, ownerNid: NID_P1, tenantId: TENANT_A });

      // Probe 1 — POSITIVE: tenant-A context should find the row
      const rowsA = await withTenantRls(TENANT_A, async (qdb) =>
        storage.getUboRegistrationsByCorporate(CORP_REF, TENANT_A, { _db: qdb })
      );
      if (rowsA.length === 1 && rowsA[0].uboRef === refP1) {
        pass("Probe 1 — positive: tenant-A GUC → getUboRegistrationsByCorporate returns own row",
          `uboRef=${refP1} found`);
        probe1Passed = true;
      } else {
        fail("Probe 1 — positive: tenant-A GUC → getUboRegistrationsByCorporate returns own row",
          `expected 1 row with ref=${refP1}, got ${rowsA.length} rows`);
      }

      // Probe 2 — NEGATIVE + Rule 18 interlock: tenant-B context must return 0 rows
      const rowsB = await withTenantRls(TENANT_B, async (qdb) =>
        storage.getUboRegistrationsByCorporate(CORP_REF, TENANT_B, { _db: qdb })
      );
      if (!probe1Passed) {
        hollowGreenFail(
          "Probe 2 — negative (Rule 18 interlock): tenant-B GUC → 0 rows for tenant-A row",
          "Probe 1 did not pass — deny-zero without positive-enforcement is HOLLOW-GREEN"
        );
      } else if (rowsB.length === 0) {
        pass("Probe 2 — negative (Rule 18 interlock): tenant-B GUC → 0 rows for tenant-A row",
          "isolation enforced; Probe 1 positive anchor intact");
      } else {
        fail("Probe 2 — negative: tenant-B GUC returned rows it should not see",
          `got ${rowsB.length} row(s): ${rowsB.map(r => r.uboRef).join(", ")}`);
      }
    }

    // ── Probe 3: processCorporateErasure POSITIVE ─────────────────────────────
    console.log("\nProbe 3: processCorporateErasure positive end-to-end (erasedCount > 0)");
    {
      const refP3 = newUboRef();
      inserted.push(refP3);
      await insertTestRow(client, { uboRef: refP3, corporateRef: CORP_REF, ownerNid: NID_P3, tenantId: TENANT_A });

      const result = await processCorporateErasure({
        corporateCustomerRef: CORP_REF,
        dsarRequestId:        `DSAR-FU056-P3-${RUN_ID}`,
        tenantId:             TENANT_A,
        actorUserId:          null,
      });

      if (result.erasedCount > 0) {
        pass("Probe 3 — processCorporateErasure positive",
          `erasedCount=${result.erasedCount}, eligibleCount=${result.eligibleCount}, uboRefsErased=[${result.uboRefsErased.join(",")}]`);
      } else {
        fail("Probe 3 — processCorporateErasure positive",
          `erasedCount=${result.erasedCount} (expected > 0). Pre-fix behavior: GUC=NULL → RLS blocks → false-success. eligibleCount=${result.eligibleCount}, raceCount=${result.raceCount}`);
      }
    }

    // ── Probe 4: Null tenantId → honest zero (RLS blocks bare appPool db) ────
    console.log("\nProbe 4: null tenantId → honest zero via RLS (not false-success from wrong-tenant rows)");
    {
      // Insert a fresh row for tenantA so there IS something to find if RLS were bypassed
      const refP4 = newUboRef();
      inserted.push(refP4);
      await insertTestRow(client, { uboRef: refP4, corporateRef: `${CORP_REF}-P4`, ownerNid: NID_P4, tenantId: TENANT_A });

      // processCorporateErasure with null tenantId.
      // withUboTenantCtx(null) → fn(db) where db = appPool Drizzle (aegis_app role, no GUC).
      // T003 RLS: current_tenant_id() = NULL → no rows pass USING clause → 0 rows returned.
      // erasedCount = 0 (honest — the row exists but RLS prevents it being seen).
      const result = await processCorporateErasure({
        corporateCustomerRef: `${CORP_REF}-P4`,
        dsarRequestId:        `DSAR-FU056-P4-${RUN_ID}`,
        tenantId:             null,
        actorUserId:          null,
      });

      if (result.erasedCount === 0 && result.eligibleCount === 0) {
        pass("Probe 4 — null tenantId → honest 0 via RLS (not false-success)",
          `erasedCount=0, eligibleCount=0 — RLS correctly blocks bare appPool with no GUC; row exists but is invisible`);
      } else {
        fail("Probe 4 — null tenantId should yield erasedCount=0 via RLS block",
          `erasedCount=${result.erasedCount}, eligibleCount=${result.eligibleCount} — unexpected: row should be invisible without GUC`);
      }
    }

    // ── Probe 5: flagUboForIndividualErasure POSITIVE (Thread D) ─────────────
    console.log("\nProbe 5: flagUboForIndividualErasure positive (Thread D decrypt→flag, flaggedCount > 0)");
    {
      const refP5 = newUboRef();
      inserted.push(refP5);
      // Insert an ACTIVE row with NID_P5 for tenantA — flagging should succeed
      await insertTestRow(client, {
        uboRef:       refP5,
        corporateRef: `${CORP_REF}-P5`,
        ownerNid:     NID_P5,
        tenantId:     TENANT_A,
        erasureState: "ACTIVE",
      });

      const result = await flagUboForIndividualErasure({
        ownerNationalId: NID_P5,
        dsarRequestId:   `DSAR-FU056-P5-${RUN_ID}`,
        tenantId:        TENANT_A,
        actorUserId:     null,
      });

      if (result.flaggedCount > 0) {
        pass("Probe 5 — flagUboForIndividualErasure positive (Thread D)",
          `flaggedCount=${result.flaggedCount}, surfacedCount=${result.surfacedCount}, uboRefsSurfaced=[${result.uboRefsSurfaced.join(",")}]`);
      } else {
        fail("Probe 5 — flagUboForIndividualErasure positive",
          `flaggedCount=${result.flaggedCount} (expected > 0). Pre-fix: GUC=NULL → HMAC lookup finds 0 rows → surfacedCount=0 → flaggedCount=0. surfacedCount=${result.surfacedCount}, alreadyFlaggedCount=${result.alreadyFlaggedCount}`);
      }
    }

  } finally {
    // ── Cleanup ───────────────────────────────────────────────────────────────
    try {
      await cleanup(client, inserted);
      console.log(`\n  [cleanup] removed ${inserted.length} test row(s) + related audit_logs`);
    } catch (e: any) {
      console.warn(`  [cleanup] WARNING: cleanup failed — ${String(e?.message || e)}`);
      console.warn(`  [cleanup] Manual cleanup: DELETE FROM ubo_registrations WHERE ubo_ref LIKE 'UBO-FU056%'`);
    }
    client.release();
    await pool.end();
  }

  // ── Summary ───────────────────────────────────────────────────────────────
  const passed  = results.filter(r => r.pass).length;
  const total   = results.length;
  const allPass = results.every(r => r.pass);

  console.log(`\n── Summary ──────────────────────────────────────────────────────────────────`);
  console.log(`  runId:   ${RUN_ID}`);
  console.log(`  result:  ${passed}/${total} PASS`);
  if (allPass) {
    console.log("  status:  ALL PASS ✓");
    console.log("  FU-056 fix verified:");
    console.log("    - withUboTenantCtx routes storage calls through withTenantRls (Probes 1–2)");
    console.log("    - processCorporateErasure succeeds with valid tenantId (Probe 3)");
    console.log("    - Null tenantId honestly returns 0 via RLS block, not false-success (Probe 4)");
    console.log("    - flagUboForIndividualErasure succeeds with valid tenantId — Thread D (Probe 5)");
    console.log("    - Rule 18 interlocks intact (denial pass anchored on positive pass in Probe 2)");
  } else {
    console.error("  status:  FAIL ✗ — one or more probes failed");
    results.filter(r => !r.pass).forEach(r => {
      console.error(`    FAIL: ${r.label} — ${r.detail}`);
    });
  }
  console.log("");

  process.exit(allPass ? 0 : 1);
}

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