import { chmod, mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { GrokBridge } from "./GrokBridge.js";

/**
 * Cache behaviour, driven by a stub standing in for the Grok CLI.
 *
 * Generation costs real money, so the rule these tests pin down is: pay once
 * per prompt, and pay again only when explicitly asked to — and when you do,
 * the new image replaces the old one rather than piling up beside it.
 */

/** A fake `grok` that writes a numbered image into `--cwd` and logs the call. */
const STUB = `#!/usr/bin/env node
const { appendFileSync, readFileSync, writeFileSync } = require("node:fs");
const { join } = require("node:path");

const argv = process.argv.slice(2);
if (argv.includes("--version")) {
  process.stdout.write("grok-stub 0.0.0\\n");
  process.exit(0);
}

const log = process.env.STUB_LOG;
let calls = 0;
try {
  calls = readFileSync(log, "utf8").split("\\n").filter(Boolean).length;
} catch {}
calls += 1;
appendFileSync(log, "call\\n");

const cwd = argv[argv.indexOf("--cwd") + 1];
writeFileSync(join(cwd, \`gen-\${calls}.png\`), \`png-\${calls}\`);
process.stdout.write(JSON.stringify({ text: "done", total_cost_usd: 0.05 }));
`;

const dirs: string[] = [];

async function makeBridge() {
  const dir = await mkdtemp(join(tmpdir(), "grok-bridge-"));
  dirs.push(dir);
  const stubPath = join(dir, "grok-stub.cjs");
  await writeFile(stubPath, STUB);
  await chmod(stubPath, 0o755);

  const log = join(dir, "calls.log");
  process.env["STUB_LOG"] = log;

  const outputDir = join(dir, "cache");
  const bridge = new GrokBridge({ command: stubPath, outputDir, timeoutMs: 20_000 });
  const callCount = async () =>
    (await readFile(log, "utf8").catch(() => "")).split("\n").filter(Boolean).length;
  return { bridge, outputDir, callCount };
}

afterEach(() => {
  delete process.env["STUB_LOG"];
});

describe("GrokBridge cache", () => {
  it("pays once per prompt, then serves the cached image", async () => {
    const { bridge, callCount } = await makeBridge();
    const request = { prompt: "a survivor atlas" };

    const first = await bridge.generate(request);
    expect(first.cached).toBe(false);
    expect(await readFile(first.path, "utf8")).toBe("png-1");

    const second = await bridge.generate(request);
    expect(second.cached).toBe(true);
    expect(second.costUsd).toBe(0);
    expect(second.path).toBe(first.path);
    // The CLI was never invoked a second time.
    expect(await callCount()).toBe(1);
  });

  it("force repaints and replaces the cache entry in place", async () => {
    const { bridge, outputDir, callCount } = await makeBridge();
    const request = { prompt: "a survivor atlas" };

    await bridge.generate(request);
    const forced = await bridge.generate({ ...request, force: true });

    expect(forced.cached).toBe(false);
    expect(await readFile(forced.path, "utf8")).toBe("png-2");
    expect(await callCount()).toBe(2);

    // One slot, one image — the stale file is gone rather than shadowing.
    const slots = await readdir(outputDir);
    expect(slots).toHaveLength(1);
    const images = await readdir(join(outputDir, slots[0]!));
    expect(images).toEqual(["gen-2.png"]);

    // And the *unforced* path now serves the repainted image, which is the
    // whole point: a forced run must land in the slot it is replacing.
    const after = await bridge.generate(request);
    expect(after.cached).toBe(true);
    expect(await readFile(after.path, "utf8")).toBe("png-2");
    expect(await callCount()).toBe(2);
  });

  it("keeps different prompts in different slots", async () => {
    const { bridge, outputDir } = await makeBridge();

    await bridge.generate({ prompt: "one" });
    await bridge.generate({ prompt: "two", force: true });

    expect(await readdir(outputDir)).toHaveLength(2);
  });
});
