// src/layer/adapters/__probes__/determinism.probe.ts
//
// Determinism gate — every Tier-1 adapter must produce byte-identical output
// across re-runs of the same input. Failure means the proxy cache key would
// change on retry → full input price every request.
//
// SHAPE
//   bun run src/layer/adapters/__probes__/determinism.probe.ts
//   exit 0 on success, exit 1 on any determinism violation.

import type { ToolResult } from "../../../provider/canonical.ts";
import type { ToolResultAdapter } from "../types.ts";
import { readDedupe } from "../read-dedupe.ts";
import { bashPassthroughSmall } from "../bash-passthrough-small.ts";
import { bashTruncateLarge } from "../bash-truncate-large.ts";
import { mcpTruncateLarge } from "../mcp-truncate-large.ts";
import { passthroughAdapter } from "../passthrough.ts";

// ---------------------------------------------------------------------------
// Bookkeeping
// ---------------------------------------------------------------------------

let passed = 0;
let failed = 0;

function record(adapterId: string, fixtureIdx: number, ok: boolean, reason?: string, out1Preview?: string, out2Preview?: string): void {
  if (ok) {
    passed++;
    console.log(`OK ${adapterId} fixture${fixtureIdx}`);
    return;
  }
  failed++;
  const detail = reason ?? "non-identical output across re-runs";
  console.log(`FAIL ${adapterId} fixture${fixtureIdx}: ${detail}`);
  if (out1Preview !== undefined) console.log(`  out1[0..80]: ${out1Preview}`);
  if (out2Preview !== undefined) console.log(`  out2[0..80]: ${out2Preview}`);
}

// ---------------------------------------------------------------------------
// Fixture builder
// ---------------------------------------------------------------------------

function tr(toolName: string, content: string, toolCallId = `tc_${toolName}_${content.length}`): ToolResult {
  return { toolCallId, content, isError: false, toolName };
}

// ---------------------------------------------------------------------------
// Core determinism check
// ---------------------------------------------------------------------------

function checkAdapter(adapter: ToolResultAdapter, fixtures: ToolResult[]): void {
  for (let i = 0; i < fixtures.length; i++) {
    const f = fixtures[i]!;

    // Verify match() first — fixture must actually hit this adapter.
    if (!adapter.match(f)) {
      record(adapter.id, i, false, "match() returned false on intended fixture");
      continue;
    }

    const o1 = JSON.stringify(adapter.compress(f));
    const o2 = JSON.stringify(adapter.compress(f));
    // These calls MUST NOT influence compress() determinism:
    Math.random();
    Date.now();
    const o3 = JSON.stringify(adapter.compress(f));

    const ok = o1 === o2 && o2 === o3;
    record(
      adapter.id,
      i,
      ok,
      ok ? undefined : "non-identical output across re-runs",
      ok ? undefined : o1.slice(0, 80),
      ok ? undefined : o2.slice(0, 80),
    );
  }
}

// ---------------------------------------------------------------------------
// Fixture content (deterministic — no Math.random())
// ---------------------------------------------------------------------------

const smallBash =
  "ls -la\ntotal 8\ndrwxr-xr-x  3 user  staff   96 Jan  1 00:00 ./\n" +
  "drwxr-xr-x  4 user  staff  128 Jan  1 00:00 ../";

// >= 2048 bytes required for bash-truncate-large / mcp-truncate-large
const largeBash = Array.from(
  { length: 200 },
  (_, i) => `line-${i}: lorem ipsum dolor sit amet, consectetur adipiscing elit`,
).join("\n");

const largeMcp = JSON.stringify(
  { rows: Array.from({ length: 200 }, (_, i) => ({ id: i, name: `row-${i}` })) },
  null,
  2,
);

const readBody = "// example file\nexport const PI = 3.14159;\n";

// ---------------------------------------------------------------------------
// Per-adapter checks
// ---------------------------------------------------------------------------

checkAdapter(bashPassthroughSmall, [
  tr("Bash", smallBash),
  tr("Bash", "echo hello"),
  tr("Bash", "pwd\n/home/user"),
]);

checkAdapter(bashTruncateLarge, [
  tr("Bash", largeBash),
  tr("Bash", largeBash + "\nextra line appended"),
  tr("Bash", largeBash.repeat(2)),
]);

checkAdapter(mcpTruncateLarge, [
  tr("mcp__github__list_issues", largeMcp),
  tr("mcp__Neon__run_sql", largeMcp),
  tr("mcp__github__pr_view", largeMcp + "\n// extra"),
]);

checkAdapter(readDedupe, [
  tr("Read", readBody),
  tr("Read", readBody + "x"),
  tr("Read", readBody.repeat(2)),
]);

checkAdapter(passthroughAdapter, [
  tr("Bash", "anything"),
  tr("WeirdTool", "weird"),
  tr("mcp__foo", "small"),
]);

// ---------------------------------------------------------------------------
// read-dedupe extra: content-hash determinism (toolCallId must not affect sigil)
// ---------------------------------------------------------------------------

const rdA = readDedupe.compress(tr("Read", readBody, "call_A"));
const rdB = readDedupe.compress(tr("Read", readBody, "call_B"));
if (rdA.preview !== rdB.preview) {
  failed++;
  console.log(
    `FAIL read-dedupe content-hash: sigils differ for identical content` +
      ` (a=${rdA.preview} b=${rdB.preview})`,
  );
} else {
  passed++;
  console.log(`OK read-dedupe content-hash: identical content → identical sigil`);
}

// ---------------------------------------------------------------------------
// read-dedupe extra: 1-byte sensitivity (different content → different sigil)
// ---------------------------------------------------------------------------

const rdC = readDedupe.compress(tr("Read", readBody, "call_C"));
const rdD = readDedupe.compress(tr("Read", readBody + " ", "call_D"));
if (rdC.preview === rdD.preview) {
  failed++;
  console.log(
    `FAIL read-dedupe sensitivity: 1-byte diff produced identical sigil (${rdC.preview})`,
  );
} else {
  passed++;
  console.log(`OK read-dedupe sensitivity: 1-byte diff → different sigil`);
}

// ---------------------------------------------------------------------------
// Summary
// ---------------------------------------------------------------------------

console.log(`summary: passed=${passed} failed=${failed}`);
process.exit(failed > 0 ? 1 : 0);
