// Map-covers-registry coverage gate for the full-database export.
//
// WHY THIS TEST EXISTS — the model_cards_v2 near-miss (2026-07-23):
//   model_cards_v2.owner_email was correctly registered in PII_COLUMNS but was
//   MISSING from the export's PII_DECRYPT_MAP (a truncated read left it out). Its
//   ciphertext would have shipped in a bundle whose manifest claims plaintext. The
//   export's ENC-residual guard caught it — but only at an OPERATOR's export, by
//   aborting a real run. This test moves that catch UPSTREAM: it fails at CI/commit,
//   for the developer who added the registry entry, before any export is attempted.
//
//   It is a STATIC check — two in-code lists, no DB, no encryption. It asserts the
//   invariant the guard enforces at runtime, one layer earlier and cheaper.
//
// It imports the REAL constants (not copies) so the check tracks the live maps and
// cannot silently drift out of agreement with the code the export actually runs.
import { test } from "node:test";
import assert from "node:assert/strict";
import { PII_COLUMNS } from "../server/lib/schema-conformance.js";
import { PII_DECRYPT_MAP, EXPORT_EXCLUDE_TABLES } from "../server/lib/pilot-readiness-extras.js";

// ── Forward direction (LOAD-BEARING) ─────────────────────────────────────────
// The failure this catches: a registered PII column that the export would emit as
// ciphertext because it is neither decrypted (in PII_DECRYPT_MAP) nor withheld
// (its table in EXPORT_EXCLUDE_TABLES). That is exactly the model_cards_v2 shape.
//
// A column is "accounted for" if EITHER:
//   (a) its table is in EXPORT_EXCLUDE_TABLES — the table is never exported, so its
//       PII is withheld, not shipped as ciphertext; OR
//   (b) the column is present in PII_DECRYPT_MAP[table] — it is decrypted, passed
//       through (hmac), or redacted on the way into the bundle.
// Any PII_COLUMNS entry that is NEITHER ships ciphertext under a plaintext manifest.
test("every PII_COLUMNS entry is either mapped for decryption or in an excluded table", () => {
  const offenders: string[] = [];
  for (const { table, column } of PII_COLUMNS) {
    const tableExcluded = Object.prototype.hasOwnProperty.call(EXPORT_EXCLUDE_TABLES, table);
    const columnMapped = PII_DECRYPT_MAP[table] !== undefined
      && Object.prototype.hasOwnProperty.call(PII_DECRYPT_MAP[table], column);
    if (!tableExcluded && !columnMapped) offenders.push(`${table}.${column}`);
  }
  assert.equal(
    offenders.length,
    0,
    `The following registered PII column(s) would ship as ciphertext under a manifest that ` +
      `claims plaintext — each is in PII_COLUMNS but neither mapped for decryption nor in an ` +
      `excluded table:\n  ${offenders.join("\n  ")}\n` +
      `Fix each one of: (1) add it to PII_DECRYPT_MAP with its WRITE-ANCHORED context ` +
      `(the same PiiContext the column was encrypted under — a wrong context yields garbage), ` +
      `or (2) if the whole table is credential/key/session material with no portability value, ` +
      `add the table to EXPORT_EXCLUDE_TABLES with a reason. Do NOT guess the context — trace the ` +
      `column's write site (see the provenance comments already in PII_DECRYPT_MAP).`,
  );
});

// ── Reverse direction (milder — stale map entry) ─────────────────────────────
// The failure this catches: a PII_DECRYPT_MAP entry whose column is no longer (or
// was never) in PII_COLUMNS — a stale selector that decrypts a column the registry
// does not track. Not the ciphertext-shipping failure above (this one is a
// correctness/hygiene issue, not a data-leak), but free to assert while we are here.
// Every current map column IS in PII_COLUMNS (incl. users.password and the _hmac
// lookup keys), so this passes with no exemptions today.
test("every PII_DECRYPT_MAP column is a registered PII_COLUMNS entry (no stale map selectors)", () => {
  const registered = new Set(PII_COLUMNS.map((e) => `${e.table}.${e.column}`));
  const stale: string[] = [];
  for (const table of Object.keys(PII_DECRYPT_MAP)) {
    for (const column of Object.keys(PII_DECRYPT_MAP[table])) {
      if (!registered.has(`${table}.${column}`)) stale.push(`${table}.${column}`);
    }
  }
  assert.equal(
    stale.length,
    0,
    `The following PII_DECRYPT_MAP entr(ies) name a column that is not in PII_COLUMNS — a stale ` +
      `map selector decrypting a column the registry does not track:\n  ${stale.join("\n  ")}\n` +
      `Either add the column to PII_COLUMNS (if it is genuinely encrypted PII) or remove the ` +
      `stale PII_DECRYPT_MAP entry.`,
  );
});
