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(),
    },
}));
const mockSpawnOpenCode = vi.fn();
vi.mock("../spawn.js", () => ({
    spawnOpenCode: (...args) => mockSpawnOpenCode(...args),
}));
const mockSetOpenCodeAuth = vi.fn();
const mockClearOpenCodeAuth = vi.fn();
vi.mock("../proxy.js", () => ({
    setOpenCodeAuth: (...args) => mockSetOpenCodeAuth(...args),
    clearOpenCodeAuth: () => mockClearOpenCodeAuth(),
}));
function createMockProcess() {
    const proc = new EventEmitter();
    proc.kill = vi.fn().mockImplementation(() => {
        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;
}
function createMockOpenCodeProcess(child) {
    const proc = child ?? createMockProcess();
    return {
        process: proc,
        auth: {
            password: "test-pass",
            baseUrl: "http://127.0.0.1:4096",
            authHeader: "Basic dGVzdA==",
        },
        stop: vi.fn().mockResolvedValue(undefined),
    };
}
describe("OpenCode process supervisor", () => {
    let supervise;
    let stopSupervisor;
    let getSupervisedProcess;
    let isShuttingDown;
    let getRestartAttempts;
    let resetSupervisor;
    beforeEach(async () => {
        vi.useFakeTimers();
        mockSpawnOpenCode.mockReset();
        mockSetOpenCodeAuth.mockReset();
        mockClearOpenCodeAuth.mockReset();
        const mod = await import("../supervisor.js");
        supervise = mod.supervise;
        stopSupervisor = mod.stopSupervisor;
        getSupervisedProcess = mod.getSupervisedProcess;
        isShuttingDown = mod.isShuttingDown;
        getRestartAttempts = mod.getRestartAttempts;
        resetSupervisor = mod.resetSupervisor;
        resetSupervisor();
    });
    afterEach(async () => {
        resetSupervisor();
        vi.useRealTimers();
        vi.restoreAllMocks();
    });
    it("should detect unexpected exit and schedule restart", async () => {
        const child = createMockProcess();
        const proc = createMockOpenCodeProcess(child);
        const newChild = createMockProcess();
        const newProc = createMockOpenCodeProcess(newChild);
        mockSpawnOpenCode.mockResolvedValueOnce(newProc);
        supervise(proc, { port: 4096 });
        // Simulate unexpected exit
        child.emit("exit", 1, null);
        // Should have cleared auth
        expect(mockClearOpenCodeAuth).toHaveBeenCalled();
        // Advance past backoff delay (first attempt: 2s)
        await vi.advanceTimersByTimeAsync(2_000);
        expect(mockSpawnOpenCode).toHaveBeenCalledWith(4096);
    });
    it("should restart process on unexpected exit", async () => {
        const child = createMockProcess();
        const proc = createMockOpenCodeProcess(child);
        const newChild = createMockProcess();
        const newProc = createMockOpenCodeProcess(newChild);
        mockSpawnOpenCode.mockResolvedValueOnce(newProc);
        supervise(proc);
        // Simulate unexpected exit
        child.emit("exit", 1, null);
        // Advance past backoff delay
        await vi.advanceTimersByTimeAsync(2_000);
        expect(mockSpawnOpenCode).toHaveBeenCalledTimes(1);
        expect(mockSetOpenCodeAuth).toHaveBeenCalledWith(newProc.auth);
        expect(getSupervisedProcess()).toBe(newProc);
        // Restart count should be reset on success
        expect(getRestartAttempts()).toBe(0);
    });
    it("should use exponential backoff for restart attempts", async () => {
        const child = createMockProcess();
        const proc = createMockOpenCodeProcess(child);
        // All restart attempts will fail
        mockSpawnOpenCode.mockRejectedValue(new Error("spawn failed"));
        supervise(proc);
        // First exit → attempt 1 at 2s
        child.emit("exit", 1, null);
        expect(getRestartAttempts()).toBe(1);
        await vi.advanceTimersByTimeAsync(2_000);
        expect(mockSpawnOpenCode).toHaveBeenCalledTimes(1);
        // Failure schedules another → attempt 2 at 4s
        expect(getRestartAttempts()).toBe(2);
        await vi.advanceTimersByTimeAsync(4_000);
        expect(mockSpawnOpenCode).toHaveBeenCalledTimes(2);
        // Failure schedules another → attempt 3 at 8s
        expect(getRestartAttempts()).toBe(3);
        await vi.advanceTimersByTimeAsync(8_000);
        expect(mockSpawnOpenCode).toHaveBeenCalledTimes(3);
        // attempt 4 at 16s
        expect(getRestartAttempts()).toBe(4);
    });
    it("should stop restarting during shutdown", async () => {
        const child = createMockProcess();
        const proc = createMockOpenCodeProcess(child);
        supervise(proc);
        // Stop the supervisor (enters shutdown mode)
        await stopSupervisor();
        expect(isShuttingDown()).toBe(true);
        // Emit exit after shutdown — should NOT trigger restart
        child.emit("exit", 1, null);
        await vi.advanceTimersByTimeAsync(10_000);
        expect(mockSpawnOpenCode).not.toHaveBeenCalled();
    });
    it("should cancel pending restart timer on shutdown", async () => {
        const child = createMockProcess();
        const proc = createMockOpenCodeProcess(child);
        supervise(proc);
        // Trigger exit — schedules restart
        child.emit("exit", 1, null);
        expect(getRestartAttempts()).toBe(1);
        // Stop before restart fires
        await stopSupervisor();
        // Advance past the backoff period
        await vi.advanceTimersByTimeAsync(5_000);
        // Restart should NOT have been attempted
        expect(mockSpawnOpenCode).not.toHaveBeenCalled();
    });
    it("should call onRestarted callback after successful restart", async () => {
        const child = createMockProcess();
        const proc = createMockOpenCodeProcess(child);
        const newChild = createMockProcess();
        const newProc = createMockOpenCodeProcess(newChild);
        mockSpawnOpenCode.mockResolvedValueOnce(newProc);
        const onRestarted = vi.fn();
        supervise(proc, { onRestarted });
        // Simulate unexpected exit
        child.emit("exit", 1, null);
        await vi.advanceTimersByTimeAsync(2_000);
        expect(onRestarted).toHaveBeenCalledWith(newProc.auth);
    });
    it("should log crash information with exit code and signal", async () => {
        const { logger } = await import("../../utils/logger.js");
        const child = createMockProcess();
        const proc = createMockOpenCodeProcess(child);
        mockSpawnOpenCode.mockResolvedValueOnce(createMockOpenCodeProcess());
        supervise(proc);
        // Simulate exit with code
        child.emit("exit", 137, "SIGKILL");
        expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("code=137"));
        expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("signal=SIGKILL"));
    });
    it("should stop the managed process on stopSupervisor", async () => {
        const child = createMockProcess();
        const proc = createMockOpenCodeProcess(child);
        supervise(proc);
        await stopSupervisor();
        expect(proc.stop).toHaveBeenCalled();
        expect(mockClearOpenCodeAuth).toHaveBeenCalled();
        expect(getSupervisedProcess()).toBeNull();
    });
    it("should supervise the new process after restart", async () => {
        const child = createMockProcess();
        const proc = createMockOpenCodeProcess(child);
        const newChild = createMockProcess();
        const newProc = createMockOpenCodeProcess(newChild);
        mockSpawnOpenCode.mockResolvedValueOnce(newProc);
        const secondNewChild = createMockProcess();
        const secondNewProc = createMockOpenCodeProcess(secondNewChild);
        mockSpawnOpenCode.mockResolvedValueOnce(secondNewProc);
        supervise(proc);
        // First exit
        child.emit("exit", 1, null);
        await vi.advanceTimersByTimeAsync(2_000);
        expect(getSupervisedProcess()).toBe(newProc);
        // Second exit (from the restarted process)
        newChild.emit("exit", 1, null);
        await vi.advanceTimersByTimeAsync(2_000);
        expect(getSupervisedProcess()).toBe(secondNewProc);
        expect(mockSpawnOpenCode).toHaveBeenCalledTimes(2);
    });
});
//# sourceMappingURL=supervisor.test.js.map