/**
 * W3a — kyt_results cross-tenant READ discharge probe (AS/PLATFORM/2026/007).
 *
 * Discharges W2c Open gap 5: VF-S5 proved durable tenant-tagged persist +
 * commit/rollback fate for kyt_results as aegis_app, but did NOT test that
 * tenant B cannot READ tenant A's kyt_results row (cross-tenant isolation).
 *
 * This probe seeds TWO synthetic kyt_results rows (one tenant-A, one tenant-B)
 * and runs ALL enforcement assertions as the RLS-subject role `aegis_app`:
 *   - tenant A sees exactly its own row, NOT B's        (POS + NEG, Rule 18)
 *   - tenant B sees exactly its own row, NOT A's        (POS + NEG, Rule 18)
 *   - empty-string GUC  -> 0 rows                        (§13.0 fail-closed)
 *   - never-set  GUC    -> 0 rows                        (§13.0 fail-closed)
 *
 * HOLLOW-GREEN GUARDS:
 *   - Owner connection (DATABASE_URL) is used ONLY to seed the fixture and to
 *     clean up + verify-0-residual. It is NEVER used as an enforcement proof
 *     (owner BYPASSes RLS — an owner read proves nothing about isolation).
 *   - Before drawing any conclusion, the physical tenant_id of each seeded row
 *     is verified == its expected tenant (fixture gate: defeats false-RED from a
 *     mis-seeded row and false-GREEN from a row that silently landed elsewhere).
 *   - Every denial (NEG) is paired with its positive (POS) in the same run.
 *
 * Requires: DATABASE_URL + AEGIS_APP_DB_PASSWORD.
 * Run: cd /home/runner/workspace && timeout 115 npx tsx scripts/w3a-kyt-xtenant-probe.ts
 */
import pg from "pg";
import { randomUUID } from "crypto";

const { Pool } = pg;

if (!process.env.DATABASE_URL) {
  console.error("FATAL: DATABASE_URL is not set.");
  process.exit(1);
}
if (!process.env.AEGIS_APP_DB_PASSWORD) {
  console.error("FATAL: AEGIS_APP_DB_PASSWORD is not set.");
  process.exit(1);
}

const TENANT_A = "aegis-sovereign";
const TENANT_B = "stanbic-ug-001";
const PREFIX = `w3a-kyt-xtenant-${Date.now()}`;
const idA = randomUUID();
const idB = randomUUID();

// Owner pool — FIXTURE SETUP/CLEANUP ONLY. Never an enforcement proof.
const ownerPool = new Pool({ connectionString: process.env.DATABASE_URL });

// App pool — aegis_app restricted RLS-subject role. ALL enforcement assertions.
const appUrl = new URL(process.env.DATABASE_URL);
appUrl.username = "aegis_app";
appUrl.password = process.env.AEGIS_APP_DB_PASSWORD!;
const appPool = new Pool({ connectionString: appUrl.toString() });

type Leg = { name: string; pass: boolean; detail: string };
const legs: Leg[] = [];
function record(name: string, pass: boolean, detail: string) {
  legs.push({ name, pass, detail });
  console.log(`  [${pass ? "PASS" : "FAIL"}] ${name} — ${detail}`);
}

async function seedRow(id: string, tenantId: string) {
  await ownerPool.query(
    `INSERT INTO kyt_results
       (id, user_id, amount, currency, recipient_type, channel,
        risk_level, risk_score, recommendation, tenant_id)
     VALUES ($1,$2,$3,'UGX','KNOWN','API','LOW',$4,'APPROVE',$5)`,
    [id, `${PREFIX}-user`, "1000.00", "0.100", tenantId],
  );
}

/** Count probe rows visible to aegis_app under a given GUC state. */
async function aegisCount(opts: {
  setGuc: boolean;
  guc?: string;
}): Promise<{ total: number; sawA: boolean; sawB: boolean }> {
  const c = await appPool.connect();
  try {
    await c.query("BEGIN");
    if (opts.setGuc) {
      await c.query("SELECT set_config('app.current_tenant_id', $1, true)", [
        opts.guc ?? "",
      ]);
    }
    // Restrict to OUR two probe rows by id so unrelated rows never confound.
    const r = await c.query<{ id: string }>(
      `SELECT id FROM kyt_results WHERE id = ANY($1::text[])`,
      [[idA, idB]],
    );
    await c.query("ROLLBACK");
    const ids = r.rows.map((x) => x.id);
    return {
      total: ids.length,
      sawA: ids.includes(idA),
      sawB: ids.includes(idB),
    };
  } finally {
    c.release();
  }
}

async function main() {
  console.log("=== W3a kyt_results cross-tenant READ discharge probe ===");
  console.log(`  probe ids: A=${idA} (${TENANT_A})  B=${idB} (${TENANT_B})`);

  // ── Fixture seed (owner) ────────────────────────────────────────────────
  await seedRow(idA, TENANT_A);
  await seedRow(idB, TENANT_B);

  // ── Fixture gate: physical tenant_id == expected (owner read, setup only) ─
  const phys = await ownerPool.query<{ id: string; tenant_id: string }>(
    `SELECT id, tenant_id FROM kyt_results WHERE id = ANY($1::text[])`,
    [[idA, idB]],
  );
  const physMap = new Map(phys.rows.map((r) => [r.id, r.tenant_id]));
  const gateOk =
    physMap.get(idA) === TENANT_A && physMap.get(idB) === TENANT_B;
  record(
    "FIXTURE-GATE physical tenant_id == expected",
    gateOk,
    `A.tenant_id=${physMap.get(idA)} (exp ${TENANT_A}); B.tenant_id=${physMap.get(idB)} (exp ${TENANT_B})`,
  );
  if (!gateOk) {
    console.error("  ABORT: fixture gate failed — would be hollow. Cleaning up.");
    await cleanup();
    process.exit(1);
  }

  // ── Enforcement assertions (ALL as aegis_app) ───────────────────────────
  // Tenant A context: sees A (POS), not B (NEG) — Rule 18 interlocked.
  const aCtx = await aegisCount({ setGuc: true, guc: TENANT_A });
  record(
    "tenant-A GUC: sees own row A (POS) AND not B (NEG)",
    aCtx.sawA && !aCtx.sawB && aCtx.total === 1,
    `sawA=${aCtx.sawA} sawB=${aCtx.sawB} total=${aCtx.total}`,
  );

  // Tenant B context: sees B (POS), not A (NEG) — Rule 18 interlocked.
  const bCtx = await aegisCount({ setGuc: true, guc: TENANT_B });
  record(
    "tenant-B GUC: sees own row B (POS) AND not A (NEG)",
    bCtx.sawB && !bCtx.sawA && bCtx.total === 1,
    `sawA=${bCtx.sawA} sawB=${bCtx.sawB} total=${bCtx.total}`,
  );

  // Empty-string GUC: fail-closed -> 0 rows.
  const emptyCtx = await aegisCount({ setGuc: true, guc: "" });
  record(
    "empty-string GUC: 0 rows (§13.0 fail-closed)",
    emptyCtx.total === 0,
    `total=${emptyCtx.total}`,
  );

  // Never-set GUC: fail-closed -> 0 rows.
  const neverCtx = await aegisCount({ setGuc: false });
  record(
    "never-set GUC: 0 rows (§13.0 fail-closed)",
    neverCtx.total === 0,
    `total=${neverCtx.total}`,
  );

  // ── Cleanup (owner) + 0-residual verify ─────────────────────────────────
  await cleanup();

  const overall = legs.every((l) => l.pass);
  console.log("");
  console.log(
    `ACCEPTANCE: ${legs.filter((l) => l.pass).length}/${legs.length} legs PASS — ${overall ? "ALL PASS" : "FAIL"}`,
  );
  await ownerPool.end();
  await appPool.end();
  process.exit(overall ? 0 : 1);
}

async function cleanup() {
  await ownerPool.query(`DELETE FROM kyt_results WHERE id = ANY($1::text[])`, [
    [idA, idB],
  ]);
  const residual = await ownerPool.query(
    `SELECT id FROM kyt_results WHERE id = ANY($1::text[])`,
    [[idA, idB]],
  );
  record(
    "CLEANUP Rule 11 — 0 residual probe rows",
    residual.rowCount === 0,
    `residual=${residual.rowCount}`,
  );
}

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