import { mkdtemp, rm, mkdir } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { ErrorCode } from "@pocketcoder/shared/constants";
import { openDatabase, closeDatabase } from "../../db/database.js";
import { createProject } from "../../db/repositories/projects.js";
import { handleAgentMessage } from "../agent-message.js";
// Suppress logger output in tests
vi.mock("../../utils/logger.js", () => ({
    logger: {
        debug: vi.fn(),
        info: vi.fn(),
        warn: vi.fn(),
        error: vi.fn(),
    },
}));
// Mock the OpenCode proxy
vi.mock("../../opencode/proxy.js", () => ({
    proxyToOpenCode: vi.fn(),
}));
import { proxyToOpenCode } from "../../opencode/proxy.js";
const mockProxyToOpenCode = vi.mocked(proxyToOpenCode);
// We need to reset the message-queue module state between tests
// Import the queue functions to clear state
import { clearAllQueues } from "../../services/message-queue.js";
describe("agent_message handler", () => {
    let tempDir;
    let projectDir;
    let dbPath;
    let projectId;
    const agentSessionId = "10ca3c7c-ece4-4c98-a251-365dad2a34ad";
    beforeEach(async () => {
        tempDir = await mkdtemp(join(tmpdir(), "pocketcoder-agent-message-test-"));
        projectDir = join(tempDir, "project");
        dbPath = join(tempDir, "daemon.db");
        await mkdir(projectDir);
        closeDatabase();
        openDatabase(dbPath);
        const project = createProject("test-project", projectDir);
        projectId = project.project_id;
        mockProxyToOpenCode.mockReset();
        clearAllQueues();
    });
    afterEach(async () => {
        closeDatabase();
        clearAllQueues();
        await rm(tempDir, { recursive: true, force: true });
    });
    function createRequest(content = "Now add tests for the component", sessionId = agentSessionId) {
        return {
            type: "agent_message",
            requestId: "req-1",
            sessionId: projectId,
            payload: { agentSessionId: sessionId, content },
        };
    }
    function createSendCollector() {
        const messages = [];
        const send = (msg) => {
            messages.push(msg);
        };
        return { messages, send };
    }
    it("should send prompt to existing session via OpenCode API", async () => {
        mockProxyToOpenCode.mockResolvedValue({
            status: 202,
            headers: {},
            body: "",
        });
        const { messages, send } = createSendCollector();
        await handleAgentMessage(createRequest(), send);
        // Wait for drain to process
        await vi.waitFor(() => {
            expect(mockProxyToOpenCode).toHaveBeenCalled();
        });
        // Should respond with started: true
        const resp = messages[0];
        expect(resp.type).toBe("agent_message_response");
        expect(resp.error).toBeUndefined();
        expect(resp.payload).toEqual({ started: true });
        // Verify proxyToOpenCode was called with correct args
        expect(mockProxyToOpenCode).toHaveBeenCalledWith({
            method: "POST",
            path: `/session/${agentSessionId}/prompt_async`,
            body: JSON.stringify({ content: "Now add tests for the component" }),
        });
    });
    it("should queue messages when busy", async () => {
        // First call: simulate a long-running process
        let resolveFirst;
        const firstCallPromise = new Promise((resolve) => {
            resolveFirst = resolve;
        });
        mockProxyToOpenCode.mockImplementationOnce(async () => {
            await firstCallPromise;
            return { status: 202, headers: {}, body: "" };
        });
        const { messages, send } = createSendCollector();
        // Send first message — starts immediately
        await handleAgentMessage(createRequest("First message"), send);
        const resp1 = messages[0];
        expect(resp1.type).toBe("agent_message_response");
        expect(resp1.payload).toEqual({ started: true });
        // Set up mock for second call
        mockProxyToOpenCode.mockImplementationOnce(async () => {
            return { status: 202, headers: {}, body: "" };
        });
        // Send second message — should be queued
        const msg2 = {
            type: "agent_message",
            requestId: "req-2",
            sessionId: projectId,
            payload: { agentSessionId, content: "Second message" },
        };
        await handleAgentMessage(msg2, send);
        const resp2 = messages[1];
        expect(resp2.type).toBe("agent_message_response");
        expect(resp2.requestId).toBe("req-2");
        expect(resp2.payload).toEqual({ queued: true });
        // Resolve first call to let the queue drain
        resolveFirst(undefined);
        // Wait for the queue to drain and second message to be processed
        await vi.waitFor(() => {
            expect(mockProxyToOpenCode).toHaveBeenCalledTimes(2);
        });
        // Second call should have been with "Second message"
        expect(mockProxyToOpenCode).toHaveBeenNthCalledWith(2, {
            method: "POST",
            path: `/session/${agentSessionId}/prompt_async`,
            body: JSON.stringify({ content: "Second message" }),
        });
    });
    it("should process queue in order", async () => {
        const processedMessages = [];
        let resolveFirst;
        const firstCallPromise = new Promise((resolve) => {
            resolveFirst = resolve;
        });
        // First call: blocks until resolved
        mockProxyToOpenCode.mockImplementationOnce(async (req) => {
            await firstCallPromise;
            const body = JSON.parse(req.body ?? "{}");
            processedMessages.push(body.content);
            return { status: 202, headers: {}, body: "" };
        });
        const { send } = createSendCollector();
        // Send first message
        await handleAgentMessage(createRequest("First"), send);
        // Mock subsequent calls
        mockProxyToOpenCode.mockImplementation(async (req) => {
            const body = JSON.parse(req.body ?? "{}");
            processedMessages.push(body.content);
            return { status: 202, headers: {}, body: "" };
        });
        // Queue multiple messages
        for (const text of ["Second", "Third", "Fourth"]) {
            const msg = {
                type: "agent_message",
                requestId: `req-${text}`,
                sessionId: projectId,
                payload: { agentSessionId, content: text },
            };
            await handleAgentMessage(msg, send);
        }
        // Resolve first to let the queue drain
        resolveFirst(undefined);
        await vi.waitFor(() => {
            expect(mockProxyToOpenCode).toHaveBeenCalledTimes(4);
        });
        // Check order
        expect(processedMessages).toEqual(["First", "Second", "Third", "Fourth"]);
    });
    it("should reject when queue full", async () => {
        // Block the first call to keep the queue busy
        let resolveFirst;
        mockProxyToOpenCode.mockImplementation(async () => {
            await new Promise((resolve) => {
                resolveFirst = resolve;
            });
            return { status: 202, headers: {}, body: "" };
        });
        const { messages, send } = createSendCollector();
        // Send first message to start processing
        await handleAgentMessage(createRequest("First"), send);
        // Fill the queue to the limit (50 messages)
        for (let i = 0; i < 50; i++) {
            const msg = {
                type: "agent_message",
                requestId: `req-${i}`,
                sessionId: projectId,
                payload: { agentSessionId, content: `Message ${i}` },
            };
            await handleAgentMessage(msg, send);
        }
        // The 51st message (beyond the limit) should be rejected
        const msg51 = {
            type: "agent_message",
            requestId: "req-overflow",
            sessionId: projectId,
            payload: { agentSessionId, content: "Overflow message" },
        };
        await handleAgentMessage(msg51, send);
        // Find the overflow response
        const overflowResp = messages.find((m) => m.requestId === "req-overflow");
        expect(overflowResp).toBeDefined();
        expect(overflowResp.error).toBeDefined();
        expect(overflowResp.error.code).toBe(ErrorCode.DAEMON_BUSY);
        expect(overflowResp.error.details).toEqual({
            queueSize: 50,
            retryAfterMs: 5000,
        });
        // Clean up: resolve the blocked call
        resolveFirst?.(undefined);
    });
    it("should error for non-existent project", async () => {
        const msg = {
            type: "agent_message",
            requestId: "req-2",
            sessionId: "non-existent-project",
            payload: { agentSessionId, content: "Hello" },
        };
        const { messages, send } = createSendCollector();
        await handleAgentMessage(msg, send);
        expect(messages).toHaveLength(1);
        const resp = messages[0];
        expect(resp.error).toBeDefined();
        expect(resp.error.code).toBe(ErrorCode.INVALID_REQUEST);
        expect(mockProxyToOpenCode).not.toHaveBeenCalled();
    });
    it("should error for empty content", async () => {
        const { messages, send } = createSendCollector();
        await handleAgentMessage(createRequest(""), send);
        expect(messages).toHaveLength(1);
        const resp = messages[0];
        expect(resp.error).toBeDefined();
        expect(resp.error.code).toBe(ErrorCode.INVALID_REQUEST);
        expect(resp.error.message).toContain("content is required");
        expect(mockProxyToOpenCode).not.toHaveBeenCalled();
    });
    it("should error for missing agentSessionId", async () => {
        const msg = {
            type: "agent_message",
            requestId: "req-3",
            sessionId: projectId,
            payload: { agentSessionId: "", content: "Hello" },
        };
        const { messages, send } = createSendCollector();
        await handleAgentMessage(msg, send);
        expect(messages).toHaveLength(1);
        const resp = messages[0];
        expect(resp.error).toBeDefined();
        expect(resp.error.code).toBe(ErrorCode.INVALID_REQUEST);
        expect(resp.error.message).toContain("agentSessionId is required");
        expect(mockProxyToOpenCode).not.toHaveBeenCalled();
    });
    it("should handle OpenCode errors gracefully", async () => {
        mockProxyToOpenCode.mockRejectedValue(new Error("Connection refused"));
        const { messages, send } = createSendCollector();
        await handleAgentMessage(createRequest(), send);
        await vi.waitFor(() => {
            expect(mockProxyToOpenCode).toHaveBeenCalled();
        });
        // Should still get the started response (error comes via SSE)
        const resp = messages[0];
        expect(resp.type).toBe("agent_message_response");
        expect(resp.payload).toEqual({ started: true });
    });
});
//# sourceMappingURL=agent-message.test.js.map