/**
 * verify-mlkem768-real.ts — NIST ML-KEM-768 (FIPS 203) KAT/ACVP conformance verifier.
 *
 * AS/PLATFORM/2026/005. This is the LOAD-BEARING proof that the ML-KEM-768
 * implementation reproduces NIST's official answers BYTE-EXACT — not merely "it runs".
 *
 * Legs:
 *   [A] Fixture integrity gate  — vendored NIST files match recorded SHA-256.
 *   [B] keyGen KAT (byte-exact) — keygen(d‖z) -> ek,dk == NIST.
 *   [C] encaps KAT (byte-exact) — encapsulate(ek,m) -> c,k == NIST.
 *   [D] decaps KAT (byte-exact) — decapsulate(c,dk) -> k == NIST.
 *   [E] paired NEGATIVE controls — a deliberately-wrong input/expectation MUST fail,
 *       so a green positive leg cannot be hollow (comparator + injection actually bite).
 *   [F] random round-trip       — fresh keypair, encaps/decaps agree, many iters.
 *   [G] size checks             — 1184/2400/1088/32 (FIPS 203 Table 3).
 *   [H] implicit rejection      — tamper 1 ct byte -> 32B SS, != real SS, no throw, deterministic.
 *
 * Rule 1: NEVER print key/secret/shared-secret bytes. Only tcIds, pass/fail,
 * byte lengths, first-difference index, and fixture SHA-256 hashes.
 *
 * Exit code 0 iff EVERY leg passes and EVERY negative control fails as required.
 *
 * Run: npx tsx scripts/verify-mlkem768-real.ts
 */
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { ml_kem768 } from "@noble/post-quantum/ml-kem.js";

const HERE = path.dirname(fileURLToPath(import.meta.url));
const FIX = path.resolve(HERE, "../tests/fixtures/mlkem768");

// Recorded in tests/fixtures/mlkem768/PROVENANCE.md (pinned NIST commit).
const FIXTURES = {
  keyGen: {
    file: "ML-KEM-keyGen-FIPS203.internalProjection.json",
    sha256: "d7a62a2c3476957f56dd8d24f9004ea6776ccfe995ffe71a65bb9506dc9c7b1b",
  },
  encapDecap: {
    file: "ML-KEM-encapDecap-FIPS203.internalProjection.json",
    sha256: "f1e22b7d399dde7bf61b838770c658a380e4b1cfc4bd395dbed9ec6c1d977d9d",
  },
} as const;

const PARAM = "ML-KEM-768";
const SIZE = { ek: 1184, dk: 2400, ct: 1088, ss: 32 } as const;

// ---- helpers (no secret output) ----
function hexToBytes(hex: string): Uint8Array {
  const h = hex.trim();
  if (h.length % 2 !== 0) throw new Error("odd-length hex");
  const out = new Uint8Array(h.length / 2);
  for (let i = 0; i < out.length; i++) out[i] = parseInt(h.substr(i * 2, 2), 16);
  return out;
}
function bytesToHex(b: Uint8Array): string {
  let s = "";
  for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
  return s;
}
/** Returns -1 if equal, else index of first differing byte. Length mismatch -> -2. */
function firstDiff(a: Uint8Array, b: Uint8Array): number {
  if (a.length !== b.length) return -2;
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return i;
  return -1;
}
function eqBytes(a: Uint8Array, b: Uint8Array): boolean {
  return firstDiff(a, b) === -1;
}
function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
  const out = new Uint8Array(a.length + b.length);
  out.set(a, 0);
  out.set(b, a.length);
  return out;
}

let passCount = 0;
let failCount = 0;
function ok(cond: boolean, label: string): boolean {
  if (cond) {
    passCount++;
  } else {
    failCount++;
    console.log(`  ✗ FAIL: ${label}`);
  }
  return cond;
}

function loadFixture(f: { file: string; sha256: string }): any {
  const raw = readFileSync(path.join(FIX, f.file));
  const actual = createHash("sha256").update(raw).digest("hex");
  ok(actual === f.sha256, `fixture integrity ${f.file} (sha256 recorded=${f.sha256.slice(0, 12)}… actual=${actual.slice(0, 12)}…)`);
  return JSON.parse(raw.toString("utf8"));
}
function group(j: any, pred: (g: any) => boolean): any {
  const g = j.testGroups.find(pred);
  if (!g) throw new Error("expected test group not found");
  return g;
}

console.log("=== ML-KEM-768 NIST KAT verifier (FIPS 203 / ACVP) ===");

// [A] integrity gate
console.log("\n[A] Fixture integrity (SHA-256 vs PROVENANCE.md):");
const keyGenJson = loadFixture(FIXTURES.keyGen);
const edJson = loadFixture(FIXTURES.encapDecap);

// [B] keyGen KAT byte-exact
console.log(`\n[B] keyGen KAT byte-exact (${PARAM}):`);
const kgGroup = group(keyGenJson, (g) => g.parameterSet === PARAM && g.testType === "AFT");
let kgChecked = 0;
for (const t of kgGroup.tests) {
  const d = hexToBytes(t.d);
  const z = hexToBytes(t.z);
  const expEk = hexToBytes(t.ek);
  const expDk = hexToBytes(t.dk);
  const { publicKey, secretKey } = ml_kem768.keygen(concat(d, z)); // seed = d‖z
  const ekDiff = firstDiff(publicKey, expEk);
  const dkDiff = firstDiff(secretKey, expDk);
  ok(ekDiff === -1, `keyGen tcId=${t.tcId} ek mismatch @byte ${ekDiff}`);
  ok(dkDiff === -1, `keyGen tcId=${t.tcId} dk mismatch @byte ${dkDiff}`);
  kgChecked++;
}
console.log(`  checked ${kgChecked} keyGen vectors (ek+dk each byte-exact).`);

// [C] encaps KAT byte-exact
console.log(`\n[C] encapsulation KAT byte-exact (${PARAM}):`);
const encGroup = group(edJson, (g) => g.parameterSet === PARAM && g.testType === "AFT" && g.function === "encapsulation");
let encChecked = 0;
for (const t of encGroup.tests) {
  const ek = hexToBytes(t.ek);
  const m = hexToBytes(t.m);
  const expC = hexToBytes(t.c);
  const expK = hexToBytes(t.k);
  const { cipherText, sharedSecret } = ml_kem768.encapsulate(ek, m); // m injected
  const cDiff = firstDiff(cipherText, expC);
  const kDiff = firstDiff(sharedSecret, expK);
  ok(cDiff === -1, `encaps tcId=${t.tcId} c mismatch @byte ${cDiff}`);
  ok(kDiff === -1, `encaps tcId=${t.tcId} k mismatch @byte ${kDiff}`);
  encChecked++;
}
console.log(`  checked ${encChecked} encaps vectors (c+k each byte-exact).`);

// [D] decaps KAT byte-exact
console.log(`\n[D] decapsulation KAT byte-exact (${PARAM}):`);
const decGroup = group(edJson, (g) => g.parameterSet === PARAM && g.testType === "VAL" && g.function === "decapsulation");
let decChecked = 0;
for (const t of decGroup.tests) {
  const dk = hexToBytes(t.dk);
  const c = hexToBytes(t.c);
  const expK = hexToBytes(t.k);
  const k = ml_kem768.decapsulate(c, dk);
  const kDiff = firstDiff(k, expK);
  ok(kDiff === -1, `decaps tcId=${t.tcId} k mismatch @byte ${kDiff}`);
  decChecked++;
}
console.log(`  checked ${decChecked} decaps vectors (k byte-exact).`);

// [E] paired negative controls — these MUST fail; if any "succeeds" the green is hollow.
console.log("\n[E] Negative controls (each MUST be rejected):");
{
  // E1: keyGen with swapped seed order (z‖d) must NOT reproduce NIST ek.
  const t = kgGroup.tests[0];
  const d = hexToBytes(t.d), z = hexToBytes(t.z), expEk = hexToBytes(t.ek);
  const { publicKey } = ml_kem768.keygen(concat(z, d)); // wrong order on purpose
  ok(!eqBytes(publicKey, expEk), "E1 swapped-seed (z‖d) MUST NOT match NIST ek");

  // E2: comparator actually bites — flip one expected byte, real output must differ.
  const { publicKey: pk2 } = ml_kem768.keygen(concat(d, z));
  const tampered = Uint8Array.from(expEk);
  tampered[0] ^= 0xff;
  ok(!eqBytes(pk2, tampered), "E2 flipped-expected-byte MUST NOT equal real ek (comparator bites)");

  // E3: encaps with wrong m must NOT reproduce NIST c.
  const et = encGroup.tests[0];
  const ek = hexToBytes(et.ek), m = hexToBytes(et.m), expC = hexToBytes(et.c);
  const wrongM = Uint8Array.from(m); wrongM[0] ^= 0xff;
  const { cipherText: cWrong } = ml_kem768.encapsulate(ek, wrongM);
  ok(!eqBytes(cWrong, expC), "E3 wrong-m MUST NOT match NIST c");

  // E4: decaps of a tampered ciphertext must NOT reproduce the NIST k (implicit-reject diverges).
  const dt = decGroup.tests[0];
  const dk = hexToBytes(dt.dk), c = hexToBytes(dt.c), expK = hexToBytes(dt.k);
  const cT = Uint8Array.from(c); cT[0] ^= 0xff;
  const kWrong = ml_kem768.decapsulate(cT, dk);
  ok(!eqBytes(kWrong, expK), "E4 tampered-ct decaps MUST NOT match NIST k");
}

// [F] random round-trip
console.log("\n[F] Random round-trip (encaps SS == decaps SS):");
{
  const ITERS = 200;
  let rtOk = 0;
  for (let i = 0; i < ITERS; i++) {
    const { publicKey, secretKey } = ml_kem768.keygen();
    const { cipherText, sharedSecret } = ml_kem768.encapsulate(publicKey);
    const recovered = ml_kem768.decapsulate(cipherText, secretKey);
    if (eqBytes(recovered, sharedSecret)) rtOk++;
  }
  ok(rtOk === ITERS, `round-trip ${rtOk}/${ITERS} agreed`);
  console.log(`  ${rtOk}/${ITERS} random round-trips agreed.`);
}

// [G] size checks (FIPS 203 Table 3)
console.log("\n[G] Size checks (FIPS 203 Table 3):");
{
  const { publicKey, secretKey } = ml_kem768.keygen();
  const { cipherText, sharedSecret } = ml_kem768.encapsulate(publicKey);
  ok(publicKey.length === SIZE.ek, `ek size ${publicKey.length} (want ${SIZE.ek})`);
  ok(secretKey.length === SIZE.dk, `dk size ${secretKey.length} (want ${SIZE.dk})`);
  ok(cipherText.length === SIZE.ct, `ct size ${cipherText.length} (want ${SIZE.ct})`);
  ok(sharedSecret.length === SIZE.ss, `ss size ${sharedSecret.length} (want ${SIZE.ss})`);
}

// [H] implicit rejection
console.log("\n[H] Implicit rejection (tamper 1 ct byte):");
{
  const { publicKey, secretKey } = ml_kem768.keygen();
  const { cipherText, sharedSecret } = ml_kem768.encapsulate(publicKey);
  const tampered = Uint8Array.from(cipherText);
  tampered[0] ^= 0xff;
  let threw = false;
  let ss1: Uint8Array | null = null, ss2: Uint8Array | null = null;
  try {
    ss1 = ml_kem768.decapsulate(tampered, secretKey);
    ss2 = ml_kem768.decapsulate(tampered, secretKey);
  } catch {
    threw = true;
  }
  ok(!threw, "implicit-rejection MUST NOT throw");
  if (ss1 && ss2) {
    ok(ss1.length === SIZE.ss, `implicit-rejection SS length ${ss1.length} (want ${SIZE.ss})`);
    ok(!eqBytes(ss1, sharedSecret), "implicit-rejection SS != real SS");
    ok(eqBytes(ss1, ss2), "implicit-rejection deterministic (same tampered ct -> same SS)");
  }
}

console.log(`\n=== SUMMARY: ${passCount} passed, ${failCount} failed ===`);
if (failCount > 0) {
  console.log("RESULT: ✗ KAT FAILED — implementation is NOT byte-exact conformant.");
  process.exit(1);
}
console.log("RESULT: ✓ ALL LEGS GREEN — ML-KEM-768 reproduces NIST KAT byte-exact, negative controls all rejected.");
process.exit(0);
