import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { parseClaudeCode } from "./capture/claude-code";
import { composeTale } from "./compose";
import { buildRedactionReport, decideGate, renderGatePreview } from "./gate";

const fixture = (name: string): string =>
  readFileSync(fileURLToPath(new URL(`../fixtures/${name}`, import.meta.url)), "utf8");

// The claude-code fixture leaks a synthetic sk- key and an AWS access key.
const leakyTale = composeTale(parseClaudeCode(fixture("claude-code/basic.jsonl")));

describe("buildRedactionReport", () => {
  test("counts every marker by kind across the whole tale", () => {
    const report = buildRedactionReport(leakyTale);
    expect(report.total).toBeGreaterThanOrEqual(2);
    expect(report.counts["api-key"]).toBeGreaterThanOrEqual(1);
    expect(report.counts["aws-access-key"]).toBeGreaterThanOrEqual(1);
    expect(Object.values(report.counts).reduce((a, b) => a + b, 0)).toBe(report.total);
  });

  test("keeps at most three samples, each showing the marker in redacted context", () => {
    const report = buildRedactionReport(leakyTale);
    expect(report.samples.length).toBeLessThanOrEqual(3);
    expect(report.samples.length).toBeGreaterThan(0);
    for (const sample of report.samples) {
      expect(sample.context).toContain(`[REDACTED:${sample.kind}]`);
      // context is the marker plus ~40 chars of surroundings (and ellipses)
      expect(sample.context.length).toBeLessThanOrEqual(`[REDACTED:${sample.kind}]`.length + 60);
    }
    const serialised = JSON.stringify(report);
    expect(serialised).not.toContain("sk-synthetic");
    expect(serialised).not.toContain("AKIAAAAABBBBCCCCDDDD");
  });

  test("a clean tale reports zero", () => {
    const clean = composeTale({
      agent: "claude-code",
      runId: "clean-run",
      userMessages: ["Say hello."],
      steps: [{ assistantText: "Hello! Nothing sensitive happened here at all today.", toolCalls: [] }],
    });
    expect(buildRedactionReport(clean)).toEqual({ total: 0, counts: {}, samples: [] });
  });
});

describe("decideGate", () => {
  test("--yes publishes without a prompt, TTY or not", () => {
    expect(decideGate({ yes: true, tty: false })).toEqual({ action: "publish" });
    expect(decideGate({ yes: true, tty: true })).toEqual({ action: "publish" });
  });

  test("a TTY without --yes gets the interactive prompt", () => {
    expect(decideGate({ yes: false, tty: true })).toEqual({ action: "prompt" });
  });

  test("no TTY and no --yes is refused, telling hooks/CI to pass --yes", () => {
    const decision = decideGate({ yes: false, tty: false });
    expect(decision.action).toBe("refuse");
    if (decision.action === "refuse") {
      expect(decision.message).toContain("--yes");
      expect(decision.message).toContain("not a TTY");
    }
  });
});

describe("renderGatePreview", () => {
  test("shows title, status, step count, evidence count and what was removed", () => {
    const preview = renderGatePreview(leakyTale, buildRedactionReport(leakyTale));
    expect(preview).toContain("Fixing the flaky checkout test");
    expect(preview).toContain("succeeded");
    expect(preview).toContain("Steps      3");
    expect(preview).toMatch(/Evidence {3}\d+ items?/);
    expect(preview).toMatch(/Secrets {4}\d+ removed — /);
    expect(preview).toContain("[REDACTED:");
  });

  test("says so when nothing was redacted", () => {
    const clean = composeTale({
      agent: "claude-code",
      runId: "clean-run",
      userMessages: ["Say hello."],
      steps: [],
    });
    const preview = renderGatePreview(clean, buildRedactionReport(clean));
    expect(preview).toContain("none found");
  });
});
