/**
 * vf-w4c-sql-allowlist-proof.ts  (AS/PLATFORM/2026/007 — Wave W4c)
 *
 * Behavioral proof for server/lib/sql-allowlist.ts — the single SQL-identifier
 * allowlist module (replit.md "SQL identifier safety via a single allowlist
 * module"). PURE IN-PROCESS LOGIC: no DB, no tenant, no RLS axis (the helpers
 * are deterministic regex validators). Paired POS/NEG (Rule 18):
 *   POS = legitimate identifiers pass through unchanged (and quote correctly).
 *   NEG = every classic injection / malformed identifier THROWS
 *         UnsafeSqlIdentifierError (the control's whole reason to exist).
 *
 * This is the family-discriminator for "is the allowlist a real gate or a
 * no-op pass-through?" — a no-op would let the NEG vectors through.
 *
 * WRITES: none. Run: npx tsx scripts/vf-w4c-sql-allowlist-proof.ts
 */
import {
  safeIdentifier,
  safeSchema,
  safeColumn,
  quotedIdentifier,
  quotedSchema,
  quotedColumn,
  quotedSchemaTable,
  UnsafeSqlIdentifierError,
} from "../server/lib/sql-allowlist";

type Check = { id: string; label: string; pass: boolean; detail: string };
const checks: Check[] = [];
const add = (id: string, label: string, pass: boolean, detail: string) =>
  checks.push({ id, label, pass, detail });

function throwsUnsafe(fn: () => unknown): boolean {
  try {
    fn();
    return false;
  } catch (e) {
    return e instanceof UnsafeSqlIdentifierError;
  }
}

function main() {
  console.log("=== W4c — sql-allowlist substrate proof (in-process, dev) ===\n");

  // ---- POS: legitimate identifiers pass through unchanged ----
  const goodIds = ["users", "audit_logs", "tenant_id", "_x", "a", "x".repeat(63)];
  const posOk = goodIds.every((s) => safeIdentifier(s) === s);
  add("P1", "valid identifiers returned unchanged (POS)", posOk,
    `inputs=${JSON.stringify(goodIds.map((s) => (s.length > 12 ? s.slice(0, 9) + "…" : s)))}`);

  add("P2", "safeSchema / safeColumn accept valid names (POS)",
    safeSchema("public") === "public" && safeColumn("created_at") === "created_at",
    `safeSchema("public") & safeColumn("created_at") both returned input`);

  // ---- POS: quoting wraps a VALIDATED identifier in double quotes ----
  add("P3", "quotedIdentifier/quotedSchema/quotedColumn double-quote a valid name (POS)",
    quotedIdentifier("users") === '"users"' &&
      quotedSchema("public") === '"public"' &&
      quotedColumn("amount") === '"amount"',
    `quotedIdentifier("users")=${quotedIdentifier("users")}`);

  add("P4", "quotedSchemaTable composes schema.table, both validated (POS)",
    quotedSchemaTable("public", "audit_logs") === '"public"."audit_logs"',
    quotedSchemaTable("public", "audit_logs"));

  // ---- NEG: injection / malformed identifiers MUST throw ----
  const badIds: Array<[string, unknown]> = [
    ["sql-injection ;DROP", "users; DROP TABLE x"],
    ["embedded double-quote", 'a"b'],
    ["dash/comment char", "users--"],
    ["uppercase (regex is lowercase-only)", "Users"],
    ["leading digit", "1users"],
    ["empty string", ""],
    ["whitespace", "user name"],
    ["apostrophe", "tenant'id"],
    ["over 63 chars", "x".repeat(64)],
    ["non-string number", 123],
    ["non-string null", null],
    ["non-string undefined", undefined],
  ];
  let negAllThrow = true;
  const negDetail: string[] = [];
  for (const [label, val] of badIds) {
    const t = throwsUnsafe(() => safeIdentifier(val as string));
    if (!t) negAllThrow = false;
    negDetail.push(`${label}:${t ? "THROW" : "PASSED-THROUGH!"}`);
  }
  add("N1", "every injection/malformed identifier throws UnsafeSqlIdentifierError (NEG)",
    negAllThrow, negDetail.join(" | "));

  // ---- NEG: the quoted* helpers validate BEFORE quoting (no bypass) ----
  add("N2", "quotedIdentifier validates first — refuses an embedded-quote name (NEG)",
    throwsUnsafe(() => quotedIdentifier('a"; DROP--')),
    `quotedIdentifier('a"; DROP--') threw`);

  add("N3", "safeSchema and safeColumn also reject injection (NEG)",
    throwsUnsafe(() => safeSchema("pub; DROP")) && throwsUnsafe(() => safeColumn("c; --")),
    `both threw`);

  // ---- summary ----
  console.log("=== Check results ===");
  let allPass = true;
  for (const c of checks) {
    if (!c.pass) allPass = false;
    console.log(`  [${c.pass ? "PASS" : "FAIL"}] ${c.id} — ${c.label}`);
    console.log(`         ${c.detail}`);
  }
  const passCount = checks.filter((c) => c.pass).length;
  console.log(`\n${passCount}/${checks.length} checks passed`);
  process.exit(allPass ? 0 : 1);
}

main();
