import { describe, expect, test } from "bun:test";
import { aggregateGaps, createSandboxToolgapAdapter } from "./sandbox-toolgap";

const IMAGE = "localhost/overdeck-agent-sandbox:abc123456789";
const OLD_IMAGE = "localhost/overdeck-agent-sandbox:000000000000";

function record(overrides: Record<string, unknown> = {}): string {
  return JSON.stringify({
    ts: "2026-08-07T10:00:00Z",
    sandboxId: "task-a",
    image: IMAGE,
    tool: "go",
    cwd: "/sandbox/workspaces/task-a",
    ...overrides,
  });
}

function makeAdapter(files: Record<string, string>) {
  return createSandboxToolgapAdapter({
    gapsPath: "/gaps.jsonl",
    imageTagPath: "/image-tag",
    existsImpl: (path) => path in files,
    readFileImpl: (path) => {
      if (!(path in files)) throw new Error(`missing ${path}`);
      return files[path]!;
    },
  });
}

describe("aggregateGaps", () => {
  test("collapses repeat hits of one tool into one gap", () => {
    const gaps = aggregateGaps(
      [
        record({ ts: "2026-08-07T10:00:00Z" }),
        record({ ts: "2026-08-07T11:00:00Z", sandboxId: "task-b" }),
        record({ ts: "2026-08-07T09:00:00Z" }),
      ],
      IMAGE,
    );
    expect(gaps).toHaveLength(1);
    expect(gaps[0]!.tool).toBe("go");
    expect(gaps[0]!.hits).toBe(3);
    expect([...gaps[0]!.sandboxes].sort()).toEqual(["task-a", "task-b"]);
    expect(gaps[0]!.firstSeen).toBe("2026-08-07T09:00:00Z");
    expect(gaps[0]!.lastSeen).toBe("2026-08-07T11:00:00Z");
  });

  test("drops records from an image that is no longer deployed", () => {
    expect(aggregateGaps([record({ image: OLD_IMAGE })], IMAGE)).toEqual([]);
  });

  test("skips malformed lines instead of failing the poll", () => {
    const gaps = aggregateGaps(["", "not json", JSON.stringify({ tool: "go" }), record()], IMAGE);
    expect(gaps).toHaveLength(1);
    expect(gaps[0]!.hits).toBe(1);
  });

  test("orders by hit count then name", () => {
    const gaps = aggregateGaps(
      [record({ tool: "zig" }), record({ tool: "go" }), record({ tool: "go" })],
      IMAGE,
    );
    expect(gaps.map((g) => g.tool)).toEqual(["go", "zig"]);
  });
});

describe("createSandboxToolgapAdapter", () => {
  test("one item per tool, keyed on the tool name", async () => {
    const adapter = makeAdapter({
      "/image-tag": `${IMAGE}\n`,
      "/gaps.jsonl": [record(), record({ tool: "shellcheck" }), record()].join("\n"),
    });
    const result = await adapter.poll();
    expect(result.items.map((i) => i.id).sort()).toEqual([
      "sandbox-toolgap:go",
      "sandbox-toolgap:shellcheck",
    ]);
    expect(result.items.every((i) => i.severity === "warn" && i.kind === "alert")).toBe(true);
  });

  test("gaps retire once the image is rebuilt under a new tag", async () => {
    const adapter = makeAdapter({
      "/image-tag": `${IMAGE}\n`,
      "/gaps.jsonl": record({ image: OLD_IMAGE }),
    });
    const result = await adapter.poll();
    expect(result.items).toEqual([]);
    expect(result.panels[0]!.data).toMatchObject({ image: IMAGE, gapCount: 0 });
  });

  test("stays silent until a sandbox image has been provisioned", async () => {
    const adapter = makeAdapter({ "/gaps.jsonl": record() });
    expect(await adapter.poll()).toEqual({ items: [], panels: [] });
  });

  test("reports no gaps when nothing has been recorded yet", async () => {
    const adapter = makeAdapter({ "/image-tag": IMAGE });
    const result = await adapter.poll();
    expect(result.items).toEqual([]);
    expect(result.panels[0]!.data).toMatchObject({ gapCount: 0 });
  });
});
