import { EventEmitter } from "node:events";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("../../utils/logger.js", () => ({
    logger: {
        debug: vi.fn(),
        info: vi.fn(),
        warn: vi.fn(),
        error: vi.fn(),
    },
}));
// Mock child_process.spawn
const mockSpawn = vi.fn();
vi.mock("node:child_process", () => ({
    spawn: (...args) => mockSpawn(...args),
}));
// Mock global fetch for health checks
const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);
function createMockProcess() {
    const proc = new EventEmitter();
    proc.kill = vi.fn().mockImplementation(() => {
        // Simulate process exiting on kill
        process.nextTick(() => proc.emit("exit", null, "SIGTERM"));
        return true;
    });
    Object.defineProperty(proc, "killed", { value: false, writable: true });
    Object.defineProperty(proc, "exitCode", { value: null, writable: true });
    Object.defineProperty(proc, "pid", { value: 12345, writable: true });
    proc.stdout = new EventEmitter();
    proc.stderr = new EventEmitter();
    return proc;
}
describe("spawnOpenCode", () => {
    let spawnOpenCode;
    beforeEach(async () => {
        mockSpawn.mockReset();
        mockFetch.mockReset();
        // Re-import to get fresh module
        const mod = await import("../spawn.js");
        spawnOpenCode = mod.spawnOpenCode;
    });
    afterEach(() => {
        vi.restoreAllMocks();
    });
    it("should spawn opencode serve process with password env", async () => {
        const proc = createMockProcess();
        mockSpawn.mockReturnValue(proc);
        mockFetch.mockResolvedValue({ ok: true });
        const result = await spawnOpenCode(4096);
        expect(mockSpawn).toHaveBeenCalledWith("opencode", ["serve", "--port", "4096"], expect.objectContaining({
            env: expect.objectContaining({
                OPENCODE_SERVER_PASSWORD: expect.any(String),
            }),
            stdio: ["ignore", "pipe", "pipe"],
        }));
        expect(result.process).toBe(proc);
        expect(result.auth.baseUrl).toBe("http://127.0.0.1:4096");
        expect(result.auth.password).toBeTruthy();
        expect(result.auth.authHeader).toMatch(/^Basic /);
        expect(typeof result.stop).toBe("function");
    });
    it("should set OPENCODE_SERVER_PASSWORD env var", async () => {
        const proc = createMockProcess();
        mockSpawn.mockReturnValue(proc);
        mockFetch.mockResolvedValue({ ok: true });
        const result = await spawnOpenCode();
        const spawnCall = mockSpawn.mock.calls[0];
        const env = spawnCall[2].env;
        expect(env.OPENCODE_SERVER_PASSWORD).toBe(result.auth.password);
    });
    it("should wait for health check before returning", async () => {
        const proc = createMockProcess();
        mockSpawn.mockReturnValue(proc);
        // First call fails, second succeeds
        mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED")).mockResolvedValueOnce({ ok: true });
        const result = await spawnOpenCode(4096);
        expect(mockFetch).toHaveBeenCalledWith("http://127.0.0.1:4096/global/health", expect.objectContaining({
            headers: { Authorization: result.auth.authHeader },
        }));
        expect(result.process).toBe(proc);
    });
    it("should retry on failure up to 3 times", async () => {
        const proc1 = createMockProcess();
        const proc2 = createMockProcess();
        const proc3 = createMockProcess();
        mockSpawn.mockReturnValueOnce(proc1).mockReturnValueOnce(proc2).mockReturnValueOnce(proc3);
        // First two attempts: process exits early (within the 200ms early-exit window)
        // We emit exit synchronously upon spawn via nextTick so it fires before the 200ms timer
        mockSpawn.mockImplementation(() => {
            const idx = mockSpawn.mock.calls.length;
            if (idx <= 2) {
                const p = idx === 1 ? proc1 : proc2;
                // Exit before the 200ms early-exit check timer fires
                process.nextTick(() => p.emit("exit", 1, null));
                return p;
            }
            return proc3;
        });
        // Reset to use the implementation
        mockSpawn.mockReset();
        let spawnCount = 0;
        mockSpawn.mockImplementation(() => {
            spawnCount++;
            if (spawnCount <= 2) {
                const p = spawnCount === 1 ? proc1 : proc2;
                // Exit immediately
                process.nextTick(() => p.emit("exit", 1, null));
                return p;
            }
            return proc3;
        });
        mockFetch.mockResolvedValue({ ok: true });
        const result = await spawnOpenCode(4096);
        expect(spawnCount).toBe(3);
        expect(result.process).toBe(proc3);
    }, 15000);
    it("should throw after exhausting all retries", async () => {
        let spawnCount = 0;
        mockSpawn.mockImplementation(() => {
            spawnCount++;
            const proc = createMockProcess();
            // Exit immediately on every attempt
            process.nextTick(() => proc.emit("exit", 1, null));
            return proc;
        });
        await expect(spawnOpenCode(4096)).rejects.toThrow();
        expect(spawnCount).toBe(3);
    }, 15000);
    it("should use default port 4096 when none specified", async () => {
        const proc = createMockProcess();
        mockSpawn.mockReturnValue(proc);
        mockFetch.mockResolvedValue({ ok: true });
        const result = await spawnOpenCode();
        const spawnCall = mockSpawn.mock.calls[0];
        expect(spawnCall[1]).toContain("4096");
        expect(result.auth.baseUrl).toBe("http://127.0.0.1:4096");
    });
    it("should use custom port when specified", async () => {
        const proc = createMockProcess();
        mockSpawn.mockReturnValue(proc);
        mockFetch.mockResolvedValue({ ok: true });
        const result = await spawnOpenCode(5555);
        const spawnCall = mockSpawn.mock.calls[0];
        expect(spawnCall[1]).toContain("5555");
        expect(result.auth.baseUrl).toBe("http://127.0.0.1:5555");
    });
    it("should stop process via stop function", async () => {
        const proc = createMockProcess();
        mockSpawn.mockReturnValue(proc);
        mockFetch.mockResolvedValue({ ok: true });
        const result = await spawnOpenCode(4096);
        await result.stop();
        expect(proc.kill).toHaveBeenCalledWith("SIGTERM");
    });
    it("should generate Basic auth header with opencode username", async () => {
        const proc = createMockProcess();
        mockSpawn.mockReturnValue(proc);
        mockFetch.mockResolvedValue({ ok: true });
        const result = await spawnOpenCode();
        // Decode and verify auth header format
        const encoded = result.auth.authHeader.replace("Basic ", "");
        const decoded = Buffer.from(encoded, "base64").toString();
        expect(decoded).toMatch(/^opencode:.+$/);
        expect(decoded.split(":")[1]).toBe(result.auth.password);
    });
});
//# sourceMappingURL=spawn.test.js.map