import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { EventEmitter } from "node:events";
import { buildAgentPrompt, fixError, } from "./pixel-fix.js";
let home;
beforeEach(() => {
    home = mkdtempSync(join(tmpdir(), "ggcoder-fix-"));
});
afterEach(() => rmSync(home, { recursive: true, force: true }));
function setupProjectMapping(projectId, name, path, secret = `sk_live_${projectId}_test`) {
    mkdirSync(join(home, ".gg"), { recursive: true });
    writeFileSync(join(home, ".gg", "projects.json"), JSON.stringify({ [projectId]: { name, path, secret } }));
}
const baseError = {
    id: "err_abc123",
    project_id: "proj_p1",
    fingerprint: "fp",
    status: "open",
    type: "TypeError",
    message: "Cannot read properties of undefined (reading 'name')",
    stack: JSON.stringify([
        { fn: "UserCard", file: "/repo/src/UserCard.tsx", line: 42, col: 23, in_app: true },
        { fn: "lib", file: "/repo/node_modules/react/index.js", line: 1, col: 1, in_app: false },
    ]),
    code_context: JSON.stringify({
        file: "/repo/src/UserCard.tsx",
        error_line: 42,
        lines: [
            "function UserCard({ user }: Props) {",
            "  return (",
            "    <div>{user.name}</div>",
            "  );",
            "}",
        ],
    }),
    runtime: "node-22",
    occurrences: 47,
    recurrence_count: 0,
    branch: null,
};
describe("buildAgentPrompt", () => {
    it("includes the error type, message, and in-app location", () => {
        const prompt = buildAgentPrompt(baseError, "fix/pixel-err_abc123");
        expect(prompt).toContain("TypeError");
        expect(prompt).toContain("Cannot read properties of undefined");
        expect(prompt).toContain("Location: /repo/src/UserCard.tsx:42");
    });
    it("renders the code window with a marker on the error line", () => {
        const prompt = buildAgentPrompt(baseError, "fix/pixel-err_abc123");
        expect(prompt).toContain("> ");
        expect(prompt).toContain("<div>{user.name}</div>");
    });
    it("renders the stack with (lib) for non-in-app frames", () => {
        const prompt = buildAgentPrompt(baseError, "fix/pixel-err_abc123");
        expect(prompt).toMatch(/UserCard\s+\/repo\/src\/UserCard\.tsx:42(?!\s*\(lib\))/);
        expect(prompt).toMatch(/\/repo\/node_modules\/react\/index\.js:1\s+\(lib\)/);
    });
    it("includes the branch instruction", () => {
        const prompt = buildAgentPrompt(baseError, "fix/pixel-err_abc123");
        expect(prompt).toContain("Create branch: fix/pixel-err_abc123");
        expect(prompt).toContain("Do not merge");
    });
    it("flags recurrence when recurrence_count > 0", () => {
        const prompt = buildAgentPrompt({ ...baseError, recurrence_count: 2 }, "fix/pixel-x");
        expect(prompt).toContain("2nd time");
        expect(prompt).toContain("earlier fix did not hold");
    });
});
function makeFakeChild(exitCode, stdout = "", stderr = "") {
    const child = new EventEmitter();
    child.stdout = new EventEmitter();
    child.stderr = new EventEmitter();
    setImmediate(() => {
        if (stdout)
            child.stdout.emit("data", Buffer.from(stdout));
        if (stderr)
            child.stderr.emit("data", Buffer.from(stderr));
        child.emit("exit", exitCode);
    });
    return child;
}
function buildSpawnFn(handlers) {
    const calls = [];
    let i = 0;
    const fn = (command, args, options) => {
        const call = { command, args, options };
        calls.push(call);
        const handler = handlers[Math.min(i, handlers.length - 1)];
        i++;
        if (!handler)
            throw new Error(`unexpected spawn call: ${command} ${args.join(" ")}`);
        return handler(call);
    };
    return { fn, calls };
}
function fakeFetchScript(handlers) {
    const calls = [];
    let i = 0;
    const fn = (async (input, init) => {
        const url = String(input);
        const method = (init?.method ?? "GET").toUpperCase();
        calls.push({ method, url });
        const handler = handlers[Math.min(i, handlers.length - 1)];
        i++;
        if (!handler)
            throw new Error(`unexpected fetch: ${method} ${url}`);
        return handler({ method, url });
    });
    return { fn, calls };
}
describe("fixError — orchestration", () => {
    it("marks awaiting_review when the agent succeeds, branch exists, and has commits", async () => {
        setupProjectMapping("proj_p1", "demo-app", "/tmp/demo-app");
        const { fn: fetchFn, calls: fetchCalls } = fakeFetchScript([
            // GET /api/errors/err_abc123
            () => new Response(JSON.stringify(baseError), { status: 200 }),
            // PATCH in_progress
            () => new Response(JSON.stringify({ ...baseError, status: "in_progress" }), { status: 200 }),
            // PATCH awaiting_review
            () => new Response(JSON.stringify({ ...baseError, status: "awaiting_review" }), { status: 200 }),
        ]);
        const { fn: spawnFn, calls: spawnCalls } = buildSpawnFn([
            // ggcoder agent run
            () => makeFakeChild(0),
            // git show-ref --verify refs/heads/fix/pixel-err_abc123
            () => makeFakeChild(0),
            // git show-ref --verify refs/heads/main
            () => makeFakeChild(0),
            // git rev-list --count main..fix/pixel-err_abc123
            () => makeFakeChild(0, "3\n"),
        ]);
        const result = await fixError("err_abc123", {
            homeDir: home,
            fetchFn,
            spawnFn,
            inheritStdio: false,
        });
        expect(result.outcome).toBe("awaiting_review");
        expect(result.branch).toBe("fix/pixel-err_abc123");
        expect(result.reason).toContain("3 commit");
        // First call to ggcoder uses the right cwd and args
        const ggCall = spawnCalls[0];
        expect(ggCall?.command).toBe("ggcoder");
        expect(ggCall?.args).toContain("--json");
        expect(ggCall?.args).toContain("--system-prompt");
        expect(ggCall?.options.cwd).toBe("/tmp/demo-app");
        // Status PATCH sequence: in_progress, then awaiting_review
        const patchBodies = fetchCalls.filter((c) => c.method === "PATCH").map((c) => c.url);
        expect(patchBodies).toEqual([
            "https://gg-pixel-server.buzzbeamaustralia.workers.dev/api/errors/err_abc123",
            "https://gg-pixel-server.buzzbeamaustralia.workers.dev/api/errors/err_abc123",
        ]);
    });
    it("marks failed when the agent exits non-zero", async () => {
        setupProjectMapping("proj_p1", "demo-app", "/tmp/demo-app");
        const { fn: fetchFn } = fakeFetchScript([
            () => new Response(JSON.stringify(baseError), { status: 200 }),
            () => new Response("{}", { status: 200 }), // PATCH in_progress
            () => new Response("{}", { status: 200 }), // PATCH failed
        ]);
        const { fn: spawnFn } = buildSpawnFn([() => makeFakeChild(1)]);
        const result = await fixError("err_abc123", {
            homeDir: home,
            fetchFn,
            spawnFn,
            inheritStdio: false,
        });
        expect(result.outcome).toBe("failed");
        expect(result.reason).toContain("exited with code 1");
    });
    it("marks failed when the branch was not created", async () => {
        setupProjectMapping("proj_p1", "demo-app", "/tmp/demo-app");
        const { fn: fetchFn } = fakeFetchScript([
            () => new Response(JSON.stringify(baseError), { status: 200 }),
            () => new Response("{}", { status: 200 }),
            () => new Response("{}", { status: 200 }),
        ]);
        const { fn: spawnFn } = buildSpawnFn([
            () => makeFakeChild(0), // ggcoder ok
            () => makeFakeChild(1), // git show-ref FAIL
        ]);
        const result = await fixError("err_abc123", {
            homeDir: home,
            fetchFn,
            spawnFn,
            inheritStdio: false,
        });
        expect(result.outcome).toBe("failed");
        expect(result.reason).toContain("not created");
    });
    it("marks failed when the branch exists but has no commits ahead of main", async () => {
        setupProjectMapping("proj_p1", "demo-app", "/tmp/demo-app");
        const { fn: fetchFn } = fakeFetchScript([
            () => new Response(JSON.stringify(baseError), { status: 200 }),
            () => new Response("{}", { status: 200 }),
            () => new Response("{}", { status: 200 }),
        ]);
        const { fn: spawnFn } = buildSpawnFn([
            () => makeFakeChild(0), // ggcoder
            () => makeFakeChild(0), // branch exists
            () => makeFakeChild(0), // main exists
            () => makeFakeChild(0, "0\n"), // 0 commits ahead
        ]);
        const result = await fixError("err_abc123", {
            homeDir: home,
            fetchFn,
            spawnFn,
            inheritStdio: false,
        });
        expect(result.outcome).toBe("failed");
        expect(result.reason).toContain("no new commits");
    });
    it("falls back to master when main does not exist", async () => {
        setupProjectMapping("proj_p1", "demo-app", "/tmp/demo-app");
        const { fn: fetchFn } = fakeFetchScript([
            () => new Response(JSON.stringify(baseError), { status: 200 }),
            () => new Response("{}", { status: 200 }),
            () => new Response("{}", { status: 200 }),
        ]);
        const { fn: spawnFn, calls: spawnCalls } = buildSpawnFn([
            () => makeFakeChild(0), // ggcoder
            () => makeFakeChild(0), // branch exists
            () => makeFakeChild(1), // main missing
            () => makeFakeChild(0), // master exists
            () => makeFakeChild(0, "2\n"), // 2 commits ahead of master
        ]);
        const result = await fixError("err_abc123", {
            homeDir: home,
            fetchFn,
            spawnFn,
            inheritStdio: false,
        });
        expect(result.outcome).toBe("awaiting_review");
        const masterCall = spawnCalls.find((c) => c.args.includes("master..fix/pixel-err_abc123"));
        expect(masterCall).toBeTruthy();
    });
    it("throws when no projects.json mapping exists for the project_id", async () => {
        // No setupProjectMapping call — projects.json doesn't exist
        const { fn: fetchFn } = fakeFetchScript([
            () => new Response(JSON.stringify(baseError), { status: 200 }),
        ]);
        const { fn: spawnFn } = buildSpawnFn([]);
        await expect(fixError("err_abc123", {
            homeDir: home,
            fetchFn,
            spawnFn,
            inheritStdio: false,
        })).rejects.toThrow(/No projects mapping/);
    });
    it("throws when no project on this machine owns the error (all secrets reject)", async () => {
        // proj_OTHER is registered but its secret returns 403/404 against err_abc123.
        setupProjectMapping("proj_OTHER", "x", "/x");
        const { fn: fetchFn } = fakeFetchScript([
            () => new Response(JSON.stringify({ error: "forbidden" }), { status: 403 }),
        ]);
        const { fn: spawnFn } = buildSpawnFn([]);
        await expect(fixError("err_abc123", {
            homeDir: home,
            fetchFn,
            spawnFn,
            inheritStdio: false,
        })).rejects.toThrow(/not found in any registered project/);
    });
    it("throws when projects.json has no entries with a stored secret", async () => {
        // Legacy entries (no secret) — must re-install before management works.
        mkdirSync(join(home, ".gg"), { recursive: true });
        writeFileSync(join(home, ".gg", "projects.json"), JSON.stringify({ proj_legacy: { name: "x", path: "/x" } }));
        const { fn: fetchFn } = fakeFetchScript([]);
        const { fn: spawnFn } = buildSpawnFn([]);
        await expect(fixError("err_abc123", {
            homeDir: home,
            fetchFn,
            spawnFn,
            inheritStdio: false,
        })).rejects.toThrow(/No managed projects/);
    });
});
describe("fixError — wiring", () => {
    it("marks the error in_progress before spawning the agent", async () => {
        setupProjectMapping("proj_p1", "demo-app", "/tmp/demo-app");
        const observed = [];
        const { fn: fetchFn } = fakeFetchScript([
            () => new Response(JSON.stringify(baseError), { status: 200 }),
            () => {
                observed.push("PATCH-1");
                return new Response("{}", { status: 200 });
            },
            () => {
                observed.push("PATCH-2");
                return new Response("{}", { status: 200 });
            },
        ]);
        const { fn: spawnFn } = buildSpawnFn([
            () => {
                observed.push("SPAWN");
                return makeFakeChild(0);
            },
            () => makeFakeChild(0),
            () => makeFakeChild(0),
            () => makeFakeChild(0, "1\n"),
        ]);
        await fixError("err_abc123", { homeDir: home, fetchFn, spawnFn, inheritStdio: false });
        expect(observed).toEqual(["PATCH-1", "SPAWN", "PATCH-2"]);
    });
});
// Silence vitest unused import warning
void vi;
//# sourceMappingURL=pixel-fix.test.js.map