/**
 * VF-A1 + VF-A2 dev demonstration harness
 * Platform Verification Plan — ref AS/AEGIS-CYBER/VERIFY/2026/001, Module 2 = PAM.
 *
 * Exercises the §5 demonstration legs of VF_A1_FIX_DESIGN_BRIEF.md v3 (RATIFIED)
 * through the REAL HTTP stack against the running dev server (localhost:5000).
 * Nothing is asserted by inspection of source — every leg is an observed HTTP
 * round-trip with status + body recorded.
 *
 * Rule 18 (paired pos/neg): every denial leg is interlocked with a positive
 * leg in the SAME run, so a "pass" cannot be a hollow deny-all.
 * Rule 17 (per-env, non-transitive): this is the DEV gate only. It does NOT
 * close anything in prod.
 *
 * Surfaces:
 *   VF-A1 — authorization on the 4 unguarded PAM mutating routes + co-equal
 *           requester-identity-binding (forged requesterId/requesterName ignored).
 *   VF-A2 — CSRF skip keys on a *validated* api-key identity (req.apiKeyAuth),
 *           not on raw x-api-key header presence.
 *
 * Discovered defect (GROUP F): apiKeyAuth's pre-tenant hash lookup is RLS-blind, so
 * NO aegis_ key can be validated → req.apiKeyAuth is never set. This BLOCKS the §5.C
 * "valid key still skips CSRF" positive (documented, not faked) and is a separate
 * Rule-9 fix (getApiKeyByHash must use withBypassRls per rls-init.ts L49-50).
 *
 * Run:  npx tsx scripts/vf-a1-a2-demo.ts
 * Seeds real fixtures, runs the legs, then cleans up (deactivates seeded users —
 * audit FK integrity preserved — and deletes the non-audit children it created).
 */
import bcrypt from "bcryptjs";
import { eq, inArray } from "drizzle-orm";
import { withBypassRls } from "../server/db";
import { storage } from "../server/storage";
import { generateApiKey } from "../server/lib/api-key-auth";
import { encryptionService } from "../server/lib/encryption-service";
import { BCRYPT_ROUNDS } from "../server/lib/bcrypt-config";
import { users, apiKeys, sessions } from "../shared/schema";
import { userTenantRoles } from "../shared/schema-rbac";
import { scimConfigs, scimProvisions } from "../shared/schema-integrations";

const BASE = "http://localhost:5000";
const TENANT = "aegis-sovereign";
const SCIM_TENANT = "vfa1-scim-tenant";
const rand = Math.random().toString(36).slice(2, 8);

const PAM1 = "PAM-001"; // requiresApproval: true
const PAM7 = "PAM-007"; // requiresApproval: false (auto-approve)

// ── tiny cookie jar (Node 24 → headers.getSetCookie() available) ──────────────
class Jar {
  private store = new Map<string, string>();
  absorb(res: Response) {
    let sc: string[] = [];
    try { sc = (res.headers as any).getSetCookie?.() ?? []; } catch { /* noop */ }
    if (sc.length === 0) { const one = res.headers.get("set-cookie"); if (one) sc = [one]; }
    for (const line of sc) {
      const first = line.split(";")[0];
      const eq2 = first.indexOf("=");
      if (eq2 > 0) this.store.set(first.slice(0, eq2).trim(), first.slice(eq2 + 1).trim());
    }
  }
  header(): string {
    return Array.from(this.store.entries()).map(([k, v]) => `${k}=${v}`).join("; ");
  }
  get has() { return this.store.size > 0; }
}

type Opts = { jar?: Jar; csrf?: string; apiKey?: string; bearer?: string; body?: any };
type R = { status: number; json: any };

async function http(method: string, path: string, opts: Opts = {}): Promise<R> {
  const headers: Record<string, string> = {};
  if (opts.body !== undefined) headers["content-type"] = "application/json";
  if (opts.jar?.has) headers["cookie"] = opts.jar.header();
  if (opts.csrf) headers["x-csrf-token"] = opts.csrf;
  if (opts.apiKey) headers["x-api-key"] = opts.apiKey;
  if (opts.bearer) headers["authorization"] = `Bearer ${opts.bearer}`;
  const res = await fetch(`${BASE}${path}`, {
    method,
    headers,
    body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
    redirect: "manual",
  });
  if (opts.jar) opts.jar.absorb(res);
  const text = await res.text();
  let json: any = null;
  try { json = text ? JSON.parse(text) : null; } catch { json = { _raw: text.slice(0, 160) }; }
  return { status: res.status, json };
}

// ── results ledger ────────────────────────────────────────────────────────────
const ledger: { group: string; leg: string; expected: string; got: number; pass: boolean; note: string }[] = [];
function rec(group: string, leg: string, expected: string, got: number, pass: boolean, note = "") {
  ledger.push({ group, leg, expected, got, pass, note });
  console.log(`  [${pass ? "PASS" : "FAIL"}] ${leg.padEnd(46)} expect ${expected.padEnd(20)} got ${got}${note ? "  | " + note : ""}`);
}
const isCsrf = (r: R) => r.status === 403 && r.json?.error === "CSRF_TOKEN_REQUIRED";
const codeOf = (r: R): string | undefined =>
  r.json?.error?.code ?? r.json?.code ?? (typeof r.json?.error === "string" ? r.json.error : undefined);

// Findings ledger — discovered defects surfaced to the advisor; these are NOT §5
// demonstration pass/fail legs. A finding "confirmed" means the observed behaviour
// matched the predicted defect (still a problem, not a green check).
const findings: { id: string; observed: string; rootCause: string }[] = [];
function recFinding(id: string, observed: string, rootCause: string) {
  findings.push({ id, observed, rootCause });
  console.log(`  [FINDING ${id}] ${observed}\n      root-cause: ${rootCause}`);
}

async function login(jar: Jar, username: string, password: string): Promise<string> {
  const r = await http("POST", "/api/auth/login", { jar, body: { username, password } });
  if (r.status !== 200 || !r.json?.success) {
    throw new Error(`login failed for ${username}: ${r.status} ${JSON.stringify(r.json)}`);
  }
  const t = await http("GET", "/api/csrf-token", { jar });
  const csrf = t.json?.csrfToken || t.json?.token;
  if (!csrf) throw new Error(`csrf-token fetch failed for ${username}: ${t.status} ${JSON.stringify(t.json)}`);
  return csrf;
}

async function main() {
  const ids: string[] = [];
  let apiKeyId = "";
  try {
    // ── FIXTURES ──────────────────────────────────────────────────────────────
    const adminU = `vfa1_admin_${rand}`, adminPw = `Adm!${rand}aA9x`;
    const userU = `vfa1_user_${rand}`, userPw = `Usr!${rand}bB9x`;
    const victimU = `vfa1_victim_${rand}`;
    const adminFull = "VFA1 Admin Real", userFull = "VFA1 NonAdmin Real", victimFull = "VFA1 Victim Forged";

    const adminHash = await bcrypt.hash(adminPw, BCRYPT_ROUNDS);
    const userHash = await bcrypt.hash(userPw, BCRYPT_ROUNDS);
    const dummyHash = await bcrypt.hash(`x${rand}`, BCRYPT_ROUNDS);

    const admin = await storage.createUser({ username: adminU, password: adminHash, fullName: adminFull, role: "admin", isActive: true } as any);
    const nonadmin = await storage.createUser({ username: userU, password: userHash, fullName: userFull, role: "auditor", isActive: true } as any);
    const victim = await storage.createUser({ username: victimU, password: dummyHash, fullName: victimFull, role: "auditor", isActive: true } as any);
    ids.push(admin.id, nonadmin.id, victim.id);

    await withBypassRls(async (bdb) => {
      await bdb.insert(userTenantRoles).values([
        { userId: admin.id, tenantId: TENANT, role: "admin", isActive: true, isPrimary: true },
        { userId: nonadmin.id, tenantId: TENANT, role: "auditor", isActive: true, isPrimary: true },
      ]);
    });

    const key = generateApiKey();
    await withBypassRls(async (bdb) => {
      const [row] = await bdb.insert(apiKeys).values({
        name: `vfa1-${rand}`, keyHash: key.keyHash, keyPrefix: key.keyPrefix, tenantId: TENANT, isActive: true,
      }).returning();
      apiKeyId = row.id;
    });

    const scimToken = `vfa1scim_${rand}_${Math.random().toString(36).slice(2, 12)}`;
    await withBypassRls(async (bdb) => {
      await bdb.insert(scimConfigs).values({
        tenantId: SCIM_TENANT, providerName: "okta",
        encryptedBearerToken: encryptionService.encrypt(scimToken), isActive: true,
      });
    });

    console.log(`\nFixtures: admin=${admin.id} nonadmin=${nonadmin.id} victim=${victim.id} apiKeyId=${apiKeyId}`);
    console.log(`Valid aegis_ key prefix=${key.keyPrefix}  SCIM tenant=${SCIM_TENANT}\n`);

    // ── LOGIN ───────────────────────────────────────────────────────────────────
    const A = new Jar(); const aCsrf = await login(A, adminU, adminPw);
    const U = new Jar(); const uCsrf = await login(U, userU, userPw);
    console.log("Logged in admin + non-admin via real login flow.\n");

    const createBody = (acct: string, extra: any = {}) => ({ accountId: acct, justification: "VF demo justification", requestedDuration: 30, ...extra });
    const ROUTES4 = [
      { m: "POST", p: "/api/pam/requests", b: createBody(PAM1) },
      { m: "POST", p: "/api/pam/requests/REQ-x/start-session", b: {} },
      { m: "POST", p: "/api/pam/sessions/SESS-x/command", b: { command: "whoami" } },
      { m: "POST", p: "/api/pam/sessions/SESS-x/end", b: { reason: "demo" } },
    ];

    // ===== GROUP A — VF-A1 GUARDS =====
    console.log("GROUP A — VF-A1 authorization guards on the 4 mutating PAM routes");
    // A1: unauth + junk x-api-key (non-aegis_ → apiKeyAuth falls through) + no csrf → CSRF 403
    for (const rt of ROUTES4) {
      const r = await http(rt.m, rt.p, { apiKey: "junk-not-aegis", body: rt.b });
      rec("A", `A1 unauth+junkKey ${rt.p.replace("/api/pam", "")}`, "403 CSRF", r.status, isCsrf(r), r.json?.error || "");
    }
    // A2: anonymous session (NO login) + valid CSRF token → request passes CSRF and
    // reaches the auth guard → 401 AEGIS_ERR_004. This is the §5.A "401 (requireAuth),
    // not the handler 400" demonstration, isolating the NEW guard's contribution by
    // getting PAST csrf without an authenticated session.
    //   Why not a valid api-key here (as originally sketched): post-VF-A2 a junk key no
    //   longer skips CSRF, and a *valid* key cannot be validated at all (GROUP F finding:
    //   apiKeyAuth's pre-tenant hash lookup is RLS-blind). The anonymous+CSRF path depends
    //   on nothing that is broken, so it proves the guard cleanly.
    const Anon = new Jar();
    const anonTokR = await http("GET", "/api/csrf-token", { jar: Anon });
    const anonCsrf = anonTokR.json?.csrfToken || anonTokR.json?.token;
    if (!anonCsrf) throw new Error(`anon csrf fetch failed: ${anonTokR.status} ${JSON.stringify(anonTokR.json)}`);
    for (const rt of ROUTES4) {
      const r = await http(rt.m, rt.p, { jar: Anon, csrf: anonCsrf, body: rt.b });
      const code = codeOf(r);
      const reachedGuard = r.status === 401 && code === "AEGIS_ERR_004";
      rec("A", `A2 anonSession+csrf ${rt.p.replace("/api/pam", "")}`, "401 ERR_004", r.status, reachedGuard, `code=${code} — past csrf → auth guard, not handler 400`);
    }
    // A3: non-admin session + csrf → create allowed (requireAuth floor), role routes denied 403
    {
      const c = await http("POST", "/api/pam/requests", { jar: U, csrf: uCsrf, body: createBody(PAM1) });
      rec("A", "A3 nonAdmin create /requests", "200", c.status, c.status === 200, "requireAuth floor");
      const role = [
        { p: "/api/pam/requests/REQ-x/start-session", b: {} },
        { p: "/api/pam/sessions/SESS-x/command", b: { command: "whoami" } },
        { p: "/api/pam/sessions/SESS-x/end", b: { reason: "demo" } },
      ];
      for (const rt of role) {
        const r = await http("POST", rt.p, { jar: U, csrf: uCsrf, body: rt.b });
        rec("A", `A3 nonAdmin ${rt.p.replace("/api/pam", "")}`, "403 role", r.status, r.status === 403 && !isCsrf(r), r.json?.error?.code || r.json?.code || "role-deny");
      }
    }

    // ===== GROUP B — VF-A1 IDENTITY BINDING (forgery) =====
    console.log("\nGROUP B — VF-A1 requester-identity-binding (forged fields ignored)");
    {
      // B1: admin forges requesterId=victim, requesterName=victim → must bind to admin
      const r = await http("POST", "/api/pam/requests", { jar: A, csrf: aCsrf, body: createBody(PAM1, { requesterId: victim.id, requesterName: victimFull }) });
      const bound = r.status === 200 && r.json?.requesterId === admin.id && r.json?.requesterName === adminFull;
      const leaked = r.json?.requesterId === victim.id || r.json?.requesterName === victimFull;
      rec("B", "B1 admin forge→bound to admin", "200 bound", r.status, bound && !leaked, `req=${r.json?.requesterId === admin.id ? "admin" : r.json?.requesterId} name="${r.json?.requesterName}"`);
      // B2: admin clean → bound to admin (positive pair)
      const r2 = await http("POST", "/api/pam/requests", { jar: A, csrf: aCsrf, body: createBody(PAM1) });
      rec("B", "B2 admin clean→bound to admin", "200 bound", r2.status, r2.status === 200 && r2.json?.requesterId === admin.id && r2.json?.requesterName === adminFull, `name="${r2.json?.requesterName}"`);
      // B3: non-admin forges requesterId=admin → must bind to non-admin
      const r3 = await http("POST", "/api/pam/requests", { jar: U, csrf: uCsrf, body: createBody(PAM1, { requesterId: admin.id, requesterName: adminFull }) });
      const bound3 = r3.status === 200 && r3.json?.requesterId === nonadmin.id && r3.json?.requesterName === userFull;
      rec("B", "B3 nonAdmin forge→bound to nonAdmin", "200 bound", r3.status, bound3, `req=${r3.json?.requesterId === nonadmin.id ? "nonadmin" : r3.json?.requesterId} name="${r3.json?.requesterName}"`);
    }

    // ===== GROUP C — AUTO-APPROVE ESCALATION (same-object pos/neg) =====
    console.log("\nGROUP C — auto-approve (PAM-007) does not grant a session to non-admin");
    let reqC = "", sessC = "";
    {
      const c1 = await http("POST", "/api/pam/requests", { jar: U, csrf: uCsrf, body: createBody(PAM7) });
      reqC = c1.json?.id;
      rec("C", "C1 nonAdmin create PAM-007", "200 approved", c1.status, c1.status === 200 && c1.json?.status === "approved", `status=${c1.json?.status} id=${reqC}`);
      const c2 = await http("POST", `/api/pam/requests/${reqC}/start-session`, { jar: U, csrf: uCsrf, body: {} });
      rec("C", "C2 nonAdmin start-session(reqC)", "403 role", c2.status, c2.status === 403 && !isCsrf(c2), "escalation blocked");
      const c3 = await http("POST", `/api/pam/requests/${reqC}/start-session`, { jar: A, csrf: aCsrf, body: {} });
      sessC = c3.json?.id;
      rec("C", "C3 admin start-session(reqC) [pair]", "200", c3.status, c3.status === 200 && !!sessC, `sessionId=${sessC}`);
      const c4 = await http("POST", `/api/pam/sessions/${sessC}/command`, { jar: A, csrf: aCsrf, body: { command: "whoami" } });
      rec("C", "C4 admin command(sessC)", "200", c4.status, c4.status === 200, "role route positive");
      const c5 = await http("POST", `/api/pam/sessions/${sessC}/end`, { jar: A, csrf: aCsrf, body: { reason: "demo done" } });
      rec("C", "C5 admin end(sessC)", "200", c5.status, c5.status === 200, "role route positive");
    }

    // ===== GROUP D — VF-A2 CSRF skip keys on validated key, not header presence =====
    // Negatives (the closed hole) + the browser positive. The §5.C "valid aegis_ key
    // still skips CSRF and succeeds" positive is BLOCKED by a discovered defect and is
    // documented in GROUP F (apiKeyAuth cannot validate any key → req.apiKeyAuth never set).
    console.log("\nGROUP D — VF-A2 CSRF skip keys on validated key, not header presence");
    {
      const d1 = await http("POST", "/api/pam/requests", { jar: A, body: createBody(PAM1) }); // session, no key, no csrf
      rec("D", "D1 adminSession noKey noCsrf", "403 CSRF", d1.status, isCsrf(d1), "control");
      const d2 = await http("POST", "/api/pam/requests", { jar: A, apiKey: "junk-not-aegis", body: createBody(PAM1) }); // junk key
      rec("D", "D2 adminSession junkKey noCsrf", "403 CSRF", d2.status, isCsrf(d2), "junk key does NOT skip (the closed hole)");
      const d4 = await http("POST", "/api/pam/requests", { jar: A, csrf: aCsrf, body: createBody(PAM1) }); // normal browser
      rec("D", "D4 adminSession validCsrf noKey", "200", d4.status, d4.status === 200, "browser path — no regression");
    }

    // ===== GROUP E — VF-A2 SCIM disambiguation =====
    console.log("\nGROUP E — SCIM (apiKeyAuth is /api-only; CSRF runs before validateBearerToken)");
    {
      const scimUser = { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], userName: `vfa1.scim.${rand}@x.io`, displayName: "VFA1 SCIM", emails: [{ primary: true, value: `vfa1.scim.${rand}@x.io` }] };
      const e1 = await http("POST", "/scim/v2/Users", { bearer: scimToken, body: scimUser });
      rec("E", "E1 POST /scim Bearer noCsrf", "403 CSRF", e1.status, isCsrf(e1), `body proves CSRF not auth: ${e1.json?.error}`);
      const e2 = await http("POST", "/scim/v2/Users", { bearer: scimToken, apiKey: "junk-not-aegis", body: scimUser });
      rec("E", "E2 POST /scim Bearer+junkKey noCsrf", "403 CSRF", e2.status, isCsrf(e2), "x-api-key does NOT skip on /scim");
      const e3 = await http("GET", "/scim/v2/Users", { bearer: scimToken });
      rec("E", "E3 GET /scim Bearer (supplementary)", "observe", e3.status, true, `status=${e3.status} (scim_configs is RLS; GUC=default → may 401 — no claim hinges on this)`);
    }

    // ===== GROUP F — DISCOVERED DEFECT: §5.C positive ("valid aegis_ key still skips
    // CSRF and succeeds") is NOT demonstrable — apiKeyAuth cannot validate ANY key. =====
    console.log("\nGROUP F — discovered defect blocking §5.C positive (valid-key-skips-CSRF)");
    {
      // F1 (HTTP): a freshly-minted VALID aegis_ key, no session, no csrf. If the skip
      // worked, req.apiKeyAuth would be set, CSRF skipped, and the request would reach
      // requireAuth → 401 AEGIS_ERR_004. Instead apiKeyAuth rejects the key first.
      const f1 = await http("POST", "/api/pam/requests", { jar: new Jar(), apiKey: key.key, body: createBody(PAM1) });
      const f1Code = codeOf(f1);
      const confirmed = f1.status === 401 && f1Code !== "AEGIS_ERR_004"; // apiKeyAuth's {error:"Unauthorized"}, NOT requireAuth
      recFinding("VF-A2-F1",
        `valid aegis_ key → POST /api/pam/requests (no session, no csrf) → ${f1.status} code=${f1Code} ${confirmed ? "[defect confirmed: rejected by apiKeyAuth BEFORE csrf; req.apiKeyAuth never set]" : "[UNEXPECTED — re-examine]"}`,
        "apiKeyAuth runs before tenantMiddleware (GUC null); getApiKeyByHash uses RLS-enforced db; api_keys is RLS-scoped → row hidden at lookup time");
      // F2 (root cause): the SAME key row IS present (visible via bypass) but the RLS-path
      // lookup apiKeyAuth relies on returns nothing — the exact blindness apiKeyAuth hits.
      const viaBypass = await withBypassRls(async (bdb) => {
        const rows = await bdb.select().from(apiKeys).where(eq(apiKeys.keyHash, key.keyHash));
        return rows.length;
      });
      const viaRls = await storage.getApiKeyByHash(key.keyHash);
      recFinding("VF-A2-F2",
        `api_keys row by keyHash: via withBypassRls → ${viaBypass} row(s) [EXISTS]; via storage.getApiKeyByHash (RLS path) → ${viaRls ? "1 row" : "undefined"} [${viaRls ? "VISIBLE" : "BLIND"}]`,
        "rls-init.ts L49-50 mandates apiKeyAuth's pre-tenant hash lookup MUST use withBypassRls(); storage.getApiKeyByHash (storage.ts L608) uses the plain RLS db — documented requirement unmet → api-key auth non-functional for all keys");
    }

    // ── SUMMARY ─────────────────────────────────────────────────────────────────
    const fails = ledger.filter((l) => !l.pass);
    console.log(`\n${"=".repeat(78)}`);
    console.log(`DEMONSTRATION (§5 legs): ${ledger.length - fails.length}/${ledger.length} PASS` + (fails.length ? `  — ${fails.length} FAIL` : "  — ALL PASS"));
    if (fails.length) {
      console.log("FAILURES:");
      for (const f of fails) console.log(`  - [${f.group}] ${f.leg} | expected ${f.expected} | got ${f.got} | ${f.note}`);
    }
    console.log(`\nFINDINGS (surfaced — NOT §5 pass/fail; a confirmed finding is a problem, not a green check): ${findings.length}`);
    for (const f of findings) console.log(`  - [${f.id}] ${f.observed}\n      root-cause: ${f.rootCause}`);
    console.log("=".repeat(78));
  } finally {
    // ── CLEANUP (non-destructive to audit surfaces) ─────────────────────────────
    console.log("\nCleanup:");
    const tryDel = async (label: string, fn: () => Promise<any>) => {
      try { await fn(); console.log(`  cleaned: ${label}`); }
      catch (e: any) { console.log(`  SKIP ${label}: ${e?.message || e}`); }
    };
    await withBypassRls(async (bdb) => {
      await tryDel("scim_provisions", () => bdb.delete(scimProvisions).where(eq(scimProvisions.tenantId, SCIM_TENANT)));
      await tryDel("scim_configs", () => bdb.delete(scimConfigs).where(eq(scimConfigs.tenantId, SCIM_TENANT)));
      if (apiKeyId) await tryDel("api_keys", () => bdb.delete(apiKeys).where(eq(apiKeys.id, apiKeyId)));
      if (ids.length) {
        await tryDel("user_tenant_roles", () => bdb.delete(userTenantRoles).where(inArray(userTenantRoles.userId, ids)));
        await tryDel("sessions", () => bdb.delete(sessions).where(inArray(sessions.userId, ids)));
        // Deactivate (not delete) seeded users: audit_logs.userId FK preserves the
        // test login audit trail; deactivation makes the accounts non-loginable.
        await tryDel("users→isActive=false", () => bdb.update(users).set({ isActive: false }).where(inArray(users.id, ids)));
      }
    });
    console.log("Done.");
  }
}

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