// ============================================================================
// VF-RP01 — PAM read-path cross-tenant isolation proof (F-RP-01, all 3 tables)
// ----------------------------------------------------------------------------
// ref AS/AEGIS-CYBER/VERIFY/2026/001 | env: DEV | HOLD at publish gate
//
// WHAT THIS PROVES
//   F-RP-01 (CRITICAL): the PAM read path leaked rows across tenants because
//   accounts/requests/sessions were served from process-global Maps (or a
//   non-tenant catalog). Operator Option 3 made ALL THREE tables per-tenant and
//   DB-backed, so the read path is now scoped by Postgres RLS (the request-tx
//   `app.current_tenant_id` GUC). This script proves that closure end-to-end,
//   ONE proof covering all three tables, with paired POSITIVE/NEGATIVE legs.
//
// WHY THIS SHAPE (non-hollow, per the standing rules)
//   • Driven as the least-privilege RLS-subject role `aegis_app` (NOT the owner,
//     which is BYPASSRLS → a hollow green). The owner pool is GROUND TRUTH ONLY
//     (proves the foreign-tenant 0 is RLS scoping, not row absence). [Rule 17/18]
//   • The seed and the POSITIVE read are driven through the REAL service methods
//     the HTTP routes call (privilegedAccess.getAccounts / createAccessRequest /
//     reviewRequest / startSession / getRequests / getSessions), inside the SAME
//     request runtime the middleware builds: a pinned aegis_app client + a real
//     Drizzle tx + the LOCAL tenant GUC + the tenantContext ALS. So the service's
//     `db` proxy resolves to the aegis_app tx client exactly as in a request.
//     (The PAM read HTTP routes themselves are requireRole("admin"); admin
//     credentials are not available to the agent shell, and route auth gates WHO
//     may call — not WHICH tenant's rows are returned. The read-path leak is
//     orthogonal to auth and lives entirely in the service+RLS read path that
//     these calls exercise verbatim.) [route-driven-proof-harness]
//   • Rule 18 (paired): every denial leg (foreign tenant sees 0) is paired with a
//     positive-enforcement leg (owner tenant sees its own row + a well-formed
//     DTO), in the same run, so a green is provably green for its EXACT reason.
//   • "0 is RLS, not a JS filter": getRequests()/getSessions() apply NO tenant JS
//     filter (only status/accountId/requesterId/userId/limit). The negative legs
//     show the service-method result and the raw aegis_app SELECT agree (both 0
//     under GUC=B, both ≥1 under GUC=A) — so the boundary is RLS, not app code.
//
// CLEANUP: all rows for the two synthetic run-scoped tenants are deleted across
//   the three tables (owner pool). Audit-chain / audit_logs rows are immutable by
//   design and are left in place (synthetic, opaque, no PII) — same precedent as
//   vf-s6.
// ============================================================================

import { sql } from "drizzle-orm";
import { appPool, tenantContext, makeClientRunner, type DrizzleDb } from "../server/db";
import {
  privilegedAccess,
  type PrivilegedAccount,
  type AccessRequest,
  type PrivilegedSession,
} from "../server/lib/privileged-access";
import {
  rlsPairedProbe,
  ownerCount,
  ownerQuery,
  endOwnerPool,
  verdict,
  ProofRun,
  type RlsPairedProbeResult,
} from "./lib/proof-harness";

const base = Date.now();
const TENANT_A = `rp01-a-${base}`; // owner tenant — seeds + reads its own rows
const TENANT_B = `rp01-b-${base}`; // foreign tenant — must see NONE of A's rows

const captured = {
  logicalAcctId: "",
  physAcctId: "",
  reqId: "",
  sessId: "",
};

/** FAITHFUL request runtime (the committing variant of vf-s6's runRequestPhaseTx):
 *  a pinned aegis_app client + a real Drizzle tx + the LOCAL tenant GUC, with the
 *  PgTransaction stashed in the same AsyncLocalStorage the middleware uses, so the
 *  service's `db` proxy + any nested db.transaction() resolve to THIS connection
 *  (a real SAVEPOINT) under RLS — the exact production mechanism. */
async function runRequestPhaseTx(
  tenantId: string,
  fn: (tx: DrizzleDb) => Promise<void>,
): Promise<"COMMIT" | "ROLLBACK"> {
  const client = await appPool.connect();
  try {
    const tdb = makeClientRunner(client);
    try {
      await tdb.transaction(async (tx) => {
        await tx.execute(
          sql`SELECT set_config('app.current_tenant_id', ${tenantId}, true)`,
        );
        await tenantContext.run(
          { runner: tx as unknown as DrizzleDb, tenantId },
          () => fn(tx as unknown as DrizzleDb),
        );
      });
      return "COMMIT";
    } catch (e) {
      console.error(`  [phase ${tenantId}] error:`, (e as Error)?.message ?? e);
      return "ROLLBACK";
    }
  } finally {
    client.release();
  }
}

const run = new ProofRun(
  "AS/CYBER/READPATH-FIX/PAM — F-RP-01 read-path cross-tenant proof (accounts + requests + sessions)",
  "DEV",
);

/** Build the per-table paired-probe leg (POSITIVE owner-visible + NEGATIVE
 *  foreign-denied + ground-truth ownerExists), all as aegis_app under RLS. */
function probeLeg(id: string, label: string, p: RlsPairedProbeResult) {
  run.leg({
    id,
    scenario: `${label} — RLS paired probe as aegis_app (owner=${p.ownerTenant}, foreign=${p.foreignTenant})`,
    expectation:
      "row PHYSICALLY EXISTS (owner/BYPASSRLS) AND owner-tenant SEES it (count=1) AND foreign-tenant DENIED (count=0)",
    observed: `ownerExists=${p.ownerExists} ownerVisible=${p.ownerVisibleCount} foreignVisible=${p.foreignVisibleCount}`,
    verdict: p.pass
      ? verdict.verified(
          "aegis_app RLS-subject read: owner sees its own row, foreign tenant sees 0 of a row proven to exist",
        )
      : verdict.fail(),
    pass: p.pass,
    signals: {
      table: p.table,
      id: p.id,
      ownerExists: p.ownerExists,
      ownerVisibleCount: p.ownerVisibleCount,
      foreignVisibleCount: p.foreignVisibleCount,
    },
  });
}

async function cleanup() {
  for (const t of ["pam_sessions", "pam_requests", "pam_accounts"]) {
    await ownerQuery(`DELETE FROM "${t}" WHERE tenant_id = $1 OR tenant_id = $2`, [
      TENANT_A,
      TENANT_B,
    ]);
  }
}

async function main() {
  console.log("=".repeat(82));
  console.log(
    "VF-RP01 — PAM read-path cross-tenant isolation proof (F-RP-01, all 3 tables)",
  );
  console.log(
    "ref AS/AEGIS-CYBER/VERIFY/2026/001 | env: DEV |",
    new Date().toISOString(),
  );
  console.log(`tenant A (owner)=${TENANT_A}  tenant B (foreign)=${TENANT_B}`);
  console.log("=".repeat(82));

  await cleanup(); // idempotent pre-clean

  // ── PHASE 1 — SEED tenant-A across all 3 tables via the REAL service path ──────
  // Tx1: getAccounts(A) lazily seeds A's per-tenant pam_accounts catalog (commits).
  let seedAcct: PrivilegedAccount | undefined;
  await runRequestPhaseTx(TENANT_A, async () => {
    const accounts = await privilegedAccess.getAccounts(undefined, {
      tenantId: TENANT_A,
    });
    seedAcct = accounts.find((a) => a.status === "active") ?? accounts[0];
  });
  if (!seedAcct) {
    console.error("FATAL: no seeded PAM account for tenant A — cannot proceed");
    await cleanup();
    await endOwnerPool();
    await appPool.end();
    process.exit(1);
  }
  captured.logicalAcctId = seedAcct.id;
  captured.physAcctId = `${TENANT_A}::${seedAcct.id}`;

  // Tx2: create → review(approve) → startSession, populating pam_requests +
  // pam_sessions for tenant A (commits). Same real path the routes drive.
  await runRequestPhaseTx(TENANT_A, async () => {
    const created = await privilegedAccess.createAccessRequest(
      {
        accountId: seedAcct!.id,
        requesterId: "rp01-requester",
        requesterName: "RP01 Requester",
        justification: "F-RP-01 read-path cross-tenant proof seed",
        requestedDuration: 15,
      },
      { tenantId: TENANT_A },
    );
    if ("error" in created) throw new Error(`createAccessRequest: ${created.error}`);
    captured.reqId = created.id;

    let approved: AccessRequest = created;
    if (created.status === "pending") {
      const rev = await privilegedAccess.reviewRequest(
        created.id,
        "rp01-reviewer",
        "approved",
        "approve for read-path proof",
        { tenantId: TENANT_A },
      );
      if ("error" in rev) throw new Error(`reviewRequest: ${rev.error}`);
      approved = rev;
    }

    const sess = await privilegedAccess.startSession(approved.id, {
      tenantId: TENANT_A,
    });
    if ("error" in sess) throw new Error(`startSession: ${sess.error}`);
    captured.sessId = sess.id;
  });

  if (!captured.reqId || !captured.sessId) {
    console.error(
      `FATAL: seed incomplete (reqId=${captured.reqId || "-"} sessId=${captured.sessId || "-"})`,
    );
    await cleanup();
    await endOwnerPool();
    await appPool.end();
    process.exit(1);
  }

  console.log("");
  console.log(
    `seeded: account(phys)=${captured.physAcctId}  request=${captured.reqId}  session=${captured.sessId}`,
  );

  // ── PHASE 2 — SUBSTRATE: per-table paired RLS probe (all 3 tables) ───────────
  // Each probe: owner ground-truth existence (BYPASSRLS) + owner-tenant visible +
  // foreign-tenant denied, the latter two as aegis_app. This is the authoritative
  // cross-tenant boundary proof for every table, incl. accounts (physical-row
  // level — see Phase 3 note on per-tenant logical-id collision).
  const pAcct = await rlsPairedProbe(
    "pam_accounts",
    captured.physAcctId,
    TENANT_A,
    TENANT_B,
  );
  const pReq = await rlsPairedProbe(
    "pam_requests",
    captured.reqId,
    TENANT_A,
    TENANT_B,
  );
  const pSess = await rlsPairedProbe(
    "pam_sessions",
    captured.sessId,
    TENANT_A,
    TENANT_B,
  );
  probeLeg("RP-ACCT", "pam_accounts", pAcct);
  probeLeg("RP-REQ", "pam_requests", pReq);
  probeLeg("RP-SESS", "pam_sessions", pSess);

  // ── PHASE 3 — PROVE-THROUGH-ROUTE: real service read methods, paired A vs B ──
  // POSITIVE (tenant A, as aegis_app): the exact methods the routes call return
  // A's rows with WELL-FORMED DTOs (mapper ran, did not throw/swallow).
  let svcAAcct: PrivilegedAccount | undefined;
  let svcAReqIds: string[] = [];
  let svcASessIds: string[] = [];
  let svcAPhysLeak = false;
  await runRequestPhaseTx(TENANT_A, async () => {
    const accs = await privilegedAccess.getAccounts(undefined, {
      tenantId: TENANT_A,
    });
    svcAAcct = accs.find((a) => a.id === captured.logicalAcctId);
    svcAPhysLeak = accs.some((a) => a.id === captured.physAcctId); // DTO must use LOGICAL id
    svcAReqIds = (await privilegedAccess.getRequests()).map((r) => r.id);
    svcASessIds = (await privilegedAccess.getSessions()).map((s) => s.id);
  });

  // NEGATIVE (tenant B, as aegis_app): the same methods must return NONE of A's
  // request/session rows. (Accounts are lazily per-tenant-seeded so the logical
  // id collides by design across tenants; cross-tenant account isolation is
  // proven at the physical-row level in Phase 2 RP-ACCT. Here we additionally
  // confirm B sees ZERO rows physically belonging to A across all 3 tables.)
  let svcBReqHasA = false;
  let svcBSessHasA = false;
  await runRequestPhaseTx(TENANT_B, async () => {
    // B touches getAccounts → seeds B's OWN catalog (must not surface A's rows).
    await privilegedAccess.getAccounts(undefined, { tenantId: TENANT_B });
    svcBReqHasA = (await privilegedAccess.getRequests()).some(
      (r) => r.id === captured.reqId,
    );
    svcBSessHasA = (await privilegedAccess.getSessions()).some(
      (s) => s.id === captured.sessId,
    );
  });

  // ground truth: A's rows physically still exist while B's service read sees 0.
  const aRowsAcct = await ownerCount("pam_accounts", "tenant_id = $1", [TENANT_A]);
  const aRowsReq = await ownerCount("pam_requests", "tenant_id = $1", [TENANT_A]);
  const aRowsSess = await ownerCount("pam_sessions", "tenant_id = $1", [TENANT_A]);

  // SVC-POS — A's service read returns A's rows with well-formed DTOs.
  const acctWellFormed =
    !!svcAAcct &&
    svcAAcct.id === captured.logicalAcctId && // logical id (prefix stripped)
    typeof svcAAcct.name === "string" &&
    svcAAcct.name.length > 0 &&
    !!svcAAcct.credentials &&
    !svcAPhysLeak;
  const posPass =
    acctWellFormed &&
    svcAReqIds.includes(captured.reqId) &&
    svcASessIds.includes(captured.sessId);
  run.leg({
    id: "SVC-POS",
    scenario:
      "POSITIVE — tenant A drives getAccounts/getRequests/getSessions (real service path, aegis_app)",
    expectation:
      "A sees its own account (LOGICAL id, well-formed DTO, no physical-id leak), request, and session",
    observed:
      `acctWellFormed=${acctWellFormed} physIdLeak=${svcAPhysLeak} ` +
      `reqSeen=${svcAReqIds.includes(captured.reqId)} sessSeen=${svcASessIds.includes(captured.sessId)}`,
    verdict: posPass
      ? verdict.verified(
          "the real route-backing read methods return A's own rows with correctly-mapped DTOs",
        )
      : verdict.fail(),
    pass: posPass,
    signals: {
      acctId: svcAAcct?.id,
      acctName: svcAAcct?.name,
      physIdLeak: svcAPhysLeak,
      reqSeen: svcAReqIds.includes(captured.reqId),
      sessSeen: svcASessIds.includes(captured.sessId),
    },
  });

  // SVC-NEG — B's service read returns NONE of A's request/session rows, while A's
  // rows are proven to physically exist (so the 0 is RLS scoping, not absence) and
  // the service applies no tenant JS filter (so the 0 is RLS, not app code).
  const negPass =
    !svcBReqHasA &&
    !svcBSessHasA &&
    pReq.foreignVisibleCount === 0 &&
    pSess.foreignVisibleCount === 0 &&
    aRowsReq >= 1 &&
    aRowsSess >= 1;
  run.leg({
    id: "SVC-NEG",
    scenario:
      "NEGATIVE — tenant B drives the SAME service read methods (aegis_app, GUC=B)",
    expectation:
      "B sees 0 of A's request/session rows via the real methods; A's rows still physically exist; raw aegis_app SELECT agrees (0) → RLS-caused, not a JS filter",
    observed:
      `B.reqHasA=${svcBReqHasA} B.sessHasA=${svcBSessHasA} ` +
      `rawForeignReq=${pReq.foreignVisibleCount} rawForeignSess=${pSess.foreignVisibleCount} ` +
      `A.physRows(acct/req/sess)=${aRowsAcct}/${aRowsReq}/${aRowsSess}`,
    verdict: negPass
      ? verdict.verified(
          "cross-tenant read CLOSED: foreign tenant's real service read returns none of A's rows; service result and raw aegis_app SELECT agree → boundary is RLS",
        )
      : verdict.fail(),
    pass: negPass,
    signals: {
      svcBReqHasA,
      svcBSessHasA,
      rawForeignReq: pReq.foreignVisibleCount,
      rawForeignSess: pSess.foreignVisibleCount,
      aPhysRowsAcct: aRowsAcct,
      aPhysRowsReq: aRowsReq,
      aPhysRowsSess: aRowsSess,
    },
  });

  // ── REPORT ───────────────────────────────────────────────────────────────────
  run.report();
  await run.writeJson();

  await cleanup();
  console.log(
    `\ncleanup: deleted all pam_accounts/pam_requests/pam_sessions rows for ${TENANT_A} + ${TENANT_B} ` +
      `(audit-chain rows immutable by design — left in place).`,
  );

  const { allPass } = run.acceptance();
  await endOwnerPool();
  await appPool.end();
  if (!allPass) {
    console.error("\n❌ READ-PATH PROOF FAILED — see failing legs above.");
    process.exit(1);
  }
  console.log(
    "\n✅ F-RP-01 read-path cross-tenant CLOSED for all 3 PAM tables (DEV, as aegis_app). HOLD at publish gate.",
  );
}

main().catch(async (err) => {
  console.error("FATAL:", err);
  try {
    await cleanup();
  } catch {}
  try {
    await endOwnerPool();
  } catch {}
  try {
    await appPool.end();
  } catch {}
  process.exit(1);
});
