/**
 * AS/PLATFORM/2026/003 T003 — Machine-API-key ingestion REAL-STACK proof.
 *
 * Drives the ACTUAL route handlers + middleware chain over HTTP (no hand-passed
 * session values), per the architect's CHANGES-REQUIRED follow-up. Every leg is
 * Rule-18 paired (a denial assertion next to its positive-enforcement twin).
 *
 * Three parts:
 *
 *  PART A (in-process) — mint commit-before-respond fix, proven via a SIMULATED
 *    request-tx (own pooled client, BEGIN, GUC, bound into tenantContext, left
 *    UNCOMMITTED — modelling tenantMiddleware's per-request tx):
 *      • A1 GAP : issueMachineKey UNWRAPPED inside the sim-tx → the returned raw
 *                 key's row is INVISIBLE to an independent connection and is LOST
 *                 on request-tx rollback (respond-before-commit hazard).
 *      • A2 FIX : issueMachineKey WRAPPED in withTenantDbPhase → the row is
 *                 VISIBLE to an independent connection before the sim-tx commits
 *                 and SURVIVES the sim-tx rollback (commits on its own client).
 *
 *  PART B (real server on :5099 with a PROCESS-LOCAL test salt — never a Replit
 *    secret, so the MACHINE_KEY_SALT operator HOLD is respected) — full HTTP
 *    acceptance against the live route:
 *      • B1  CSRF-allowlist reachable : state-changing POST, NO x-csrf-token,
 *            valid Bearer → 201 (proves the CSRF allowlist entry works).
 *      • B1c CSRF still enforced      : POST a NON-allowlisted state-changing
 *            route with no token → 403 CSRF (allowlist is scoped, not a blanket).
 *      • B2  positive opaque + DB read-back : case row exists, tenant == KEY
 *            tenant (not payload), subject opaque, state OPEN.
 *      • B3  scoped-negative          : non-ingest-scoped key → 403 insufficient-scope.
 *      • B4  opaque-subject fence     : raw-PII ('@') and 'kyc:' subjects → 400.
 *      • B5  tenant strictness        : payload tenant_id key → 400 (.strict()).
 *      • B6  bearer hygiene           : missing → 401; junk → 401 unknown-token.
 *      • B7  rate-limit isolation     : keyA floods → 429 after its per-key limit,
 *            keyB (same egress IP) UNAFFECTED (per-key isolation + generic-bucket
 *            exemption working together).
 *      • B8  revocation read-back     : works pre-revoke, 401 'revoked' post-revoke,
 *            revokedAt persisted.
 *
 *  PART C (live :5000, genuinely NO salt) — dormant fail-closed AND CSRF
 *    reachability on the unprovisioned stack:
 *      • C1 valid-shaped Bearer → 503 machine-key-auth-not-configured.
 *      • C2 no Bearer           → 401 machine-bearer-token-required.
 *    Both ≠ 403 CSRF (pre-fix this route answered 403 CSRF_TOKEN_REQUIRED).
 *
 * Cleanup (Rule 11 — only self-created scratch state): every case + case-event +
 * machine key minted here is deleted in a finally block via the BYPASSRLS owner
 * pool. The immutable audit_chain_entries are left in place by design.
 *
 * Exit 0 iff ALL legs PASS.
 */
import { spawn, ChildProcess } from "child_process";
import crypto from "crypto";
import { and, eq, inArray } from "drizzle-orm";
import { pool, auditDb, tenantContext, makeClientRunner } from "../server/db";
import { withTenantDbPhase } from "../server/lib/tenant-context";
import { issueMachineKey, revokeMachineKey } from "../server/lib/fincrime/machine-keys";
import { fincrimeIngestKeys, fincrimeCases, fincrimeCaseEvents } from "@shared/schema";

// Process-local test salt (>= 16 chars). NEVER a Replit secret → the operator
// MACHINE_KEY_SALT provisioning HOLD is respected; this only lives in this proc
// and the child it spawns.
const TEST_SALT = "rsproof-machine-key-salt-2026-LOCAL-ONLY";
const TENANT = "aegis-sovereign"; // a real seeded tenant (passes the GUC format guard)
const PORT = 5099;
const LIVE_PORT = 5000;

const sha256 = (s: string) => crypto.createHash("sha256").update(s).digest("hex");
const rid = () => crypto.randomBytes(6).toString("hex");

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

// Collected scratch state for cleanup.
const caseIds = new Set<string>();
const keyIds = new Set<string>();

// ── HTTP helper ──────────────────────────────────────────────────────────────
async function http(
  port: number,
  path: string,
  opts: { method?: string; bearer?: string; csrf?: string; body?: any } = {},
): Promise<{ status: number; json: any }> {
  const headers: Record<string, string> = {};
  if (opts.body !== undefined) headers["content-type"] = "application/json";
  if (opts.bearer) headers["authorization"] = `Bearer ${opts.bearer}`;
  if (opts.csrf) headers["x-csrf-token"] = opts.csrf;
  const res = await fetch(`http://127.0.0.1:${port}${path}`, {
    method: opts.method ?? "POST",
    headers,
    body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
  });
  let json: any = null;
  try {
    json = await res.json();
  } catch {
    json = null;
  }
  return { status: res.status, json };
}

function validSignal(tag: string, sourceType = "KYT_ALERT") {
  return { sourceType, subjectRef: `opaque:rsproof.${tag}`, signal: { tag } };
}

function collectCase(json: any) {
  if (json && typeof json.caseId === "string") caseIds.add(json.caseId);
}

// Independent-connection visibility check (owner pool, BYPASSRLS).
async function keyRowByHash(keyHash: string) {
  const rows = await auditDb
    .select()
    .from(fincrimeIngestKeys)
    .where(eq(fincrimeIngestKeys.keyHash, keyHash))
    .limit(1);
  return rows[0];
}

// Open a pooled client, BEGIN, set the tenant GUC, bind it into tenantContext,
// run fn, and LEAVE THE TX OPEN (modelling tenantMiddleware's per-request tx).
async function simRequestTx<T>(tenantId: string, fn: () => Promise<T>) {
  const client = await pool.connect();
  await client.query("BEGIN");
  await client.query("SELECT set_config('app.current_tenant_id', $1, true)", [tenantId]);
  const runner = makeClientRunner(client);
  const result = await tenantContext.run({ runner, tenantId }, fn);
  return { client, result };
}

// ── PART A: mint commit-before-respond ───────────────────────────────────────
async function partA() {
  // A1 GAP — unwrapped issue inside an uncommitted sim-tx.
  {
    const { client, result } = await simRequestTx(TENANT, () =>
      issueMachineKey(
        { label: `rsproof-A1-${rid()}`, scopes: ["ingest:fincrime"], tenantId: TENANT, ttlHours: 1, issuedBy: "rsproof" },
        { salt: TEST_SALT },
      ),
    );
    const h = sha256(TEST_SALT + (result as any).rawKey);
    const visibleBefore = !!(await keyRowByHash(h)); // expect FALSE (uncommitted)
    await client.query("ROLLBACK");
    client.release();
    const visibleAfter = !!(await keyRowByHash(h)); // expect FALSE (lost)
    record(
      "A1 mint GAP (unwrapped → respond-before-commit hazard)",
      visibleBefore === false && visibleAfter === false,
      `independentVisibleBeforeCommit=${visibleBefore} (want false), survivesRollback=${visibleAfter} (want false) — returned key never persisted`,
    );
  }
  // A2 FIX — wrapped issue inside an uncommitted sim-tx.
  {
    const { client, result } = await simRequestTx(TENANT, () =>
      withTenantDbPhase({ tenantId: TENANT }, () =>
        issueMachineKey(
          { label: `rsproof-A2-${rid()}`, scopes: ["ingest:fincrime"], tenantId: TENANT, ttlHours: 1, issuedBy: "rsproof" },
          { salt: TEST_SALT },
        ),
      ),
    );
    keyIds.add((result as any).id);
    const h = sha256(TEST_SALT + (result as any).rawKey);
    const visibleBefore = !!(await keyRowByHash(h)); // expect TRUE (committed on own client)
    await client.query("ROLLBACK");
    client.release();
    const visibleAfter = !!(await keyRowByHash(h)); // expect TRUE (survives)
    record(
      "A2 mint FIX (withTenantDbPhase → commit-before-respond)",
      visibleBefore === true && visibleAfter === true,
      `independentVisibleBeforeCommit=${visibleBefore} (want true), survivesRollback=${visibleAfter} (want true) — durable before response`,
    );
  }
}

// ── server spawn ─────────────────────────────────────────────────────────────
function waitForServer(child: ChildProcess): Promise<void> {
  return new Promise((resolve, reject) => {
    let done = false;
    let tail = "";
    const onData = (buf: Buffer) => {
      const s = buf.toString();
      tail = (tail + s).slice(-4000);
      if (!done && /serving on port 5099/.test(tail)) {
        done = true;
        resolve();
      }
    };
    child.stdout?.on("data", onData);
    child.stderr?.on("data", onData);
    const timer = setTimeout(() => {
      if (!done) {
        done = true;
        reject(new Error(`child server did not report ready in time. Last output:\n${tail}`));
      }
    }, 60000);
    timer.unref?.();
  });
}

// ── PART B: HTTP acceptance against the spawned :5099 server ──────────────────
async function partB() {
  // Mint dedicated keys from the PARENT (same dev DB the child reads; same salt).
  const mint = async (label: string, scopes: string[]) => {
    const k = await issueMachineKey({ label: `rsproof-${label}-${rid()}`, scopes, tenantId: TENANT, ttlHours: 1, issuedBy: "rsproof" }, { salt: TEST_SALT });
    keyIds.add((k as any).id);
    return k as any;
  };
  const keyIngest = await mint("ingest", ["ingest:fincrime"]);
  const keyRead = await mint("read", ["read:fincrime"]);
  const keyA7 = await mint("rlA", ["ingest:fincrime"]);
  const keyB7 = await mint("rlB", ["ingest:fincrime"]);
  const keyRevoke = await mint("revoke", ["ingest:fincrime"]);

  // B1 — CSRF-allowlist reachable: valid Bearer, NO csrf token → 201.
  const b1 = await http(PORT, "/api/fincrime/ingest/machine", { bearer: keyIngest.rawKey, body: validSignal(`b1-${rid()}`) });
  collectCase(b1.json);
  const b1CaseId = b1.json?.caseId as string | undefined;
  record(
    "B1 CSRF allowlist reachable (no x-csrf-token, valid key → 201)",
    b1.status === 201 && !!b1CaseId,
    `status=${b1.status} caseId=${b1CaseId ?? "—"}`,
  );

  // B1c — CSRF still enforced on a NON-allowlisted state-changing route.
  const b1c = await http(PORT, "/api/fincrime/ingest", { body: validSignal(`b1c-${rid()}`) });
  record(
    "B1c CSRF still enforced (non-allowlisted route, no token → 403 CSRF)",
    b1c.status === 403 && b1c.json?.error === "CSRF_TOKEN_REQUIRED",
    `status=${b1c.status} error=${b1c.json?.error}`,
  );

  // B2 — DB read-back of the B1 case: tenant from KEY, opaque subject, OPEN.
  if (b1CaseId) {
    const rows = await auditDb.select().from(fincrimeCases).where(eq(fincrimeCases.id, b1CaseId)).limit(1);
    const c = rows[0] as any;
    const ok = !!c && c.tenantId === TENANT && typeof c.subjectRef === "string" && c.subjectRef.startsWith("opaque:") && c.state === "OPEN";
    record(
      "B2 positive ingest + DB read-back (tenant from key, opaque subject, OPEN)",
      ok,
      c ? `tenantId=${c.tenantId} (want ${TENANT}) subjectRef=${c.subjectRef} state=${c.state}` : "case row not found",
    );
  } else {
    record("B2 positive ingest + DB read-back", false, "no caseId from B1");
  }

  // B3 — scoped-negative (read-only key on the ingest route → 403 insufficient-scope).
  const b3 = await http(PORT, "/api/fincrime/ingest/machine", { bearer: keyRead.rawKey, body: validSignal(`b3-${rid()}`) });
  record(
    "B3 scoped-negative (non-ingest scope → 403 insufficient-scope)",
    b3.status === 403 && b3.json?.reason === "insufficient-scope",
    `status=${b3.status} reason=${b3.json?.reason}`,
  );

  // B4 — opaque-subject fence: raw-PII ('@') and 'kyc:' subjects → 400.
  const b4a = await http(PORT, "/api/fincrime/ingest/machine", {
    bearer: keyIngest.rawKey,
    body: { sourceType: "KYT_ALERT", subjectRef: "attacker@example.com", signal: {} },
  });
  const b4b = await http(PORT, "/api/fincrime/ingest/machine", {
    bearer: keyIngest.rawKey,
    body: { sourceType: "KYT_ALERT", subjectRef: "kyc:6df338d1-customer", signal: {} },
  });
  record(
    "B4 opaque-subject fence (raw-PII '@' and 'kyc:' subjects → 400)",
    b4a.status === 400 && b4b.status === 400,
    `rawPII=${b4a.status} kycPrefix=${b4b.status} (want 400/400)`,
  );

  // B5 — tenant strictness: payload tenant_id key → 400 (.strict()).
  const b5 = await http(PORT, "/api/fincrime/ingest/machine", {
    bearer: keyIngest.rawKey,
    body: { sourceType: "KYT_ALERT", subjectRef: `opaque:rsproof.b5-${rid()}`, tenant_id: "attacker-tenant", signal: {} },
  });
  record(
    "B5 tenant strictness (payload tenant_id → 400, tenant bound to key only)",
    b5.status === 400,
    `status=${b5.status} (want 400)`,
  );

  // B6 — bearer hygiene: missing → 401 bearer-required; junk → 401 unknown-token.
  const b6a = await http(PORT, "/api/fincrime/ingest/machine", { body: validSignal(`b6a-${rid()}`) });
  const b6b = await http(PORT, "/api/fincrime/ingest/machine", { bearer: "mk_deadbeefdeadbeef", body: validSignal(`b6b-${rid()}`) });
  record(
    "B6 bearer hygiene (missing → 401 bearer-required; junk → 401 unknown-token)",
    b6a.status === 401 && b6a.json?.error === "machine-bearer-token-required" && b6b.status === 401 && b6b.json?.reason === "unknown-token",
    `missing=${b6a.status}/${b6a.json?.error} junk=${b6b.status}/${b6b.json?.reason}`,
  );

  // B7 — per-key rate-limit isolation. keyA floods (62 with the SAME signal so it
  // dedups to one case); keyB (same egress IP) stays unaffected.
  const a7sig = validSignal(`b7a-${rid()}`);
  const a7statuses: number[] = [];
  for (let i = 0; i < 62; i++) {
    const r = await http(PORT, "/api/fincrime/ingest/machine", { bearer: keyA7.rawKey, body: a7sig });
    a7statuses.push(r.status);
    collectCase(r.json);
  }
  const b7b = await http(PORT, "/api/fincrime/ingest/machine", { bearer: keyB7.rawKey, body: validSignal(`b7b-${rid()}`) });
  collectCase(b7b.json);
  const a7Limited = a7statuses.includes(429);
  const a7First = a7statuses[0];
  const bUnaffected = b7b.status === 200 || b7b.status === 201;
  record(
    "B7 rate-limit isolation (keyA floods → 429; keyB same IP unaffected)",
    a7Limited && bUnaffected && (a7First === 201 || a7First === 200),
    `keyA first=${a7First} 429count=${a7statuses.filter((s) => s === 429).length} keyB=${b7b.status} (want keyA has 429, keyB 200/201)`,
  );

  // B8 — revocation read-back. Works pre-revoke; 401 'revoked' post-revoke.
  const b8pre = await http(PORT, "/api/fincrime/ingest/machine", { bearer: keyRevoke.rawKey, body: validSignal(`b8-${rid()}`) });
  collectCase(b8pre.json);
  await revokeMachineKey(keyRevoke.id, "rsproof");
  const b8post = await http(PORT, "/api/fincrime/ingest/machine", { bearer: keyRevoke.rawKey, body: validSignal(`b8b-${rid()}`) });
  const revRows = await auditDb.select().from(fincrimeIngestKeys).where(eq(fincrimeIngestKeys.id, keyRevoke.id)).limit(1);
  const revokedAtSet = !!(revRows[0] as any)?.revokedAt;
  record(
    "B8 revocation read-back (works pre-revoke; 401 'revoked' post; revokedAt set)",
    (b8pre.status === 200 || b8pre.status === 201) && b8post.status === 401 && b8post.json?.reason === "revoked" && revokedAtSet,
    `pre=${b8pre.status} post=${b8post.status}/${b8post.json?.reason} revokedAtSet=${revokedAtSet}`,
  );

  return { keyIngest };
}

// ── PART C: dormant + CSRF reachability on the live (no-salt) :5000 ───────────
async function partC(keyRawForShape: string) {
  const c1 = await http(LIVE_PORT, "/api/fincrime/ingest/machine", { bearer: keyRawForShape, body: validSignal(`c1-${rid()}`) });
  const c2 = await http(LIVE_PORT, "/api/fincrime/ingest/machine", { body: validSignal(`c2-${rid()}`) });
  record(
    "C1 live :5000 dormant fail-closed + CSRF-reachable (Bearer → 503 not-configured)",
    c1.status === 503 && c1.json?.error === "machine-key-auth-not-configured",
    `status=${c1.status} error=${c1.json?.error} (503 ⇒ passed CSRF, hit requireMachineKey)`,
  );
  record(
    "C2 live :5000 no-Bearer past CSRF (→ 401 bearer-required, not 403 CSRF)",
    c2.status === 401 && c2.json?.error === "machine-bearer-token-required",
    `status=${c2.status} error=${c2.json?.error}`,
  );
}

async function cleanup() {
  try {
    const cids = [...caseIds];
    if (cids.length) {
      await auditDb.delete(fincrimeCaseEvents).where(inArray(fincrimeCaseEvents.caseId, cids));
      await auditDb.delete(fincrimeCases).where(inArray(fincrimeCases.id, cids));
    }
    const kids = [...keyIds];
    if (kids.length) {
      await auditDb.delete(fincrimeIngestKeys).where(inArray(fincrimeIngestKeys.id, kids));
    }
    console.log(`\n[cleanup] deleted ${cids.length} case(s)+events and ${kids.length} machine key(s). audit_chain_entries left intact (immutable by design).`);
  } catch (e: any) {
    console.log(`[cleanup] WARNING: ${e?.message || e}`);
  }
}

async function main() {
  let child: ChildProcess | null = null;
  try {
    console.log("== PART A: mint commit-before-respond (in-process sim-tx) ==");
    await partA();

    console.log(`\n== PART B: spawning real server on :${PORT} (process-local salt) ==`);
    child = spawn("npx", ["tsx", "server/index.ts"], {
      env: { ...process.env, PORT: String(PORT), MACHINE_KEY_SALT: TEST_SALT, NODE_ENV: "development" },
      stdio: ["ignore", "pipe", "pipe"],
    });
    await waitForServer(child);
    console.log(`   child ready on :${PORT}`);
    const { keyIngest } = await partB();

    console.log(`\n== PART C: dormant + CSRF reachability on live :${LIVE_PORT} (no salt) ==`);
    await partC(keyIngest.rawKey);
  } catch (e: any) {
    record("HARNESS", false, `threw: ${e?.message || e}`);
  } finally {
    await cleanup();
    if (child) {
      child.kill("SIGTERM");
      await new Promise((r) => setTimeout(r, 1200));
      if (!child.killed) child.kill("SIGKILL");
    }
  }

  const passed = legs.filter((l) => l.pass).length;
  const total = legs.length;
  console.log(`\n==== RESULT: ${passed}/${total} legs PASS ====`);
  process.exit(passed === total && total > 0 ? 0 : 1);
}

main();
