/**
 * vf-w5a-ai-scoring-proof.ts  (AS/PLATFORM/2026/007 — Wave W5a)
 *
 * Behavioral proof for the AI-scoring / explainability CORE (the REAL claims
 * of W5a). PURE IN-PROCESS LOGIC + in-memory singletons: no DB, no tenant, no
 * RLS axis. Like the W4c sql-allowlist + secret-boot-guard primitives, "run as
 * aegis_app" is deliberately N/A here — these surfaces have NO tenant-scoped
 * DB tables to isolate; the complete behavioral axis IS the in-process axis.
 *
 * Paired POS/NEG (Rule 18) per leg:
 *
 *  A. deterministic-threat-floor — A1 POS: curated event type (ransomware) →
 *     critical band via event-type taxonomy; A1b POS: canonical UNION-based SQLi
 *     payload → payload_signature path fires (contribution>0); A4 DEFECT-CONFIRM
 *     (F-W5a-2, LOW): textbook stacked-query "'; DROP TABLE …; --" NOT caught
 *     (leading-\b bypass); A2 NEG: unknown event type → honest default-40/medium
 *     WITH "not in curated taxonomy" reasoning; A3 determinism: identical input →
 *     byte-identical score/reasoning (no random).
 *
 *  B. ai-provider registry failover — POS: throwing-primary + active-secondary
 *     → secondary result returned AND totalFailovers incremented; NEG: only the
 *     inert (isActive=false) slot present → "No active AI providers" throw, NOT
 *     a fabricated success; honest flag: 2 active ⇒ failoverActive=true, but
 *     1 active + 1 inert ⇒ failoverActive=false (the real default posture).
 *
 *  C. explainable-ai (HONEST-CONTAINED) — POS: high-risk inputs → INCREASE_RISK
 *     top-factors surfaced in summary; NEG (determinism): two identical calls →
 *     identical confidenceScore + identical factors (proves the old
 *     0.85+Math.random() noise + random percentiles are GONE); honesty markers:
 *     confidenceScore === riskScore/100, modelInfo.lastTrainedAt === null,
 *     comparisons.unavailable === true, inputSource === CALLER_SUPPLIED_INPUTS,
 *     auditChain.anchored === false (the containment held; no re-leak).
 *
 *  D. ai-threat-scoring disabled-by-policy → deterministic floor — env-injection
 *     (AI_SCORING_DISABLED=true; NO secret values touched/printed). D1 POS: returns
 *     model "deterministic-floor (AI disabled by policy)" with the SAME score the
 *     floor produces for that context (honest fallback, not a flat constant, not
 *     an outbound AI call); D2 NEG (real control): with the flag CLEARED, scoreThreat
 *     is actually re-invoked and reaches the provider path — a deterministic in-process
 *     stub provider returns valid scoring JSON, so result.model is the STUB's model
 *     (NOT either deterministic-floor label) ⇒ the disabled branch is policy-gated,
 *     not the default path. (No live/outbound AI call: the registry is seeded with a
 *     local stub double for this leg.)
 *
 *  E. static-fallback guardrails — drive the REAL aiCircuitBreaker to OPEN via 5
 *     failing execute() calls, then prove the rule engine: E1 POS over-single-txn
 *     limit ⇒ blocked RULE_001; E2 POS sanctioned-country ⇒ blocked RULE_003; E4 POS
 *     cumulative daily total over limit ⇒ blocked RULE_002 (velocity, accumulated
 *     in-process); E3 NEG under-limit domestic ⇒ allowed (STATIC_FALLBACK); E5
 *     DEFECT-CONFIRM (F-W5a-3): two calls differing ONLY in txn.timestamp produce an
 *     IDENTICAL result ⇒ RULE_004 after-hours reads the server wall-clock
 *     (new Date().getHours()) and ignores txn.timestamp. Control E0: breaker CLOSED
 *     ⇒ applyStaticGuardrails short-circuits to AI_SENTINEL (rules dormant).
 *
 * WRITES: none (in-memory only; circuit breaker reset at end). Run:
 *   npx tsx scripts/vf-w5a-ai-scoring-proof.ts
 */
import { deterministicThreatFloor } from "../server/lib/deterministic-threat-floor";
import { providerRegistry } from "../server/lib/ai-provider/registry";
import { InertSecondaryProvider } from "../server/lib/ai-provider/inert-secondary-provider";
import type { AIProvider, ProviderResponse } from "../server/lib/ai-provider/types";
import { explainDecision } from "../server/lib/explainable-ai";
import { scoreThreat } from "../server/lib/ai-threat-scoring";
import {
  applyStaticGuardrails,
  clearDailyTotals,
  type FallbackResult,
} from "../server/lib/static-fallback";
import { aiCircuitBreaker } from "../server/lib/circuit-breaker";

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 });

// ---- test doubles for registry leg (offline; never calls a real provider) ----
class ThrowingPrimary implements AIProvider {
  readonly id = "test-throwing-primary";
  readonly displayName = "Test throwing primary";
  isActive(): boolean {
    return true;
  }
  async call(): Promise<ProviderResponse> {
    throw new Error("primary transport failure (injected)");
  }
}
class StubSecondary implements AIProvider {
  readonly id = "test-stub-secondary";
  readonly displayName = "Test stub secondary";
  isActive(): boolean {
    return true;
  }
  async call(): Promise<ProviderResponse> {
    return { text: '{"ok":true}', model: "test-model", providerId: this.id };
  }
}
// Returns a VALID ThreatScore JSON so scoreThreat()'s success path completes and
// stamps result.model = response.model (ai-threat-scoring.ts:152). Used by Leg D2
// to prove the disabled branch is policy-gated: with the flag cleared, scoreThreat
// reaches the provider path and returns THIS stub's model (no live AI call).
class ScoringStub implements AIProvider {
  readonly id = "vf-scoring-stub";
  readonly displayName = "VF scoring stub";
  isActive(): boolean {
    return true;
  }
  async call(): Promise<ProviderResponse> {
    return {
      text: JSON.stringify({
        score: 73,
        severity: "high",
        confidence: 0.9,
        reasoning: "stub provider deterministic scoring",
        recommendations: ["stub-action"],
        mitreTechnique: "T1059",
        pdpoRelevance: "n/a",
      }),
      model: "vf-scoring-stub-model",
      providerId: this.id,
    };
  }
}

async function main() {
  console.log("=== W5a — AI scoring/explainability core proof (in-process, dev; role axis N/A) ===\n");

  // ===================================================================
  // LEG A — deterministic-threat-floor (REAL rule engine)
  // ===================================================================
  const floorPos = deterministicThreatFloor({
    eventType: "ransomware",
    sourceIP: "203.0.113.10",
  });
  add(
    "A1",
    "curated event (ransomware) → critical band via event-type taxonomy (POS, core)",
    floorPos.score >= 80 &&
      floorPos.severity === "critical" &&
      floorPos.mitreTechnique === "T1486" &&
      floorPos.factors.some((f) => f.name === "event_type_taxonomy" && f.contribution === 95),
    `score=${floorPos.score} severity=${floorPos.severity} mitre=${floorPos.mitreTechnique} ` +
      `factors=${floorPos.factors.map((f) => `${f.name}:${f.contribution}`).join(",")}`,
  );

  // A1b — the payload-signature path genuinely fires for a canonical UNION-based
  // payload (proves the signature engine is real, not inert).
  const floorUnion = deterministicThreatFloor({
    eventType: "audit_event", // low base (25) so payload boost is visible
    payload: "UNION SELECT password FROM users",
  });
  add(
    "A1b",
    "canonical UNION-based SQLi payload → payload_signature path fires (contribution>0) (POS)",
    floorUnion.factors.some((f) => f.name === "payload_signature" && f.contribution > 0),
    `factors=${floorUnion.factors.map((f) => `${f.name}:${f.contribution}`).join(",")}`,
  );

  // A4 — DEFECT-CONFIRM (F-W5a-2, LOW): a TEXTBOOK stacked-query SQLi
  // ("'; DROP TABLE …; --") does NOT trip the SQLi signature. Root cause: the
  // alternation's leading `\b` cannot anchor at a `;` preceded by a non-word
  // char, so the `;\s*drop\s+table` / `;\s*delete\s+from` / `;\s*truncate`
  // branches are dead for the most common `'; DROP …` form. Bounded: the file
  // already discloses bypass tolerance (L132-135), the floor is a non-AI
  // best-effort fallback (not the primary control), event-type taxonomy
  // dominates, and the payload boost is secondary/capped. This check PASSES by
  // confirming the documented miss (honest defect documentation, W3b-style).
  const floorStacked = deterministicThreatFloor({
    eventType: "audit_event",
    payload: "'; DROP TABLE customers; --",
  });
  add(
    "A4",
    "DEFECT-CONFIRM F-W5a-2: textbook '; DROP TABLE …; -- NOT caught by SQLi signature (leading-\\b bypass) (LOW)",
    floorStacked.factors.find((f) => f.name === "payload_signature")?.contribution === 0,
    `payload_signature contribution=${floorStacked.factors.find((f) => f.name === "payload_signature")?.contribution} ` +
      `(stacked-DROP branch defeated by leading \\b; UNION/tautology forms still caught)`,
  );

  const floorNeg = deterministicThreatFloor({ eventType: "totally_unknown_event_xyz" });
  add(
    "A2",
    "unknown event type → honest default-40/medium WITH 'not in curated taxonomy' disclosure (NEG)",
    floorNeg.score === 40 &&
      floorNeg.severity === "medium" &&
      /not in curated taxonomy/i.test(floorNeg.reasoning),
    `score=${floorNeg.score} severity=${floorNeg.severity} reasoning="${floorNeg.reasoning.slice(0, 90)}…"`,
  );

  const floorDet1 = deterministicThreatFloor({ eventType: "phishing", sourceIP: "198.51.100.7" });
  const floorDet2 = deterministicThreatFloor({ eventType: "phishing", sourceIP: "198.51.100.7" });
  add(
    "A3",
    "identical input → byte-identical score + reasoning (deterministic, no random)",
    floorDet1.score === floorDet2.score &&
      floorDet1.reasoning === floorDet2.reasoning &&
      floorDet1.confidence === 0.6,
    `score1=${floorDet1.score} score2=${floorDet2.score} confidence=${floorDet1.confidence} (fixed-honest)`,
  );

  // ===================================================================
  // LEG B — ai-provider registry failover (REAL orchestration)
  // ===================================================================
  // Reset the singleton (fresh tsx process — does not touch the running app).
  (providerRegistry as unknown as { _resetForTests: () => void })._resetForTests();
  providerRegistry.register(new ThrowingPrimary());
  providerRegistry.register(new StubSecondary());

  const failoverResp = await providerRegistry.call({ maxTokens: 16, prompt: "x" });
  const statusAfterFailover = providerRegistry.getStatus();
  add(
    "B1",
    "throwing-primary + active-secondary → secondary result returned AND failover counted (POS)",
    failoverResp.providerId === "test-stub-secondary" &&
      statusAfterFailover.totalFailovers === 1 &&
      statusAfterFailover.totalErrors === 1,
    `returned=${failoverResp.providerId} totalFailovers=${statusAfterFailover.totalFailovers} ` +
      `totalErrors=${statusAfterFailover.totalErrors}`,
  );
  add(
    "B2",
    "two active providers → failoverActive=true (honest availability flag) (POS)",
    statusAfterFailover.activeCount === 2 && statusAfterFailover.failoverActive === true,
    `activeCount=${statusAfterFailover.activeCount} failoverActive=${statusAfterFailover.failoverActive}`,
  );

  // NEG: only the inert (isActive=false) slot present → throws, no fake success.
  (providerRegistry as unknown as { _resetForTests: () => void })._resetForTests();
  providerRegistry.register(new InertSecondaryProvider());
  let inertThrew = false;
  let inertErrMsg = "";
  try {
    await providerRegistry.call({ maxTokens: 16, prompt: "x" });
  } catch (e) {
    inertThrew = true;
    inertErrMsg = e instanceof Error ? e.message : String(e);
  }
  const inertStatus = providerRegistry.getStatus();
  add(
    "B3",
    "only inert slot active → 'No active AI providers' throw, NOT a fabricated success (NEG)",
    inertThrew && /no active ai providers/i.test(inertErrMsg) && inertStatus.activeCount === 0,
    `threw=${inertThrew} activeCount=${inertStatus.activeCount} msg="${inertErrMsg.slice(0, 70)}…"`,
  );
  add(
    "B4",
    "single live posture (1 active + 1 inert) → failoverActive=false (honest default) (NEG)",
    (() => {
      (providerRegistry as unknown as { _resetForTests: () => void })._resetForTests();
      providerRegistry.register(new StubSecondary());
      providerRegistry.register(new InertSecondaryProvider());
      const s = providerRegistry.getStatus();
      return s.activeCount === 1 && s.failoverActive === false;
    })(),
    `1 active + 1 inert ⇒ failoverActive=false`,
  );

  // ===================================================================
  // LEG C — explainable-ai (HONEST-CONTAINED; determinism + markers)
  // ===================================================================
  const hiInput = {
    amount: 100_000_000,
    time: "After hours (02:30)",
    location: "Nigeria",
    channel: "API",
    recipientType: "New",
  };
  const exPos = explainDecision("VF-W5A-DECL-1", 85, "DECLINE", hiInput);
  add(
    "C1",
    "high-risk inputs → INCREASE_RISK top-factors surfaced in summary (POS)",
    exPos.explanation.factors.some((f) => f.direction === "INCREASE_RISK") &&
      /declined/i.test(exPos.explanation.summary) &&
      exPos.explanation.reasoning.length > 0,
    `summary="${exPos.explanation.summary.slice(0, 80)}…" ` +
      `incRiskFactors=${exPos.explanation.factors.filter((f) => f.direction === "INCREASE_RISK").length}`,
  );

  const exA = explainDecision("VF-W5A-DET", 64, "REVIEW", hiInput);
  const exB = explainDecision("VF-W5A-DET", 64, "REVIEW", hiInput);
  const stripTs = (r: typeof exA) => JSON.stringify({ ...r, timestamp: undefined });
  add(
    "C2",
    "two identical calls → identical confidenceScore + identical factors (no Math.random) (NEG)",
    exA.confidenceScore === exB.confidenceScore && stripTs(exA) === stripTs(exB),
    `confidence1=${exA.confidenceScore} confidence2=${exB.confidenceScore} fullEqual=${stripTs(exA) === stripTs(exB)}`,
  );

  add(
    "C3",
    "honesty markers intact: confidence=riskScore/100, model.lastTrainedAt=null, comparisons.unavailable, CALLER_SUPPLIED_INPUTS, auditChain not anchored",
    Math.abs(exPos.confidenceScore - 85 / 100) < 1e-9 &&
      exPos.modelInfo.lastTrainedAt === null &&
      exPos.comparisons.unavailable === true &&
      exPos.inputSource === "CALLER_SUPPLIED_INPUTS" &&
      exPos.auditChain.anchored === false,
    `confidence=${exPos.confidenceScore} lastTrainedAt=${exPos.modelInfo.lastTrainedAt} ` +
      `comparisons.unavailable=${exPos.comparisons.unavailable} inputSource=${exPos.inputSource} ` +
      `auditChain.anchored=${exPos.auditChain.anchored}`,
  );

  // ===================================================================
  // LEG D — ai-threat-scoring disabled-by-policy → deterministic floor
  // (env injection only; NO secret values read/printed)
  // ===================================================================
  const dctx = {
    eventType: "brute_force",
    sourceIP: "203.0.113.55",
    payload: "<script>alert(1)</script>",
  };
  const floorRef = deterministicThreatFloor(dctx);

  delete process.env.AI_SCORING_DISABLED;
  delete process.env.AI_DISABLED;
  process.env.AI_SCORING_DISABLED = "true";
  const disabledScore = await scoreThreat(dctx);
  delete process.env.AI_SCORING_DISABLED;

  add(
    "D1",
    "AI_SCORING_DISABLED=true → model='deterministic-floor (AI disabled by policy)' with floor-equal score (POS)",
    disabledScore.model === "deterministic-floor (AI disabled by policy)" &&
      disabledScore.score === floorRef.score &&
      /disabled by operator policy/i.test(disabledScore.reasoning),
    `model="${disabledScore.model}" score=${disabledScore.score} floorScore=${floorRef.score}`,
  );

  // D2 (REAL control): clear the flag and ACTUALLY re-invoke scoreThreat. Seed the
  // registry with a local deterministic stub so the provider success path returns
  // valid scoring JSON WITHOUT any live/outbound AI call. scoreThreat then stamps
  // result.model = response.model (the stub's), proving the disabled branch was NOT
  // taken when the flag is clear (it is policy-gated, not the default path).
  (providerRegistry as unknown as { _resetForTests: () => void })._resetForTests();
  providerRegistry.register(new ScoringStub());
  delete process.env.AI_SCORING_DISABLED;
  delete process.env.AI_DISABLED;
  const enabledScore = await scoreThreat(dctx);
  add(
    "D2",
    "flag CLEARED ⇒ scoreThreat reaches the provider path (stub) — model is the stub's, NOT a deterministic-floor label (real control/NEG)",
    enabledScore.model === "vf-scoring-stub-model" &&
      enabledScore.score === 73 &&
      !enabledScore.model.includes("deterministic-floor"),
    `model="${enabledScore.model}" score=${enabledScore.score} (disabled-branch NOT taken; both floor labels avoided)`,
  );

  // ===================================================================
  // LEG E — static-fallback guardrails (drive REAL circuit breaker OPEN)
  // ===================================================================
  // Control: breaker CLOSED ⇒ guardrails dormant (AI_SENTINEL passthrough).
  aiCircuitBreaker.reset();
  const closedResult: FallbackResult = applyStaticGuardrails({
    amount: 99_000_000,
    currency: "UGX",
    userId: 4242,
  });
  add(
    "E0",
    "breaker CLOSED → guardrails dormant, AI_SENTINEL passthrough (control)",
    closedResult.processingMode === "AI_SENTINEL" && closedResult.allowed === true,
    `mode=${closedResult.processingMode} allowed=${closedResult.allowed}`,
  );

  // Trip the breaker: 5 failing execute() calls (failureThreshold=5).
  for (let i = 0; i < 5; i++) {
    await aiCircuitBreaker.execute(
      async () => {
        throw new Error("injected AI failure to trip breaker");
      },
      () => null,
    );
  }
  const trippedState = aiCircuitBreaker.getStats().state;

  const overLimit = applyStaticGuardrails({
    amount: 20_000_000, // > 10M single-txn limit
    currency: "UGX",
    userId: 1001,
  });
  add(
    "E1",
    "breaker OPEN + over single-txn limit → blocked RULE_001 (POS)",
    String(trippedState) === "OPEN" &&
      overLimit.allowed === false &&
      overLimit.ruleTriggered === "RULE_001_SINGLE_TXN_LIMIT" &&
      overLimit.processingMode === "STATIC_FALLBACK",
    `circuit=${trippedState} allowed=${overLimit.allowed} rule=${overLimit.ruleTriggered}`,
  );

  const geoBlocked = applyStaticGuardrails({
    amount: 1_000_000,
    currency: "UGX",
    destinationCountry: "KP", // sanctioned
    userId: 1002,
  });
  add(
    "E2",
    "breaker OPEN + sanctioned destination → blocked RULE_003 (POS)",
    geoBlocked.allowed === false && geoBlocked.ruleTriggered === "RULE_003_GEO_BLOCK",
    `allowed=${geoBlocked.allowed} rule=${geoBlocked.ruleTriggered} reason="${geoBlocked.reason.slice(0, 60)}…"`,
  );

  const underLimit = applyStaticGuardrails({
    amount: 500_000, // under all limits, domestic
    currency: "UGX",
    destinationCountry: "UG",
    userId: 1003,
  });
  add(
    "E3",
    "breaker OPEN + under-limit domestic → allowed via STATIC_FALLBACK (NEG)",
    underLimit.allowed === true &&
      underLimit.processingMode === "STATIC_FALLBACK" &&
      underLimit.ruleTriggered === null,
    `allowed=${underLimit.allowed} mode=${underLimit.processingMode} rule=${underLimit.ruleTriggered}`,
  );

  // E4 POS — RULE_002 daily velocity. Accumulate 10×UGX 5M (each ≤ single-txn limit
  // AND ≤ 5M so RULE_004's amount>5M inner gate never fires regardless of server
  // wall-clock) → cumulative 50M; the 11th call pushes the day total over UGX 50M ⇒
  // blocked RULE_002. Proves the velocity rule genuinely fires (process-local Map).
  clearDailyTotals();
  const velUser = 7777;
  let velWarmAllOk = true;
  for (let i = 0; i < 10; i++) {
    const r = applyStaticGuardrails({ amount: 5_000_000, currency: "UGX", userId: velUser });
    if (!(r.allowed === true && r.ruleTriggered === null)) velWarmAllOk = false;
  }
  const velBlocked = applyStaticGuardrails({ amount: 5_000_000, currency: "UGX", userId: velUser });
  add(
    "E4",
    "breaker OPEN + cumulative daily total over UGX 50M → blocked RULE_002 (velocity, accumulated in-process) (POS)",
    velWarmAllOk &&
      velBlocked.allowed === false &&
      velBlocked.ruleTriggered === "RULE_002_DAILY_VELOCITY" &&
      velBlocked.processingMode === "STATIC_FALLBACK",
    `warmupAllAllowed=${velWarmAllOk} blocked=${!velBlocked.allowed} rule=${velBlocked.ruleTriggered}`,
  );

  // E5 DEFECT-CONFIRM (F-W5a-3) — RULE_004 after-hours reads the SERVER wall-clock
  // (new Date().getHours(), static-fallback.ts:109) and never consults txn.timestamp
  // (the field exists at the interface L10 but is unused). Two requests identical
  // EXCEPT txn.timestamp (02:30 vs 14:30) therefore yield an IDENTICAL result —
  // proving txn.timestamp has zero effect. Deterministic regardless of when the
  // harness runs (both legs see the same server hour). Distinct userIds so neither
  // single 6M call trips RULE_002.
  clearDailyTotals();
  const r_night = applyStaticGuardrails({
    amount: 6_000_000,
    currency: "UGX",
    userId: 8881,
    timestamp: new Date("2026-06-17T02:30:00"),
  });
  const r_day = applyStaticGuardrails({
    amount: 6_000_000,
    currency: "UGX",
    userId: 8882,
    timestamp: new Date("2026-06-17T14:30:00"),
  });
  add(
    "E5",
    "DEFECT-CONFIRM F-W5a-3: identical txn except txn.timestamp (02:30 vs 14:30) → IDENTICAL result ⇒ RULE_004 ignores txn.timestamp (server wall-clock)",
    r_night.allowed === r_day.allowed && r_night.ruleTriggered === r_day.ruleTriggered,
    `night(ts=02:30): allowed=${r_night.allowed} rule=${r_night.ruleTriggered} | ` +
      `day(ts=14:30): allowed=${r_day.allowed} rule=${r_day.ruleTriggered} (txn.timestamp has zero effect)`,
  );

  // cleanup: clear daily totals + return breaker to CLOSED.
  clearDailyTotals();
  aiCircuitBreaker.reset();

  // ===================================================================
  // 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().catch((e) => {
  console.error("HARNESS ERROR:", e);
  process.exit(2);
});
