/**
 * KYC v1 — FEATURE 1 — FUNCTIONAL PROOF (binding exit-criterion demonstration)
 * Spec: docs/regulatory/AEGIS_CYBER_KYC_V1_BUILD_SPEC.md (AS/KYC/2026/001, Part C).
 *
 * Exercises the REAL exported substrate — not a code-read. Paired controls (Rule 18:
 * every denial is paired with a positive-enforcement assertion in the same run, so a
 * green is never HOLLOW-GREEN):
 *
 *   LEG 0 — adapter unit: no provider configured ⇒ verifyIdentity returns NOT_CONFIGURED,
 *           verified:false, honest evidence, providerId "none". No synthetic pass reachable.
 *   LEG 1 — POSITIVE through the REAL running server (:5000), route-driven: seed an admin
 *           user + primary UTR + a PENDING customer on aegis-sovereign, log in over HTTP,
 *           mint CSRF, POST /api/kyc/verify/:id. Assert HTTP 200; verification NOT_CONFIGURED /
 *           verified:false; customerStatus REVIEW_REQUIRED. Independent owner read-back:
 *           durable status === REVIEW_REQUIRED (NEVER APPROVED), last_verified_at set, and a
 *           KYC_IDENTITY_VERIFY audit row present (positive enforcement: the route really ran
 *           end-to-end and durably transitioned PENDING→REVIEW_REQUIRED).
 *   LEG 2 — APPROVED is structurally refused (storage):
 *           (a) update→REVIEW_REQUIRED succeeds (positive); update→APPROVED THROWS
 *               (assertNotApprovedWrite); owner read-back proves no APPROVED landed.
 *           (b) createKycCustomer(status:"APPROVED") THROWS; owner read-back proves no row.
 *   LEG 3 — Encryption + HMAC round-trip + cross-tenant isolation (storage):
 *           create in tenant A; owner read-back proves national_id is ciphertext (≠ plaintext)
 *           and national_id_hmac === computeLookupHash(plaintext, A); getKycCustomerByNationalId
 *           in A returns the decrypted plaintext (positive); the SAME id / national_id is
 *           invisible in tenant B (denial — paired with the A positive).
 *   LEG 4 — Fail-loud durability (storage): first create succeeds (positive); a second create
 *           with the SAME national_id THROWS on the unique HMAC index (no swallow); owner
 *           read-back proves exactly ONE row exists for that HMAC (the duplicate left nothing).
 *
 * Connections:
 *   owner = pg.Pool on DATABASE_URL (dev DB owner; BYPASSRLS) — independent out-of-band
 *           observer for seed / read-back / cleanup.
 *   real  = storage.* + the identity adapter, driven inside withTenantRls (appPool / aegis_app,
 *           RLS-enforced) exactly as the request/worker tenant phase does; plus the real HTTP
 *           route on :5000 for LEG 1.
 *
 * Side-effect discipline: imports only storage, db (withTenantRls), the adapter and the PII
 * HMAC helper — NOT server/index.ts or server/routes.ts (no server/scheduler boot).
 */

import pg from "pg";
import bcrypt from "bcryptjs";
import { randomUUID } from "node:crypto";
import { storage, type CreateKycCustomerInput } from "../server/storage";
import { withTenantRls } from "../server/db";
import {
  getIdentityVerificationAdapter,
  __resetIdentityVerificationAdapterCacheForTests,
} from "../server/lib/kyc/identity-verification-adapter";
import { computeLookupHash } from "../server/lib/pii-encryption";

const owner = new pg.Pool({ connectionString: process.env.DATABASE_URL });

const SERVER = "http://127.0.0.1:5000";
const TENANT_A = "aegis-sovereign"; // real seeded dev tenant
const TENANT_B = "stanbic-ug-001"; // a DIFFERENT real seeded dev tenant
const base = Date.now();
const HARNESS_XFF = "203.0.113.57"; // RFC 5737 TEST-NET-3 — own rate-limit bucket

const cid = (n: string) => `kyc-f1-${base}-${n}`; // sentinel customer id
const nid = (n: string) => `NID-${base}-${n}`; //    sentinel national id (plaintext)

const settle = (ms = 1500) => new Promise((r) => setTimeout(r, ms));

function makeInput(n: string, over: Partial<CreateKycCustomerInput> = {}): CreateKycCustomerInput {
  return {
    id: cid(n),
    nationalId: nid(n),
    fullName: `KYC-F1 Subject ${n}`,
    dateOfBirth: "1990-01-01",
    phoneNumber: "+256700000000",
    ...over,
  };
}

interface LegResult {
  id: string;
  scenario: string;
  expectation: string;
  observed: string;
  verdict: string;
  pass: boolean;
}
const results: LegResult[] = [];
const rec = (r: LegResult) => results.push(r);

// ── owner (BYPASSRLS) read-back helpers ──────────────────────────────────────
async function ownerRow(id: string) {
  const r = await owner.query<{
    national_id: string;
    national_id_hmac: string | null;
    status: string;
    last_verified_at: Date | null;
  }>(
    "SELECT national_id, national_id_hmac, status, last_verified_at FROM kyc_customers WHERE id = $1",
    [id],
  );
  return r.rows[0] ?? null;
}
async function ownerCountByHmac(hmac: string): Promise<number> {
  const r = await owner.query<{ n: string }>(
    "SELECT count(*)::int AS n FROM kyc_customers WHERE national_id_hmac = $1",
    [hmac],
  );
  return Number(r.rows[0].n);
}

// ── minimal cookie jar (Node fetch / undici getSetCookie) ────────────────────
class Jar {
  private cookies = new Map<string, string>();
  ingest(res: Response) {
    const setCookies =
      (res.headers as unknown as { getSetCookie?: () => string[] }).getSetCookie?.() ?? [];
    for (const sc of setCookies) {
      const pair = sc.split(";")[0];
      const idx = pair.indexOf("=");
      if (idx > 0) this.cookies.set(pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
    }
  }
  header(): string {
    return [...this.cookies.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// LEG 0 — adapter unit: no provider ⇒ NOT_CONFIGURED / verified:false
// ─────────────────────────────────────────────────────────────────────────────
async function leg0() {
  __resetIdentityVerificationAdapterCacheForTests();
  const adapter = getIdentityVerificationAdapter();
  const configured = adapter.isConfigured();
  const result = await adapter.verifyIdentity({
    tenantId: TENANT_A,
    customerId: cid("0"),
    nationalId: nid("0"),
    fullName: "KYC-F1 Subject 0",
    dateOfBirth: "1990-01-01",
    phoneNumber: "+256700000000",
    correlationId: `kyc-f1-leg0-${base}`,
  });
  const pass =
    configured === false &&
    result.status === "NOT_CONFIGURED" &&
    result.verified === false &&
    adapter.id === "none" &&
    typeof result.evidenceSummary === "string" &&
    result.evidenceSummary.length > 0 &&
    (result as Record<string, unknown>).providerReference === undefined &&
    (result as Record<string, unknown>).confidence === undefined;
  rec({
    id: "L0",
    scenario: "adapter unit — no KYC_IDENTITY_PROVIDER configured",
    expectation:
      "isConfigured()=false; status=NOT_CONFIGURED; verified=false; no confidence/providerReference; honest evidence",
    observed: `id=${adapter.id} configured=${configured} status=${result.status} verified=${result.verified} conf=${result.confidence ?? "—"} ref=${result.providerReference ?? "—"}`,
    verdict: pass ? "PASS (no synthetic pass reachable with no provider)" : "FAIL",
    pass,
  });
}

// ─────────────────────────────────────────────────────────────────────────────
// LEG 1 — POSITIVE through the REAL running server (:5000), route-driven verify
// ─────────────────────────────────────────────────────────────────────────────
async function leg1(): Promise<string | null> {
  const demoUserId = randomUUID();
  const demoUsername = `kyc-f1-admin-${base}`;
  const demoPassword = `Kyc!F1-${base}-Demo`;
  const demoHash = bcrypt.hashSync(demoPassword, 10);

  // seed admin user + primary UTR on tenant A via owner (BYPASSRLS)
  await owner.query(
    `INSERT INTO users (id, username, password, role, full_name, is_active)
     VALUES ($1, $2, $3, 'admin', 'KYC-F1 Demo Admin', true)`,
    [demoUserId, demoUsername, demoHash],
  );
  await owner.query(
    `INSERT INTO user_tenant_roles (user_id, tenant_id, role, is_active, is_primary)
     VALUES ($1, $2, 'admin', true, true)`,
    [demoUserId, TENANT_A],
  );

  // seed a PENDING customer on tenant A through the REAL storage (real encryption + HMAC)
  await withTenantRls(TENANT_A, (tdb) =>
    storage.createKycCustomer(makeInput("1"), TENANT_A, { _db: tdb }),
  );

  const jar = new Jar();
  try {
    const loginRes = await fetch(`${SERVER}/api/auth/login`, {
      method: "POST",
      headers: { "content-type": "application/json", "x-forwarded-for": HARNESS_XFF },
      body: JSON.stringify({ username: demoUsername, password: demoPassword }),
    });
    jar.ingest(loginRes);
    const loginJson: any = await loginRes.json().catch(() => ({}));
    if (loginRes.status !== 200 || loginJson?.success !== true) {
      rec({
        id: "L1",
        scenario: "real server :5000 — login → csrf → POST /api/kyc/verify/:id",
        expectation: "durable status REVIEW_REQUIRED; never APPROVED",
        observed: `login HTTP ${loginRes.status} ${JSON.stringify(loginJson).slice(0, 160)}`,
        verdict: "FAIL (could not authenticate seeded admin)",
        pass: false,
      });
      return demoUserId;
    }

    const csrfRes = await fetch(`${SERVER}/api/csrf-token`, {
      headers: { cookie: jar.header(), "x-forwarded-for": HARNESS_XFF },
    });
    jar.ingest(csrfRes);
    const csrfJson: any = await csrfRes.json().catch(() => ({}));
    const csrfToken: string = csrfJson?.csrfToken;

    const verifyRes = await fetch(`${SERVER}/api/kyc/verify/${cid("1")}`, {
      method: "POST",
      headers: {
        "content-type": "application/json",
        cookie: jar.header(),
        "x-csrf-token": csrfToken,
        "x-forwarded-for": HARNESS_XFF,
      },
      body: JSON.stringify({}),
    });
    const vJson: any = await verifyRes.json().catch(() => ({}));
    await settle(1200); // let the request commit before reading back

    const row = await ownerRow(cid("1"));
    const auditCount = await owner.query<{ n: string }>(
      "SELECT count(*)::int AS n FROM audit_logs WHERE action = 'KYC_IDENTITY_VERIFY' AND resource = $1",
      [`kyc_customers/${cid("1")}`],
    );
    const audited = Number(auditCount.rows[0].n) >= 1;

    const pass =
      verifyRes.status === 200 &&
      vJson?.verification?.status === "NOT_CONFIGURED" &&
      vJson?.verification?.verified === false &&
      vJson?.customerStatus === "REVIEW_REQUIRED" &&
      row != null &&
      row.status === "REVIEW_REQUIRED" &&
      row.status !== "APPROVED" &&
      row.last_verified_at != null &&
      audited;
    rec({
      id: "L1",
      scenario: "real server :5000 — route-driven /api/kyc/verify (no provider configured)",
      expectation:
        "HTTP 200; verification NOT_CONFIGURED/verified:false; durable status REVIEW_REQUIRED (NEVER APPROVED); last_verified_at set; audit row present",
      observed: `HTTP ${verifyRes.status}; verify.status=${vJson?.verification?.status} verified=${vJson?.verification?.verified}; customerStatus=${vJson?.customerStatus}; durable.status=${row?.status}; lastVerified=${row?.last_verified_at ? "set" : "null"}; audited=${audited}`,
      verdict: pass
        ? "PASS (no provider ⇒ non-pass; durable REVIEW_REQUIRED; APPROVED never written; route really ran)"
        : "FAIL",
      pass,
    });
  } catch (e: any) {
    rec({
      id: "L1",
      scenario: "real server :5000 — route-driven /api/kyc/verify",
      expectation: "durable status REVIEW_REQUIRED; never APPROVED",
      observed: `EXCEPTION ${e?.message ?? String(e)} (is the dev server running on :5000?)`,
      verdict: "FAIL (harness/connectivity error)",
      pass: false,
    });
  }
  return demoUserId;
}

// ─────────────────────────────────────────────────────────────────────────────
// LEG 2 — APPROVED is structurally refused (storage)
// ─────────────────────────────────────────────────────────────────────────────
async function leg2() {
  // seed C2 (PENDING) in tenant A
  await withTenantRls(TENANT_A, (tdb) =>
    storage.createKycCustomer(makeInput("2"), TENANT_A, { _db: tdb }),
  );

  // 2(a) positive: a legal status update succeeds; denial: APPROVED throws
  let positiveStatus = "n/a";
  let approvedThrew = false;
  let approvedErr = "";
  await withTenantRls(TENANT_A, async (tdb) => {
    const upd = await storage.updateKycCustomerStatusAndRisk(
      cid("2"),
      TENANT_A,
      { status: "REVIEW_REQUIRED" },
      { _db: tdb },
    );
    positiveStatus = upd?.status ?? "undefined";
  });
  try {
    await withTenantRls(TENANT_A, (tdb) =>
      storage.updateKycCustomerStatusAndRisk(
        cid("2"),
        TENANT_A,
        { status: "APPROVED" },
        { _db: tdb },
      ),
    );
  } catch (e: any) {
    approvedThrew = true;
    approvedErr = e?.message ?? String(e);
  }
  const row2 = await ownerRow(cid("2"));
  const passA =
    positiveStatus === "REVIEW_REQUIRED" &&
    approvedThrew === true &&
    row2?.status === "REVIEW_REQUIRED" &&
    row2?.status !== "APPROVED";
  rec({
    id: "L2a",
    scenario: "updateKycCustomerStatusAndRisk — legal status vs APPROVED",
    expectation:
      "REVIEW_REQUIRED update succeeds (positive); APPROVED update THROWS; durable row stays REVIEW_REQUIRED (no APPROVED landed)",
    observed: `positive=${positiveStatus}; approvedThrew=${approvedThrew}${approvedErr ? ` ("${approvedErr.slice(0, 70)}…")` : ""}; durable.status=${row2?.status}`,
    verdict: passA ? "PASS (APPROVED structurally refused; legal write unaffected)" : "FAIL",
    pass: passA,
  });

  // 2(b) create with status APPROVED throws; no row lands
  let createApprovedThrew = false;
  try {
    await withTenantRls(TENANT_A, (tdb) =>
      storage.createKycCustomer(makeInput("2c", { status: "APPROVED" }), TENANT_A, { _db: tdb }),
    );
  } catch {
    createApprovedThrew = true;
  }
  const row2c = await ownerRow(cid("2c"));
  const passB = createApprovedThrew === true && row2c === null;
  rec({
    id: "L2b",
    scenario: "createKycCustomer(status:'APPROVED')",
    expectation: "THROWS; no row persisted",
    observed: `threw=${createApprovedThrew}; rowPresent=${row2c !== null}`,
    verdict: passB ? "PASS (create cannot mint an APPROVED customer)" : "FAIL",
    pass: passB,
  });
}

// ─────────────────────────────────────────────────────────────────────────────
// LEG 3 — Encryption + HMAC round-trip + cross-tenant isolation (storage)
// ─────────────────────────────────────────────────────────────────────────────
async function leg3() {
  await withTenantRls(TENANT_A, (tdb) =>
    storage.createKycCustomer(makeInput("3"), TENANT_A, { _db: tdb }),
  );

  const expectedHmac = computeLookupHash(nid("3"), TENANT_A, "kyc_customers", "national_id");
  const row = await ownerRow(cid("3"));
  const ciphertextAtRest = row != null && row.national_id !== nid("3");
  const hmacCorrect = row != null && row.national_id_hmac === expectedHmac;

  // positive: lookup-by-national-id in A returns the decrypted plaintext
  const foundA = await withTenantRls(TENANT_A, (tdb) =>
    storage.getKycCustomerByNationalId(nid("3"), TENANT_A, { _db: tdb }),
  );
  const decryptRoundTrip = foundA != null && foundA.nationalId === nid("3") && foundA.id === cid("3");

  // denial: invisible in tenant B (by id AND by national_id), paired with the A positive
  const crossById = await withTenantRls(TENANT_B, (tdb) =>
    storage.getKycCustomerById(cid("3"), TENANT_B, { _db: tdb }),
  );
  const crossByNid = await withTenantRls(TENANT_B, (tdb) =>
    storage.getKycCustomerByNationalId(nid("3"), TENANT_B, { _db: tdb }),
  );
  const crossTenantEmpty = crossById === undefined && crossByNid === undefined;

  const pass = ciphertextAtRest && hmacCorrect && decryptRoundTrip && crossTenantEmpty;
  rec({
    id: "L3",
    scenario: "encryption-at-rest + tenant-keyed HMAC lookup + cross-tenant isolation",
    expectation:
      "national_id stored as ciphertext (≠ plaintext); national_id_hmac matches computeLookupHash; lookup in A decrypts to plaintext (positive); id+national_id invisible in tenant B (denial)",
    observed: `ciphertextAtRest=${ciphertextAtRest}; hmacCorrect=${hmacCorrect}; A.lookup=${decryptRoundTrip ? "decrypted-match" : "MISS"}; B.byId=${crossById === undefined ? "empty" : "LEAK"}; B.byNid=${crossByNid === undefined ? "empty" : "LEAK"}`,
    verdict: pass ? "PASS (PII encrypted; HMAC correct; cross-tenant denied with positive control)" : "FAIL",
    pass,
  });
}

// ─────────────────────────────────────────────────────────────────────────────
// LEG 4 — Fail-loud durability: duplicate national_id THROWS, not swallowed
// ─────────────────────────────────────────────────────────────────────────────
async function leg4() {
  // positive: first create succeeds
  let firstOk = false;
  await withTenantRls(TENANT_A, async (tdb) => {
    const r = await storage.createKycCustomer(makeInput("4"), TENANT_A, { _db: tdb });
    firstOk = r.id === cid("4");
  });

  // denial: a second create with the SAME national_id collides on the unique HMAC index
  let dupThrew = false;
  let dupErr = "";
  try {
    await withTenantRls(TENANT_A, (tdb) =>
      storage.createKycCustomer(makeInput("4b", { nationalId: nid("4") }), TENANT_A, { _db: tdb }),
    );
  } catch (e: any) {
    dupThrew = true;
    dupErr = e?.message ?? String(e);
  }

  const hmac4 = computeLookupHash(nid("4"), TENANT_A, "kyc_customers", "national_id");
  const count = await ownerCountByHmac(hmac4);
  const pass = firstOk && dupThrew && count === 1;
  rec({
    id: "L4",
    scenario: "duplicate national_id create (unique HMAC index)",
    expectation:
      "first create persists (positive); duplicate THROWS loudly (not swallowed); exactly ONE row for that HMAC",
    observed: `firstOk=${firstOk}; dupThrew=${dupThrew}${dupErr ? ` ("${dupErr.slice(0, 60)}…")` : ""}; rowsForHmac=${count}`,
    verdict: pass ? "PASS (write failure is loud; no silent duplicate)" : "FAIL",
    pass,
  });
}

async function cleanup(demoUserId: string | null) {
  try {
    const del = await owner.query("DELETE FROM kyc_customers WHERE id LIKE $1", [`kyc-f1-${base}-%`]);
    console.log(`\ncleanup: deleted ${del.rowCount} sentinel kyc_customers row(s).`);
  } catch (e: any) {
    console.log(`cleanup kyc_customers error: ${e?.message ?? e}`);
  }
  // NB: audit_logs rows written during the proof (the seeded admin's login + the KYC verify)
  // are intentionally NOT deleted — audit_logs is immutable + hash-chained and tearing a row
  // would break chain verification. The resource column is free text (no FK to kyc_customers).
  if (demoUserId) {
    for (const [label, q, p] of [
      ["user_tenant_roles", "DELETE FROM user_tenant_roles WHERE user_id = $1", [demoUserId]],
      ["sessions", "DELETE FROM sessions WHERE user_id = $1", [demoUserId]],
    ] as [string, string, any[]][]) {
      try {
        await owner.query(q, p);
      } catch (e: any) {
        console.log(`cleanup ${label} (non-fatal): ${e?.message ?? e}`);
      }
    }
    // Hard-delete is blocked by the audit_logs FK (login + verify reference this user and the
    // chain must not be torn). Repo pattern: deactivate + neutralize the credential instead.
    try {
      await owner.query(
        "UPDATE users SET is_active = false, password = gen_random_uuid()::text WHERE id = $1",
        [demoUserId],
      );
      console.log("cleanup: seeded admin deactivated + credential neutralized (audit chain preserved).");
    } catch (e: any) {
      console.log(`cleanup users deactivate (non-fatal): ${e?.message ?? e}`);
    }
  }
}

async function main() {
  console.log("=".repeat(80));
  console.log("KYC v1 — FEATURE 1 — FUNCTIONAL PROOF (binding exit-criterion demonstration)");
  console.log("spec AS/KYC/2026/001 Part C | env: DEV |", new Date().toISOString());
  console.log("=".repeat(80));

  let demoUserId: string | null = null;
  try {
    await leg0();
    demoUserId = await leg1();
    await leg2();
    await leg3();
    await leg4();
  } finally {
    await cleanup(demoUserId);
    await owner.end().catch(() => {});
  }

  console.log("");
  for (const r of results) {
    console.log("-".repeat(80));
    console.log(`[${r.id}] ${r.scenario}`);
    console.log(`     expect  : ${r.expectation}`);
    console.log(`     observe : ${r.observed}`);
    console.log(`     VERDICT : ${r.verdict}`);
  }
  console.log("-".repeat(80));
  const passed = results.filter((r) => r.pass).length;
  const total = results.length;
  const allPass = results.every((r) => r.pass);
  console.log(`ACCEPTANCE: ${passed}/${total} checks PASS — ${allPass ? "ALL PASS" : "SEE FAILURES ABOVE"}`);
  process.exit(allPass ? 0 : 1);
}

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