/**
 * vf-w4c-ip-policy-proof.ts  (AS/PLATFORM/2026/007 — Wave W4c)
 *
 * Behavioral substrate proof for server/lib/ip-policy-manager.ts — the IP
 * allow/block control. Drives the REAL `ipPolicyManager` methods through the
 * REAL `db` ALS-proxy as the RLS-subject role `aegis_app` inside a faithful
 * request-tx wrapper (the vf-s5 mechanism: db.transaction on appPool/aegis_app
 * + LOCAL app.current_tenant_id GUC + tenantContext ALS runner). ip_policies
 * IS an RLS-scoped table (rls-init.ts:89), so this exercises the real
 * enforcement axis.
 *
 * Paired POS/NEG (Rule 18), all as aegis_app, on SYNTHETIC self-cleaning rows:
 *   - block exact-IP   -> blocked (NEG) ; unrelated IP -> allowed (POS)
 *   - block CIDR       -> in-range blocked (NEG) ; out-of-range allowed (POS)
 *   - allowlist        -> listed IP allowed (POS) ; unlisted IP denied (NEG default-deny)
 *   - RLS isolation    -> getPolicies(undefined) (NO app tenant filter) returns
 *                         ONLY the GUC-tenant's rows: tenant-A sees its 2 rows
 *                         and NOT tenant-B's (POS+NEG); tenant-B vice-versa.
 *
 * HOLLOW-GREEN GUARDS: owner pool (DATABASE_URL, BYPASSRLS) seeds/cleans ONLY;
 *   never an enforcement proof. Fixture gate confirms each row physically
 *   landed with the expected tenant_id before any conclusion. Two FRESH
 *   synthetic tenant ids => zero pre-existing rows confound determinism.
 *
 * WRITES: synthetic ip_policies rows (marker-tagged), deleted + 0-residual
 *   verified at end (Rule 11). Requires DATABASE_URL + AEGIS_APP_DB_PASSWORD.
 * Run: cd /home/runner/workspace && timeout 115 npx tsx scripts/vf-w4c-ip-policy-proof.ts
 */
import pg from "pg";
import { sql } from "drizzle-orm";
import { db, tenantContext, type DrizzleDb } from "../server/db";
import { ipPolicyManager } from "../server/lib/ip-policy-manager";

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

const owner = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const TS = Date.now();
const TA = `w4c-ippol-a-${TS}`;
const TB = `w4c-ippol-b-${TS}`;
const MARKER = `W4C-IPPOL-${TS}`;

async function withRequestTx<T>(tenantId: string, fn: () => Promise<T>): Promise<T> {
  return await db.transaction(async (tx) => {
    await tx.execute(sql`SELECT set_config('app.current_tenant_id', ${tenantId}, true)`);
    return await tenantContext.run({ runner: tx as unknown as DrizzleDb, tenantId }, fn);
  });
}

type Leg = { id: string; label: string; pass: boolean; detail: string };
const legs: Leg[] = [];
const rec = (id: string, label: string, pass: boolean, detail: string) => {
  legs.push({ id, label, pass, detail });
  console.log(`  [${pass ? "PASS" : "FAIL"}] ${id} — ${label}\n         ${detail}`);
};

async function main() {
  console.log("=== W4c — ip-policy-manager substrate proof (aegis_app, dev) ===");
  console.log(`  tenants: A=${TA}  B=${TB}\n`);

  // ---- create policies, each within its own tenant's GUC (WITH CHECK) ----
  const { idBlockExact, idBlockCidr } = await withRequestTx(TA, async () => {
    const be = await ipPolicyManager.createPolicy({ tenantId: TA, policyType: "block", ipAddress: "203.0.113.10", description: MARKER });
    const bc = await ipPolicyManager.createPolicy({ tenantId: TA, policyType: "block", ipAddress: "0.0.0.0", cidrRange: "10.0.0.0/8", description: MARKER });
    return { idBlockExact: be.id, idBlockCidr: bc.id };
  });
  const idAllow = await withRequestTx(TB, async () => {
    const a = await ipPolicyManager.createPolicy({ tenantId: TB, policyType: "allow", ipAddress: "192.168.1.50", description: MARKER });
    return a.id;
  });

  // ---- fixture gate (owner; setup-only) ----
  const phys = await owner.query<{ id: string; tenant_id: string }>(
    `SELECT id, tenant_id FROM ip_policies WHERE id = ANY($1::text[])`, [[idBlockExact, idBlockCidr, idAllow]]);
  const pm = new Map(phys.rows.map(r => [r.id, r.tenant_id]));
  const gateOk = pm.get(idBlockExact) === TA && pm.get(idBlockCidr) === TA && pm.get(idAllow) === TB;
  rec("GATE", "physical tenant_id == expected (defeats hollow-green)", gateOk,
    `blockExact=${pm.get(idBlockExact)} blockCidr=${pm.get(idBlockCidr)} allow=${pm.get(idAllow)}`);
  if (!gateOk) { await cleanup(); await finish(); return; }

  // ---- tenant-A: block legs + RLS isolation ----
  await withRequestTx(TA, async () => {
    const rExact = await ipPolicyManager.checkIp("203.0.113.10", TA);
    rec("A1", "block exact-IP -> blocked (NEG)", rExact.allowed === false && /Blocked/.test(rExact.reason), JSON.stringify(rExact));

    const rCidr = await ipPolicyManager.checkIp("10.5.5.5", TA);
    rec("A2", "block CIDR in-range -> blocked (NEG)", rCidr.allowed === false && /Blocked/.test(rCidr.reason), JSON.stringify(rCidr));

    const rOut = await ipPolicyManager.checkIp("11.0.0.1", TA);
    rec("A3", "unrelated IP (no allowlist present) -> allowed (POS)", rOut.allowed === true && /No restrictive/.test(rOut.reason), JSON.stringify(rOut));

    const pols = await ipPolicyManager.getPolicies(undefined); // NO app tenant filter
    const ids = pols.map(p => p.id);
    const onlyMineA = ids.every(i => i === idBlockExact || i === idBlockCidr);
    rec("A4", "RLS: getPolicies(undefined) returns ONLY tenant-A rows (POS own + NEG not-B)",
      ids.includes(idBlockExact) && ids.includes(idBlockCidr) && !ids.includes(idAllow) && onlyMineA,
      `n=${ids.length} sawBlockExact=${ids.includes(idBlockExact)} sawBlockCidr=${ids.includes(idBlockCidr)} sawAllow(B)=${ids.includes(idAllow)} onlyMine=${onlyMineA}`);
  });

  // ---- tenant-B: allowlist default-deny + RLS isolation ----
  await withRequestTx(TB, async () => {
    const rListed = await ipPolicyManager.checkIp("192.168.1.50", TB);
    rec("B1", "allowlisted IP -> allowed (POS)", rListed.allowed === true && /allowlist/.test(rListed.reason), JSON.stringify(rListed));

    const rUnlisted = await ipPolicyManager.checkIp("8.8.8.8", TB);
    rec("B2", "unlisted IP under an allowlist -> denied (NEG default-deny)", rUnlisted.allowed === false && /not in allowlist/.test(rUnlisted.reason), JSON.stringify(rUnlisted));

    const pols = await ipPolicyManager.getPolicies(undefined);
    const ids = pols.map(p => p.id);
    const onlyMineB = ids.every(i => i === idAllow);
    rec("B3", "RLS: getPolicies(undefined) returns ONLY tenant-B row (POS own + NEG not-A)",
      ids.includes(idAllow) && !ids.includes(idBlockExact) && !ids.includes(idBlockCidr) && onlyMineB,
      `n=${ids.length} sawAllow=${ids.includes(idAllow)} sawBlockExact(A)=${ids.includes(idBlockExact)} onlyMine=${onlyMineB}`);
  });

  await cleanup();
  await finish();
}

async function cleanup() {
  await owner.query(`DELETE FROM ip_policies WHERE description = $1`, [MARKER]);
  const residual = await owner.query(`SELECT id FROM ip_policies WHERE description = $1`, [MARKER]);
  rec("CLEAN", "Rule 11 — 0 residual probe rows", residual.rowCount === 0, `residual=${residual.rowCount}`);
}

async function finish() {
  const ok = legs.every(l => l.pass);
  console.log(`\n${legs.filter(l => l.pass).length}/${legs.length} legs PASS — ${ok ? "ALL PASS" : "FAIL"}`);
  await owner.end();
  process.exit(ok ? 0 : 1);
}

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