/**
 * verify-mlkem768-engine.ts — proves the REAL ML-KEM-768 KAT holds THROUGH the
 * MLKEMEngine public handlers (server/pqcEngine.ts), not just raw @noble.
 *
 * AS/PLATFORM/2026/005, T004 acceptance. The engine handlers draw randomness
 * internally (no seed/m injection on the public API), so:
 *   [I] decapsulate byte-exact vs NIST — inject a NIST keypair, feed NIST c,
 *       assert engine.decapsulate(c) == NIST k for ALL VAL vectors (true KAT through handler).
 *   [J] cross-handler round-trip on NIST keys — engineA.encapsulate(NIST ek) -> {ct,ss};
 *       engineB(dk=NIST dk).decapsulate(ct) -> ss2; assert ss == ss2 (m random, so round-trip-exact).
 *   [K] fresh keypair round-trip + sizes + fingerprint shape (two independent engines).
 *   [L] implicit rejection through the engine (tamper ct -> 32B, != real, no throw).
 *   [M] public return-shape preservation (Buffer fields, algorithm tags, getAlgorithmInfo).
 *
 * Rule 1: never print key/secret/SS bytes — only tcIds, lengths, booleans.
 * Exit 0 iff every leg green.
 *
 * Run: npx tsx scripts/verify-mlkem768-engine.ts
 */
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { MLKEMEngine, type PQCKeyPair } from "../server/pqcEngine";

const HERE = path.dirname(fileURLToPath(import.meta.url));
const FIX = path.resolve(HERE, "../tests/fixtures/mlkem768");
const PARAM = "ML-KEM-768";
const SIZE = { ek: 1184, dk: 2400, ct: 1088, ss: 32 } as const;

function hexToBytes(hex: string): Uint8Array {
  const h = hex.trim();
  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 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;
}

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

function injectKeyPair(engine: MLKEMEngine, ek: Buffer, dk: Buffer): void {
  const kp: PQCKeyPair = {
    publicKey: ek,
    secretKey: dk,
    algorithm: PARAM,
    createdAt: new Date(),
    fingerprint: createHash("sha256").update(ek).digest("hex").substring(0, 16),
  };
  (engine as any).keyPair = kp;
}

const edJson = JSON.parse(readFileSync(path.join(FIX, "ML-KEM-encapDecap-FIPS203.internalProjection.json"), "utf8"));
const decGroup = edJson.testGroups.find((g: any) => g.parameterSet === PARAM && g.testType === "VAL" && g.function === "decapsulation");

console.log("=== ML-KEM-768 KAT THROUGH MLKEMEngine handlers ===");

// [I] engine.decapsulate byte-exact vs NIST k (all VAL vectors)
console.log("\n[I] engine.decapsulate byte-exact vs NIST k:");
{
  let n = 0;
  for (const t of decGroup.tests) {
    const ek = Buffer.from(hexToBytes(t.ek));
    const dk = Buffer.from(hexToBytes(t.dk));
    const c = Buffer.from(hexToBytes(t.c));
    const expK = hexToBytes(t.k);
    const engine = new MLKEMEngine("verify-node");
    injectKeyPair(engine, ek, dk);
    const k = engine.decapsulate(c);
    const d = firstDiff(k, expK);
    ok(d === -1, `engine decaps tcId=${t.tcId} k mismatch @byte ${d}`);
    n++;
  }
  console.log(`  ${n} VAL vectors decapsulated byte-exact through the engine handler.`);
}

// [J] cross-handler round-trip on NIST keypair (encapsulate -> decapsulate)
console.log("\n[J] cross-handler round-trip on NIST keypair (encaps SS == decaps SS):");
{
  let n = 0, agreed = 0;
  for (const t of decGroup.tests) {
    const ek = Buffer.from(hexToBytes(t.ek));
    const dk = Buffer.from(hexToBytes(t.dk));
    const sender = new MLKEMEngine("sender");
    const recipient = new MLKEMEngine("recipient");
    injectKeyPair(recipient, ek, dk);
    const enc = sender.encapsulate(ek);                 // to recipient's NIST public key
    const recovered = recipient.decapsulate(enc.ciphertext);
    if (eqBytes(recovered, enc.sharedSecret)) agreed++;
    n++;
  }
  ok(agreed === n, `cross-handler round-trip ${agreed}/${n}`);
  console.log(`  ${agreed}/${n} round-trips agreed through both engine handlers.`);
}

// [K] fresh keypair round-trip + sizes + fingerprint shape (two independent engines)
console.log("\n[K] fresh-keypair round-trip + sizes + fingerprint:");
{
  const recipient = new MLKEMEngine("fresh-recipient");
  const kp = recipient.generateKeyPair();
  ok(kp.publicKey.length === SIZE.ek, `fresh ek size ${kp.publicKey.length}`);
  ok(kp.secretKey.length === SIZE.dk, `fresh dk size ${kp.secretKey.length}`);
  ok(Buffer.isBuffer(kp.publicKey) && Buffer.isBuffer(kp.secretKey), "keypair fields are Buffers");
  ok(/^[0-9a-f]{16}$/.test(kp.fingerprint), `fingerprint shape ${kp.fingerprint.length} hex chars`);
  ok(kp.fingerprint === createHash("sha256").update(kp.publicKey).digest("hex").substring(0, 16), "fingerprint == sha256(pubkey)[:16]");

  const sender = new MLKEMEngine("fresh-sender");
  const enc = sender.encapsulate(recipient.getPublicKey());
  ok(enc.ciphertext.length === SIZE.ct, `fresh ct size ${enc.ciphertext.length}`);
  ok(enc.sharedSecret.length === SIZE.ss, `fresh ss size ${enc.sharedSecret.length}`);
  const recovered = recipient.decapsulate(enc.ciphertext);
  ok(eqBytes(recovered, enc.sharedSecret), "fresh-keypair round-trip SS agree");
}

// [L] implicit rejection through the engine
console.log("\n[L] implicit rejection through engine.decapsulate:");
{
  const recipient = new MLKEMEngine("ir-recipient");
  recipient.generateKeyPair();
  const sender = new MLKEMEngine("ir-sender");
  const enc = sender.encapsulate(recipient.getPublicKey());
  const tampered = Buffer.from(enc.ciphertext);
  tampered[0] ^= 0xff;
  let threw = false, ss1: Buffer | null = null, ss2: Buffer | null = null;
  try { ss1 = recipient.decapsulate(tampered); ss2 = recipient.decapsulate(tampered); }
  catch { threw = true; }
  ok(!threw, "engine implicit-rejection MUST NOT throw");
  if (ss1 && ss2) {
    ok(ss1.length === SIZE.ss, `engine IR ss length ${ss1.length}`);
    ok(!eqBytes(ss1, enc.sharedSecret), "engine IR ss != real ss");
    ok(eqBytes(ss1, ss2), "engine IR deterministic");
  }
}

// [M] public return-shape / API preservation
console.log("\n[M] public API shape preservation:");
{
  const engine = new MLKEMEngine("shape-node");
  engine.generateKeyPair();
  const info = engine.getAlgorithmInfo();
  ok(info.name === PARAM, "getAlgorithmInfo().name == ML-KEM-768");
  ok(info.publicKeySize === SIZE.ek && info.secretKeySize === SIZE.dk && info.ciphertextSize === SIZE.ct && info.sharedSecretSize === SIZE.ss, "getAlgorithmInfo sizes intact");
  ok(info.nistStandard === "FIPS 203", "getAlgorithmInfo nistStandard intact");
  const enc = engine.encapsulate(engine.getPublicKey());
  ok(Buffer.isBuffer(enc.ciphertext) && Buffer.isBuffer(enc.sharedSecret), "encapsulate returns Buffers");
  ok(enc.algorithm === PARAM && enc.timestamp instanceof Date, "encapsulation algorithm/timestamp tags intact");
  ok(Buffer.isBuffer(engine.getPublicKey()) && engine.getPublicKey().length === SIZE.ek, "getPublicKey Buffer 1184");
  ok(/^[0-9a-f]{16}$/.test(engine.getFingerprint()), "getFingerprint 16 hex");
}

console.log(`\n=== SUMMARY: ${passCount} passed, ${failCount} failed ===`);
if (failCount > 0) { console.log("RESULT: ✗ engine KAT FAILED."); process.exit(1); }
console.log("RESULT: ✓ ALL ENGINE LEGS GREEN — real ML-KEM-768 verified through the MLKEMEngine public API.");
process.exit(0);
