/**
 * T004a Tier C — biometric_profiles PII encryption verification
 * Table: biometric_profiles (schema-final.ts)
 * Columns: keystroke_baseline_json (text, nullable), mouse_baseline_json (text, nullable)
 * Key path: per-user — biometricUserCtx(userId) → salt "aegis-pii-user-aes:" + userId
 *
 * Novel aspects vs. Tier B:
 *   (1) Per-user key derivation — THIRD key path (distinct from tenant + platform)
 *   (2) JSON blob content — serialize→encrypt→decrypt→parse with DEEP-EQUAL structure check
 *
 * Checks per column:
 *   (a) DB stores ciphertext (isEncrypted true)
 *   (b) getBiometricProfile() returns parsed plaintext object (not ciphertext string)
 *   (c) JSON round-trip structural integrity — deep-equal to original object
 *   (d) Per-user isolation — ciphertext encrypted under userA's key THROWS when decrypted
 *       under userB's key (AES-GCM auth-tag rejection — not just "wrong value", must THROW)
 *   (e) Random IV — two encrypts of identical value produce DIFFERENT ciphertext strings
 *   (f) Nullable path — null baseline → stored as NULL (not ciphertext), reads back as null
 *   (g) Update path (onConflictDoUpdate) — re-encrypt with new content; DB stores new ciphertext
 */

import { db } from "../server/db";
import { encryptPii, decryptPii, isEncrypted } from "../server/lib/pii-encryption";
import { biometricProfiles } from "../shared/schema-final";
import { eq } from "drizzle-orm";

// Per-user context — mirrors biometricUserCtx() in wave-final-services.ts
const userCtx = (userId: number | string) =>
  ({ type: "user" as const, userId: String(userId) });

// Cross-context — must cause AES-GCM auth-tag rejection
const WRONG_USER_CTX = (wrongId: number | string) =>
  ({ type: "user" as const, userId: String(wrongId) });

interface Result { check: string; pass: boolean; detail?: string }
const results: Result[] = [];
const cleanupIds: number[] = []; // userId values to delete

function pass(check: string, detail?: string) {
  results.push({ check, pass: true, detail });
  console.log(`  ✓ ${check}${detail ? " — " + detail : ""}`);
}
function fail(check: string, detail: string) {
  results.push({ check, pass: false, detail });
  console.error(`  ✗ FAIL: ${check} — ${detail}`);
}

// ── helpers ───────────────────────────────────────────────────────────────────

function deepEqual(a: unknown, b: unknown): boolean {
  return JSON.stringify(a) === JSON.stringify(b);
}

function assertEncrypted(val: string | null | undefined, label: string) {
  if (val === null || val === undefined) { fail(label, "value is null/undefined"); return; }
  if (!isEncrypted(val)) { fail(label, `plaintext stored: ${val.slice(0, 60)}`); return; }
  pass(label, "ciphertext confirmed");
}

// ─────────────────────────────────────────────────────────────────────────────
// TEST FIXTURE — two distinct user IDs that must not collide with prod data
// Using large negative-adjacent IDs that are unlikely to exist in prod
// ─────────────────────────────────────────────────────────────────────────────

const USER_A_ID = 900001; // user A for primary tests
const USER_B_ID = 900002; // user B for cross-user isolation

// Rich JSON fixtures that verify structural complexity survives round-trip
const KEYSTROKE_FIXTURE_A = {
  avgDwell: 82.4,
  avgFlight: 134.7,
  digraphs: { "ab": 112.3, "th": 95.1, "er": 140.2 },
  metadata: { capturedAt: "2026-06-05T09:00:00Z", samples: 150, device: "laptop-aegis-b3" },
  anomalyThreshold: 2.5,
  nested: { level1: { level2: { value: "deep-structure-check" } } },
};
const MOUSE_FIXTURE_A = {
  avgSpeed: 312.8,
  clickPattern: [0.23, 0.41, 0.18, 0.35],
  heatmap: { topLeft: 0.12, topRight: 0.28, bottomLeft: 0.31, bottomRight: 0.29 },
  scrollBehavior: { avgScrollSpeed: 820, reversalRate: 0.08 },
  capturedAt: "2026-06-05T09:00:00Z",
};
const KEYSTROKE_UPDATED = {
  avgDwell: 90.1,
  avgFlight: 128.3,
  digraphs: { "ab": 118.9, "th": 99.7 },
  metadata: { capturedAt: "2026-06-05T09:30:00Z", samples: 200, device: "laptop-aegis-b3-updated" },
  anomalyThreshold: 2.2,
  nested: { level1: { level2: { value: "updated-deep-structure" } } },
};

// ═══════════════════════════════════════════════════════════════════════════════
// CHECK (a)+(b)+(c): Write, DB ciphertext, read, JSON structure
// ═══════════════════════════════════════════════════════════════════════════════

async function testWriteAndRead() {
  console.log("\n[CHECK a/b/c] Write → ciphertext on disk → getBiometricProfile() → deep-equal structure");

  const { saveBiometricProfile, getBiometricProfile } =
    await import("../server/lib/wave-final-services");

  await saveBiometricProfile({
    userId: USER_A_ID,
    keystrokeBaseline: KEYSTROKE_FIXTURE_A,
    mouseBaseline: MOUSE_FIXTURE_A,
    sampleCount: 150,
    confidenceScore: 0.87,
  });
  cleanupIds.push(USER_A_ID);

  // (a) DB stores ciphertext
  const [dbRow] = await db.select().from(biometricProfiles)
    .where(eq(biometricProfiles.userId, USER_A_ID));
  assertEncrypted(dbRow?.keystrokeBaselineJson, "[Ca] keystroke_baseline_json stored as ciphertext");
  assertEncrypted(dbRow?.mouseBaselineJson,     "[Ca] mouse_baseline_json stored as ciphertext");

  // (b) getBiometricProfile returns objects (not strings, not ciphertext)
  const profile = await getBiometricProfile(USER_A_ID);
  if (!profile) { fail("[Cb] getBiometricProfile()", "returned null"); return; }

  if (typeof profile.keystrokeBaseline === "object" && profile.keystrokeBaseline !== null) {
    pass("[Cb] keystrokeBaseline returned as parsed object (not ciphertext string)");
  } else {
    fail("[Cb] keystrokeBaseline type wrong", `type=${typeof profile.keystrokeBaseline}, value=${JSON.stringify(profile.keystrokeBaseline).slice(0,60)}`);
  }
  if (isEncrypted(JSON.stringify(profile.keystrokeBaseline))) {
    fail("[Cb-guard] keystrokeBaseline contains ciphertext string — decrypt step missing", "");
  }

  if (typeof profile.mouseBaseline === "object" && profile.mouseBaseline !== null) {
    pass("[Cb] mouseBaseline returned as parsed object (not ciphertext string)");
  } else {
    fail("[Cb] mouseBaseline type wrong", `type=${typeof profile.mouseBaseline}`);
  }

  // (c) Deep-equal structure check — not just "a non-null object came back"
  if (deepEqual(profile.keystrokeBaseline, KEYSTROKE_FIXTURE_A)) {
    pass("[Cc] keystroke JSON deep-equal after serialize→encrypt→decrypt→parse");
  } else {
    fail("[Cc] keystroke JSON structure MISMATCH",
      `expected=${JSON.stringify(KEYSTROKE_FIXTURE_A).slice(0,80)} got=${JSON.stringify(profile.keystrokeBaseline).slice(0,80)}`);
  }
  if (deepEqual(profile.mouseBaseline, MOUSE_FIXTURE_A)) {
    pass("[Cc] mouse JSON deep-equal after serialize→encrypt→decrypt→parse");
  } else {
    fail("[Cc] mouse JSON structure MISMATCH",
      `expected=${JSON.stringify(MOUSE_FIXTURE_A).slice(0,80)} got=${JSON.stringify(profile.mouseBaseline).slice(0,80)}`);
  }
}

// ═══════════════════════════════════════════════════════════════════════════════
// CHECK (d): Per-user isolation — cross-user decrypt THROWS (AES-GCM auth-tag)
// ═══════════════════════════════════════════════════════════════════════════════

async function testPerUserIsolation() {
  console.log("\n[CHECK d] Per-user isolation — cross-user AES-GCM auth-tag rejection");

  // Fetch the actual ciphertext stored under USER_A_ID's key
  const [dbRow] = await db.select().from(biometricProfiles)
    .where(eq(biometricProfiles.userId, USER_A_ID));

  if (!dbRow?.keystrokeBaselineJson) {
    fail("[Cd] isolation pre-check", "no keystrokeBaselineJson in DB — run write test first");
    return;
  }

  const ciphertextA = dbRow.keystrokeBaselineJson;

  // Attempt to decrypt USER_A's ciphertext using USER_B's key
  let threw = false;
  try {
    decryptPii(ciphertextA, WRONG_USER_CTX(USER_B_ID));
  } catch {
    threw = true;
  }

  if (threw) {
    pass("[Cd-keystroke] cross-user decrypt throws (AES-GCM auth-tag rejection) — per-user isolation holds");
  } else {
    fail("[Cd-keystroke] cross-user decrypt did NOT throw — isolation broken", "user B decrypted user A's ciphertext");
  }

  // Also verify mouse column
  if (dbRow.mouseBaselineJson) {
    let mousethrew = false;
    try {
      decryptPii(dbRow.mouseBaselineJson, WRONG_USER_CTX(USER_B_ID));
    } catch {
      mousethrew = true;
    }
    if (mousethrew) {
      pass("[Cd-mouse] cross-user decrypt throws for mouseBaselineJson — isolation holds");
    } else {
      fail("[Cd-mouse] cross-user decrypt did NOT throw for mouseBaselineJson", "isolation broken");
    }
  }

  // Positive control — USER_A's key correctly decrypts USER_A's ciphertext
  try {
    const recovered = decryptPii(ciphertextA, userCtx(USER_A_ID));
    const parsed = JSON.parse(recovered);
    if (deepEqual(parsed, KEYSTROKE_FIXTURE_A)) {
      pass("[Cd-positive] USER_A's key correctly decrypts USER_A's ciphertext — rule 18 interlock");
    } else {
      fail("[Cd-positive] USER_A's key decrypted but structure differs", "");
    }
  } catch (e: any) {
    fail("[Cd-positive] USER_A's key failed to decrypt USER_A's ciphertext", e?.message);
  }
}

// ═══════════════════════════════════════════════════════════════════════════════
// CHECK (e): Random IV — same plaintext encrypted twice produces different ciphertext
// ═══════════════════════════════════════════════════════════════════════════════

async function testRandomIV() {
  console.log("\n[CHECK e] Random IV — deterministic plaintext → non-deterministic ciphertext");

  const plaintext = JSON.stringify(KEYSTROKE_FIXTURE_A);
  const ctx = userCtx(USER_A_ID);

  const ct1 = encryptPii(plaintext, ctx);
  const ct2 = encryptPii(plaintext, ctx);

  if (ct1 !== ct2) {
    pass("[Ce] Two encrypts of identical value produce different ciphertexts (random IV confirmed)");
  } else {
    fail("[Ce] Two encrypts of identical value produced SAME ciphertext — IV is not random", ct1.slice(0, 60));
  }

  // Both must still decrypt to the same plaintext
  const d1 = JSON.parse(decryptPii(ct1, ctx));
  const d2 = JSON.parse(decryptPii(ct2, ctx));
  if (deepEqual(d1, KEYSTROKE_FIXTURE_A) && deepEqual(d2, KEYSTROKE_FIXTURE_A)) {
    pass("[Ce-roundtrip] Both ciphertexts decrypt to identical original value");
  } else {
    fail("[Ce-roundtrip] At least one ciphertext decrypted incorrectly", "");
  }
}

// ═══════════════════════════════════════════════════════════════════════════════
// CHECK (f): Nullable path — null baseline → stored as NULL, reads back null
// ═══════════════════════════════════════════════════════════════════════════════

async function testNullablePath() {
  console.log("\n[CHECK f] Nullable path — null baseline stored as NULL, not ciphertext");

  const { saveBiometricProfile, getBiometricProfile } =
    await import("../server/lib/wave-final-services");

  // Save with only keystrokeBaseline set, mouseBaseline omitted (undefined → null)
  await saveBiometricProfile({
    userId: USER_B_ID,
    keystrokeBaseline: KEYSTROKE_FIXTURE_A,
    // mouseBaseline deliberately omitted
    sampleCount: 10,
    confidenceScore: 0.50,
  });
  cleanupIds.push(USER_B_ID);

  const [dbRow] = await db.select().from(biometricProfiles)
    .where(eq(biometricProfiles.userId, USER_B_ID));

  // mouseBaselineJson must be NULL (not ciphertext, not encrypted-empty-string)
  if (dbRow?.mouseBaselineJson === null) {
    pass("[Cf-db] Absent mouseBaseline stored as NULL (not ciphertext) — nullable guard correct");
  } else if (isEncrypted(dbRow?.mouseBaselineJson ?? "")) {
    fail("[Cf-db] Absent mouseBaseline stored as ciphertext — encryptPii called on undefined", String(dbRow?.mouseBaselineJson).slice(0, 40));
  } else {
    fail("[Cf-db] Absent mouseBaseline unexpected value", JSON.stringify(dbRow?.mouseBaselineJson));
  }

  // keystroke must be ciphertext (was provided)
  assertEncrypted(dbRow?.keystrokeBaselineJson, "[Cf-db-key] Provided keystroke stored as ciphertext");

  // getBiometricProfile reads null back as null without crash
  const profile = await getBiometricProfile(USER_B_ID);
  if (!profile) {
    fail("[Cf-read] getBiometricProfile() returned null for USER_B", "");
    return;
  }
  if (profile.mouseBaseline === null) {
    pass("[Cf-read] getBiometricProfile() returns null for absent mouseBaseline — no crash");
  } else {
    fail("[Cf-read] getBiometricProfile() returned non-null for absent mouseBaseline", JSON.stringify(profile.mouseBaseline).slice(0, 60));
  }
  if (deepEqual(profile.keystrokeBaseline, KEYSTROKE_FIXTURE_A)) {
    pass("[Cf-read-key] keystrokeBaseline reads correctly when mouseBaseline is null");
  } else {
    fail("[Cf-read-key] keystrokeBaseline mismatch when mouseBaseline is null", "");
  }
}

// ═══════════════════════════════════════════════════════════════════════════════
// CHECK (g): Update path — onConflictDoUpdate re-encrypts with new content
// ═══════════════════════════════════════════════════════════════════════════════

async function testUpdatePath() {
  console.log("\n[CHECK g] Update path — onConflictDoUpdate re-encrypts new content");

  const { saveBiometricProfile, getBiometricProfile } =
    await import("../server/lib/wave-final-services");

  // Update USER_A's profile with new keystroke data
  await saveBiometricProfile({
    userId: USER_A_ID,
    keystrokeBaseline: KEYSTROKE_UPDATED,
    mouseBaseline: MOUSE_FIXTURE_A, // unchanged
    sampleCount: 200,
    confidenceScore: 0.92,
  });

  // DB row must have NEW ciphertext (isEncrypted) for keystroke
  const [dbRow] = await db.select().from(biometricProfiles)
    .where(eq(biometricProfiles.userId, USER_A_ID));
  assertEncrypted(dbRow?.keystrokeBaselineJson, "[Cg-db] Updated keystroke stored as ciphertext after UPDATE");
  assertEncrypted(dbRow?.mouseBaselineJson,     "[Cg-db] Mouse stored as ciphertext after UPDATE");

  // Read back — must return UPDATED content deep-equal
  const profile = await getBiometricProfile(USER_A_ID);
  if (!profile) { fail("[Cg-read] getBiometricProfile() returned null after UPDATE", ""); return; }

  if (deepEqual(profile.keystrokeBaseline, KEYSTROKE_UPDATED)) {
    pass("[Cg-read] Updated keystroke reads back correctly after onConflictDoUpdate");
  } else {
    fail("[Cg-read] Updated keystroke MISMATCH",
      `expected=${JSON.stringify(KEYSTROKE_UPDATED).slice(0,60)} got=${JSON.stringify(profile.keystrokeBaseline).slice(0,60)}`);
  }
  if (deepEqual(profile.mouseBaseline, MOUSE_FIXTURE_A)) {
    pass("[Cg-read] Mouse unchanged after UPDATE — deep-equal to original");
  } else {
    fail("[Cg-read] Mouse baseline altered by UPDATE", "");
  }

  // Cross-user isolation still holds for updated ciphertext
  if (dbRow?.keystrokeBaselineJson) {
    let threw = false;
    try { decryptPii(dbRow.keystrokeBaselineJson, WRONG_USER_CTX(USER_B_ID)); }
    catch { threw = true; }
    if (threw) {
      pass("[Cg-isolation] Updated ciphertext still rejects USER_B's key — isolation preserved post-UPDATE");
    } else {
      fail("[Cg-isolation] Updated ciphertext decryptable by USER_B — isolation broken post-UPDATE", "");
    }
  }
}

// ═══════════════════════════════════════════════════════════════════════════════
// Cleanup
// ═══════════════════════════════════════════════════════════════════════════════

async function cleanup() {
  console.log("\n[Cleanup]");
  let cleaned = 0;
  for (const uid of [...new Set(cleanupIds)]) {
    try {
      await db.delete(biometricProfiles).where(eq(biometricProfiles.userId, uid));
      cleaned++;
    } catch (e: any) {
      console.warn(`  cleanup failed userId=${uid}: ${e?.message}`);
    }
  }
  console.log(`  cleaned ${cleaned}/${new Set(cleanupIds).size} biometric_profiles rows`);
  return cleaned;
}

// ═══════════════════════════════════════════════════════════════════════════════
// Main
// ═══════════════════════════════════════════════════════════════════════════════

async function main() {
  console.log("=== T004a Tier C — biometric_profiles Verification ===");
  console.log("Table: biometric_profiles | Columns: keystroke_baseline_json, mouse_baseline_json");
  console.log("Key path: per-user (salt: aegis-pii-user-aes:<userId>)");
  console.log("Novel checks: (d) per-user cross-user isolation, (c) JSON deep-equal structure");
  console.log(`Test users: USER_A=${USER_A_ID}, USER_B=${USER_B_ID} (no prod data at these IDs)`);

  try {
    await testWriteAndRead();
    await testPerUserIsolation();
    await testRandomIV();
    await testNullablePath();
    await testUpdatePath();
  } finally {
    const cleaned = await cleanup();

    const total  = results.length;
    const passed = results.filter(r => r.pass).length;
    const failed = results.filter(r => !r.pass);

    console.log(`\n=== RESULT: ${passed}/${total} PASS ===`);
    if (failed.length > 0) {
      console.log("FAILURES:");
      for (const f of failed) console.error(`  ✗ ${f.check}: ${f.detail}`);
      console.log(`cleanupRows: ${cleaned}/${new Set(cleanupIds).size}`);
      process.exit(1);
    } else {
      console.log(`ALL ${total} CHECKS PASS — Tier C biometric_profiles wiring verified`);
      console.log(`cleanupRows: ${cleaned}/${new Set(cleanupIds).size}`);
      process.exit(0);
    }
  }
}

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