import { describe, it, expect, vi } from "vitest";
import { parseToolResult, runCheck, clip, redact } from "./runners";
import { withBaseline, runVerificationContract } from "./harness";
import {
  normalizeAcceptanceChecks,
  formatFailureFeedback,
  failureSignature,
} from "./generation";
import { executeTask } from "./executor";
import type {
  AcceptanceCheck,
  ExecCtx,
  ToolExecutor,
  VerificationReport,
  VerificationTask,
} from "./types";

// A mock tool executor: maps a tool name to a canned raw result string.
function mockExec(map: Record<string, string>): ToolExecutor {
  return async (name: string) => map[name] ?? "{}";
}
function ctx(map: Record<string, string>, previewUrl?: string): ExecCtx {
  return { projectId: "p1", previewUrl, exec: mockExec(map) };
}
const check = (
  kind: AcceptanceCheck["kind"],
  spec: Record<string, unknown> = {},
  hard = true,
): AcceptanceCheck => ({ kind, hard, description: kind, spec });

const task = (checks: AcceptanceCheck[]): VerificationTask => ({
  id: "t1",
  title: "Test task",
  status: "open",
  acceptanceChecks: checks,
  attempts: 0,
});

// ── parsing ──────────────────────────────────────────────────────────────

describe("parseToolResult", () => {
  it("extracts code from a shell result", () => {
    const r = parseToolResult(
      JSON.stringify({ code: 1, stdout: "", stderr: "boom" }),
    );
    expect(r.code).toBe(1);
    expect(r.stderr).toBe("boom");
  });
  it("unwraps a double-nested stdout JSON payload", () => {
    const r = parseToolResult(
      JSON.stringify({ stdout: JSON.stringify({ code: 0, stdout: "ok" }) }),
    );
    expect(r.code).toBe(0);
    expect(r.stdout).toBe("ok");
  });
  it("reads healthCheck.status for server checks", () => {
    const r = parseToolResult(
      JSON.stringify({ previewUrl: "x", healthCheck: { status: 200 } }),
    );
    expect(r.status).toBe(200);
  });
  it("survives non-JSON", () => {
    const r = parseToolResult("not json");
    expect(r.code).toBeNull();
    expect(r.raw).toBe("not json");
  });
});

describe("redaction", () => {
  it("redacts db urls and jwts and long secrets", () => {
    const s = redact(
      "db postgresql://u:p4ssword@host:5432/mydb token eyJhbGciOiJIUzI1.eyJzdWIiOjEy.SflKxwRJSMeKKF secret sk_live_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789xyz",
    );
    expect(s).toContain("[REDACTED_DB_URL]");
    expect(s).toContain("[REDACTED_JWT]");
    expect(s).toContain("[REDACTED_SECRET]");
  });
  it("clip truncates and trims long non-secret text", () => {
    const long = "the quick brown fox jumps over the lazy dog. ".repeat(20);
    expect(clip(long).endsWith("…")).toBe(true);
    expect(clip(long).length).toBeLessThanOrEqual(401);
  });
});

// ── runners ──────────────────────────────────────────────────────────────

describe("runners", () => {
  it("build passes on exit 0, fails on non-zero with stderr", async () => {
    const pass = await runCheck(
      check("build"),
      ctx({ shell_exec: JSON.stringify({ code: 0, stdout: "done" }) }),
    );
    expect(pass.pass).toBe(true);

    const failR = await runCheck(
      check("build"),
      ctx({
        shell_exec: JSON.stringify({
          code: 1,
          stderr: "Type error on auth.ts:14",
        }),
      }),
    );
    expect(failR.pass).toBe(false);
    expect(failR.evidence).toContain("auth.ts:14");
  });

  it("server_up passes on 200, fails otherwise", async () => {
    const pass = await runCheck(
      check("server_up"),
      ctx({
        dev_server_start: JSON.stringify({ healthCheck: { status: 200 } }),
      }),
    );
    expect(pass.pass).toBe(true);

    const failR = await runCheck(
      check("server_up"),
      ctx({
        dev_server_start: JSON.stringify({ healthCheck: { status: 502 } }),
      }),
    );
    expect(failR.pass).toBe(false);
  });

  it("route_ok matches the expected status code", async () => {
    const pass = await runCheck(
      check("route_ok", { url: "http://x/dashboard", expectedStatus: 200 }),
      ctx({ shell_exec: JSON.stringify({ code: 0, stdout: "200" }) }),
    );
    expect(pass.pass).toBe(true);

    const failR = await runCheck(
      check("route_ok", { url: "http://x/dashboard" }),
      ctx({ shell_exec: JSON.stringify({ code: 0, stdout: "404" }) }),
    );
    expect(failR.pass).toBe(false);
    expect(failR.evidence).toContain("404");
  });

  it("console_clean fails when an error is present", async () => {
    const failR = await runCheck(
      check("console_clean", { url: "http://x" }),
      ctx({
        browser_console: JSON.stringify([{ type: "error", text: "boom" }]),
      }),
    );
    expect(failR.pass).toBe(false);

    const pass = await runCheck(
      check("console_clean", { url: "http://x" }),
      ctx({ browser_console: JSON.stringify([{ type: "log", text: "ok" }]) }),
    );
    expect(pass.pass).toBe(true);
  });

  it("content checks for a substring", async () => {
    const pass = await runCheck(
      check("content", { url: "http://x", contains: "GetAcquired" }),
      ctx({
        shell_exec: JSON.stringify({ code: 0, stdout: "<h1>GetAcquired</h1>" }),
      }),
    );
    expect(pass.pass).toBe(true);
  });
});

// ── harness ──────────────────────────────────────────────────────────────

describe("harness", () => {
  it("auto-attaches the baseline contract", () => {
    const merged = withBaseline([], "http://preview");
    const kinds = merged.map((c) => c.kind).sort();
    expect(kinds).toContain("build");
    expect(kinds).toContain("server_up");
    expect(kinds).toContain("console_clean");
  });

  it("does not duplicate a baseline check the planner already specified", () => {
    const merged = withBaseline([check("build")]);
    expect(merged.filter((c) => c.kind === "build").length).toBe(1);
  });

  it("reports passed only when all hard checks pass", async () => {
    const report = await runVerificationContract(
      task([check("route_ok", { url: "http://x/d" })]),
      ctx({
        shell_exec: JSON.stringify({ code: 0, stdout: "200" }),
        dev_server_start: JSON.stringify({ healthCheck: { status: 200 } }),
        browser_console: JSON.stringify([]),
      }),
      { shortCircuit: false },
    );
    expect(report.passed).toBe(true);
    expect(report.failures.length).toBe(0);
  });

  it("short-circuits on the first hard failure", async () => {
    const exec = vi.fn(async (name: string) => {
      if (name === "shell_exec")
        return JSON.stringify({ code: 1, stderr: "build broke" });
      return "{}";
    });
    const report = await runVerificationContract(
      task([]),
      { projectId: "p1", exec },
      { shortCircuit: true },
    );
    expect(report.passed).toBe(false);
    // build is the first hard check; we should NOT have called dev_server_start.
    expect(exec).toHaveBeenCalledWith("shell_exec", expect.anything());
    expect(exec).not.toHaveBeenCalledWith(
      "dev_server_start",
      expect.anything(),
    );
  });

  it("soft check failure does NOT block done", async () => {
    const report = await runVerificationContract(
      task([check("visual", { targetPath: "x" }, false)]),
      ctx({
        shell_exec: JSON.stringify({ code: 0 }),
        dev_server_start: JSON.stringify({ healthCheck: { status: 200 } }),
        browser_console: JSON.stringify([]),
        request_visual_qa: JSON.stringify({ score: 2 }),
      }),
      { shortCircuit: false },
    );
    // visual scored 2 (would fail) but it's soft → does not block.
    expect(report.passed).toBe(true);
  });
});

// ── generation ───────────────────────────────────────────────────────────

describe("generation", () => {
  it("normalizes and caps acceptance checks, dropping unknown kinds", () => {
    const out = normalizeAcceptanceChecks([
      { kind: "route_ok", spec: { url: "x" } },
      { kind: "bogus" },
      { kind: "content", spec: { url: "x", contains: "y" } },
      { kind: "build" },
      { kind: "data", spec: { command: "q" } },
    ]);
    expect(out.length).toBe(3); // capped
    expect(out.find((c) => c.kind === ("bogus" as never))).toBeUndefined();
  });

  it("defaults visual to a soft check", () => {
    const out = normalizeAcceptanceChecks([{ kind: "visual", spec: {} }]);
    expect(out[0].hard).toBe(false);
  });

  it("formats actionable failure feedback", () => {
    const fb = formatFailureFeedback([
      {
        check: check("build"),
        pass: false,
        evidence: "Cannot find name foo (auth.ts:14)",
      },
    ]);
    expect(fb).toContain("[VERIFICATION FAILED]");
    expect(fb).toContain("auth.ts:14");
    expect(fb).toContain("Do not claim success");
  });

  it("failure signatures are stable and order-independent", () => {
    const a = failureSignature([
      { check: check("build"), pass: false, evidence: "x" },
      { check: check("route_ok"), pass: false, evidence: "y" },
    ]);
    const b = failureSignature([
      { check: check("route_ok"), pass: false, evidence: "y" },
      { check: check("build"), pass: false, evidence: "x" },
    ]);
    expect(a).toBe(b);
  });
});

// ── executor fix-loop ────────────────────────────────────────────────────

const passReport = (): VerificationReport => ({
  passed: true,
  results: [],
  failures: [],
});
const failReport = (evidence: string): VerificationReport => ({
  passed: false,
  results: [],
  failures: [{ check: check("build"), pass: false, evidence }],
});

describe("executeTask fix-loop", () => {
  it("FINALIZES immediately when the first verify passes", async () => {
    const runExecution = vi.fn(async () => {});
    const verify = vi.fn(async () => passReport());
    const out = await executeTask(task([]), { runExecution, verify });
    expect(out.status).toBe("done");
    expect(out.attempts).toBe(1);
    expect(runExecution).toHaveBeenCalledTimes(1);
  });

  it("KEEPS FIXING then finalizes when a later attempt passes", async () => {
    const verify = vi
      .fn()
      .mockResolvedValueOnce(failReport("err A"))
      .mockResolvedValueOnce(failReport("err B")) // different evidence = progress
      .mockResolvedValueOnce(passReport());
    const feedbacks: string[] = [];
    const runExecution = vi.fn(async (a: { failureFeedback: string }) => {
      feedbacks.push(a.failureFeedback);
    });
    const out = await executeTask(task([]), { runExecution, verify });
    expect(out.status).toBe("done");
    expect(out.attempts).toBe(3);
    // The 2nd execution received the 1st attempt's concrete failure as context.
    expect(feedbacks[1]).toContain("err A");
  });

  it("ESCALATES (blocked: no_progress) when the same failure repeats", async () => {
    const verify = vi.fn(async () => failReport("same error"));
    const runExecution = vi.fn(async () => {});
    const out = await executeTask(task([]), {
      runExecution,
      verify,
      noProgressLimit: 2,
    });
    expect(out.status).toBe("blocked");
    if (out.status === "blocked") expect(out.reason).toBe("no_progress");
  });

  it("ESCALATES (blocked: max_attempts) if it never goes green but keeps changing", async () => {
    let n = 0;
    const verify = vi.fn(async () => failReport(`err ${n++}`)); // always different
    const runExecution = vi.fn(async () => {});
    const out = await executeTask(task([]), {
      runExecution,
      verify,
      maxAttempts: 3,
    });
    expect(out.status).toBe("blocked");
    if (out.status === "blocked") {
      expect(out.reason).toBe("max_attempts");
      expect(out.attempts).toBe(3);
    }
  });

  it("persists progress on every attempt (resume support)", async () => {
    const verify = vi
      .fn()
      .mockResolvedValueOnce(failReport("e1"))
      .mockResolvedValueOnce(passReport());
    const persisted: number[] = [];
    const t = task([]);
    await executeTask(t, {
      runExecution: async () => {},
      verify,
      persist: (tk) => {
        persisted.push(tk.attempts);
      },
    });
    expect(persisted).toContain(1); // persisted the failing attempt
    expect(t.status).toBe("done");
  });
});
