import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { setSseSendFn, startSseSubscription, stopSseSubscription } from "../sse.js";
vi.mock("../../utils/logger.js", () => ({
    logger: {
        debug: vi.fn(),
        info: vi.fn(),
        warn: vi.fn(),
        error: vi.fn(),
    },
}));
// Mock global fetch
const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);
function createMockAuth() {
    return {
        password: "test-password",
        baseUrl: "http://127.0.0.1:4096",
        authHeader: "Basic dGVzdDp0ZXN0",
    };
}
/**
 * Create a ReadableStream from an array of SSE chunks.
 */
function createSseStream(chunks) {
    const encoder = new TextEncoder();
    let index = 0;
    return new ReadableStream({
        pull(controller) {
            if (index < chunks.length) {
                controller.enqueue(encoder.encode(chunks[index]));
                index++;
            }
            else {
                controller.close();
            }
        },
    });
}
describe("OpenCode SSE subscription", () => {
    let sentMessages;
    beforeEach(() => {
        sentMessages = [];
        mockFetch.mockReset();
        setSseSendFn((msg) => {
            sentMessages.push(msg);
        });
        vi.useFakeTimers();
    });
    afterEach(() => {
        stopSseSubscription();
        vi.useRealTimers();
    });
    it("should connect to SSE stream with auth header", async () => {
        const auth = createMockAuth();
        const stream = createSseStream([]);
        mockFetch.mockResolvedValueOnce({
            ok: true,
            body: stream,
        });
        startSseSubscription(auth);
        await vi.advanceTimersByTimeAsync(0);
        expect(mockFetch).toHaveBeenCalledWith("http://127.0.0.1:4096/event", expect.objectContaining({
            headers: expect.objectContaining({
                Authorization: auth.authHeader,
                Accept: "text/event-stream",
            }),
        }));
    });
    it("should forward SSE events through send function", async () => {
        const auth = createMockAuth();
        const stream = createSseStream([
            'event:session.status\ndata:{"type":"session.status","properties":{"sessionID":"sess-1","status":{"type":"idle"}}}\n\n',
        ]);
        mockFetch.mockResolvedValueOnce({
            ok: true,
            body: stream,
        });
        startSseSubscription(auth);
        await vi.advanceTimersByTimeAsync(0);
        // Allow stream processing microtasks to complete
        await vi.advanceTimersByTimeAsync(0);
        expect(sentMessages.length).toBeGreaterThanOrEqual(1);
        const sseMsg = sentMessages[0];
        expect(sseMsg.type).toBe("sse_event");
        expect(sseMsg.payload.event).toBe("session.status");
        expect(sseMsg.payload.data).toContain("session.status");
        expect(sseMsg.payload.sessionId).toBe("sess-1");
    });
    it("should reconnect on disconnect", async () => {
        const auth = createMockAuth();
        // First connection: fails
        mockFetch.mockRejectedValueOnce(new Error("Connection refused"));
        // Second connection: succeeds then closes
        const stream = createSseStream([]);
        mockFetch.mockResolvedValueOnce({
            ok: true,
            body: stream,
        });
        startSseSubscription(auth);
        await vi.advanceTimersByTimeAsync(0);
        expect(mockFetch).toHaveBeenCalledTimes(1);
        // Advance timer for reconnection (1000ms base delay)
        await vi.advanceTimersByTimeAsync(1000);
        expect(mockFetch).toHaveBeenCalledTimes(2);
    });
    it("should use exponential backoff on repeated failures", async () => {
        const auth = createMockAuth();
        // Each call fails individually
        mockFetch.mockRejectedValueOnce(new Error("fail 1"));
        mockFetch.mockRejectedValueOnce(new Error("fail 2"));
        mockFetch.mockRejectedValueOnce(new Error("fail 3"));
        // Fourth call succeeds to stop reconnection cascade
        mockFetch.mockResolvedValueOnce({ ok: true, body: createSseStream([]) });
        startSseSubscription(auth);
        // First attempt
        await vi.advanceTimersByTimeAsync(0);
        expect(mockFetch).toHaveBeenCalledTimes(1);
        // First reconnect: 1s
        await vi.advanceTimersByTimeAsync(1000);
        expect(mockFetch).toHaveBeenCalledTimes(2);
        // Second reconnect: 2s
        await vi.advanceTimersByTimeAsync(2000);
        expect(mockFetch).toHaveBeenCalledTimes(3);
        // Third reconnect: 4s
        await vi.advanceTimersByTimeAsync(4000);
        expect(mockFetch).toHaveBeenCalledTimes(4);
    });
    it("should stop reconnecting when stopSseSubscription is called", async () => {
        const auth = createMockAuth();
        mockFetch.mockRejectedValueOnce(new Error("Connection refused"));
        startSseSubscription(auth);
        await vi.advanceTimersByTimeAsync(0);
        expect(mockFetch).toHaveBeenCalledTimes(1);
        // Stop before reconnect timer fires
        stopSseSubscription();
        // Advance past any reconnect timers
        await vi.advanceTimersByTimeAsync(60000);
        // Should not have made any more fetch calls
        expect(mockFetch).toHaveBeenCalledTimes(1);
    });
    it("should handle multiple SSE events in a single chunk", async () => {
        const auth = createMockAuth();
        const stream = createSseStream([
            'event:session.status\ndata:{"type":"session.status","properties":{"sessionID":"s1","status":{"type":"idle"}}}\n\nevent:file.edited\ndata:{"type":"file.edited","properties":{"file":"test.ts"}}\n\n',
        ]);
        mockFetch.mockResolvedValueOnce({
            ok: true,
            body: stream,
        });
        startSseSubscription(auth);
        await vi.advanceTimersByTimeAsync(0);
        await vi.advanceTimersByTimeAsync(0);
        expect(sentMessages.length).toBeGreaterThanOrEqual(2);
        const msg1 = sentMessages[0];
        expect(msg1.payload.event).toBe("session.status");
        expect(msg1.payload.sessionId).toBe("s1");
        const msg2 = sentMessages[1];
        expect(msg2.payload.event).toBe("file.edited");
    });
    it("should handle events split across chunks", async () => {
        const auth = createMockAuth();
        const stream = createSseStream([
            "event:session.status\n",
            'data:{"type":"session.status","properties":{"sessionID":"s1","status":{"type":"busy"}}}\n\n',
        ]);
        mockFetch.mockResolvedValueOnce({
            ok: true,
            body: stream,
        });
        startSseSubscription(auth);
        await vi.advanceTimersByTimeAsync(0);
        await vi.advanceTimersByTimeAsync(0);
        await vi.advanceTimersByTimeAsync(0);
        expect(sentMessages.length).toBeGreaterThanOrEqual(1);
        const msg = sentMessages[0];
        expect(msg.payload.event).toBe("session.status");
    });
    it("should handle non-JSON data gracefully", async () => {
        const auth = createMockAuth();
        const stream = createSseStream(["event:heartbeat\ndata:ping\n\n"]);
        mockFetch.mockResolvedValueOnce({
            ok: true,
            body: stream,
        });
        startSseSubscription(auth);
        await vi.advanceTimersByTimeAsync(0);
        await vi.advanceTimersByTimeAsync(0);
        expect(sentMessages.length).toBeGreaterThanOrEqual(1);
        const msg = sentMessages[0];
        expect(msg.payload.event).toBe("heartbeat");
        expect(msg.payload.data).toBe("ping");
        expect(msg.payload.sessionId).toBeUndefined();
    });
    it("should not forward events when no send function is set", async () => {
        setSseSendFn(null);
        const auth = createMockAuth();
        const stream = createSseStream(["event:test\ndata:hello\n\n"]);
        mockFetch.mockResolvedValueOnce({
            ok: true,
            body: stream,
        });
        // Should not throw
        startSseSubscription(auth);
        await vi.advanceTimersByTimeAsync(0);
        await vi.advanceTimersByTimeAsync(0);
        // sendFn is null, so no messages should be sent (no crash)
        expect(sentMessages).toHaveLength(0);
    });
    it("should be idempotent when starting multiple times", async () => {
        const auth = createMockAuth();
        const stream = createSseStream([]);
        mockFetch.mockResolvedValueOnce({
            ok: true,
            body: stream,
        });
        startSseSubscription(auth);
        startSseSubscription(auth); // Second call should be no-op
        await vi.advanceTimersByTimeAsync(0);
        expect(mockFetch).toHaveBeenCalledTimes(1);
    });
    it("should handle HTTP error response and reconnect", async () => {
        const auth = createMockAuth();
        // First call: 500 error
        mockFetch.mockResolvedValueOnce({
            ok: false,
            status: 500,
            statusText: "Internal Server Error",
            body: null,
        });
        // Second call: success
        const stream = createSseStream([]);
        mockFetch.mockResolvedValueOnce({
            ok: true,
            body: stream,
        });
        startSseSubscription(auth);
        // Let the first fetch resolve (HTTP 500)
        await vi.advanceTimersByTimeAsync(0);
        expect(mockFetch).toHaveBeenCalledTimes(1);
        // Reconnect after backoff
        await vi.advanceTimersByTimeAsync(1000);
        expect(mockFetch).toHaveBeenCalledTimes(2);
    });
});
//# sourceMappingURL=sse.test.js.map