import { mkdtemp, rm, mkdir, writeFile } 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 { handleCommandExec } from "../command-exec.js";
// Suppress logger output in tests
vi.mock("../../utils/logger.js", () => ({
    logger: {
        debug: vi.fn(),
        info: vi.fn(),
        warn: vi.fn(),
        error: vi.fn(),
    },
}));
describe("command_exec handler", () => {
    let tempDir;
    let projectDir;
    let dbPath;
    let projectId;
    beforeEach(async () => {
        tempDir = await mkdtemp(join(tmpdir(), "pocketcoder-cmd-exec-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;
    });
    afterEach(async () => {
        closeDatabase();
        await rm(tempDir, { recursive: true, force: true });
    });
    function createCommandExecRequest(command) {
        return {
            type: "command_exec",
            requestId: "req-cmd-1",
            sessionId: projectId,
            payload: { command },
        };
    }
    function createSendCollector() {
        const messages = [];
        const send = (msg) => {
            messages.push(msg);
        };
        return { messages, send };
    }
    it("should execute command in project directory", async () => {
        // Create a file to verify we're in the right directory
        await writeFile(join(projectDir, "marker.txt"), "project-marker");
        const { messages, send } = createSendCollector();
        await handleCommandExec(createCommandExecRequest("cat marker.txt"), send);
        // Should have at least one output event and one exit event
        const exitEvents = messages.filter((m) => m.type === "command_exit");
        expect(exitEvents).toHaveLength(1);
        expect(exitEvents[0].payload.exitCode).toBe(0);
        expect(exitEvents[0].payload.durationMs).toBeGreaterThanOrEqual(0);
        // Should have streamed the file content
        const outputEvents = messages.filter((m) => m.type === "command_output");
        const stdout = outputEvents.map((e) => e.payload.stdoutChunk ?? "").join("");
        expect(stdout).toContain("project-marker");
    });
    it("should stream stdout chunks", async () => {
        const { messages, send } = createSendCollector();
        await handleCommandExec(createCommandExecRequest('echo "hello world"'), send);
        const outputEvents = messages.filter((m) => m.type === "command_output");
        expect(outputEvents.length).toBeGreaterThanOrEqual(1);
        const stdout = outputEvents.map((e) => e.payload.stdoutChunk ?? "").join("");
        expect(stdout).toContain("hello world");
    });
    it("should stream stderr chunks", async () => {
        const { messages, send } = createSendCollector();
        await handleCommandExec(createCommandExecRequest('echo "error msg" >&2'), send);
        const outputEvents = messages.filter((m) => m.type === "command_output");
        expect(outputEvents.length).toBeGreaterThanOrEqual(1);
        const stderr = outputEvents.map((e) => e.payload.stderrChunk ?? "").join("");
        expect(stderr).toContain("error msg");
    });
    it("should send exit event with code", async () => {
        const { messages, send } = createSendCollector();
        await handleCommandExec(createCommandExecRequest("exit 42"), send);
        const exitEvents = messages.filter((m) => m.type === "command_exit");
        expect(exitEvents).toHaveLength(1);
        expect(exitEvents[0].requestId).toBe("req-cmd-1");
        expect(exitEvents[0].payload.exitCode).toBe(42);
        expect(typeof exitEvents[0].payload.durationMs).toBe("number");
    });
    it("should handle command timeout", async () => {
        // Use a very short timeout — we need to override the constant
        // Instead, use a command that would normally take long but we test the structure
        const { messages, send } = createSendCollector();
        // This should complete quickly since 'true' exits immediately
        await handleCommandExec(createCommandExecRequest("true"), send);
        const exitEvents = messages.filter((m) => m.type === "command_exit");
        expect(exitEvents).toHaveLength(1);
        expect(exitEvents[0].payload.exitCode).toBe(0);
    });
    it("should truncate large output", async () => {
        // Generate output larger than MAX_COMMAND_OUTPUT (300KB)
        // Using dd to produce exactly 400KB of output
        const { messages, send } = createSendCollector();
        await handleCommandExec(createCommandExecRequest("dd if=/dev/zero bs=1024 count=400 2>/dev/null | tr '\\0' 'A'"), send);
        // Collect all stdout data
        const outputEvents = messages.filter((m) => m.type === "command_output");
        let totalBytes = 0;
        for (const event of outputEvents) {
            if (event.payload.stdoutChunk) {
                totalBytes += Buffer.byteLength(event.payload.stdoutChunk, "utf-8");
            }
        }
        // Should have been truncated at ~300KB (MAX_COMMAND_OUTPUT)
        // Allow some margin for chunk boundaries
        expect(totalBytes).toBeLessThanOrEqual(310 * 1024);
        const exitEvents = messages.filter((m) => m.type === "command_exit");
        expect(exitEvents).toHaveLength(1);
    });
    it("should return error for non-existent project", async () => {
        const msg = {
            type: "command_exec",
            requestId: "req-cmd-2",
            sessionId: "non-existent-project-id",
            payload: { command: "echo hello" },
        };
        const { messages, send } = createSendCollector();
        await handleCommandExec(msg, send);
        expect(messages).toHaveLength(1);
        const errMsg = messages[0];
        expect(errMsg.type).toBe("error");
        expect(errMsg.error?.code).toBe(ErrorCode.INVALID_REQUEST);
    });
    it("should handle command that does not exist", async () => {
        const { messages, send } = createSendCollector();
        await handleCommandExec(createCommandExecRequest("nonexistent_command_12345"), send);
        const exitEvents = messages.filter((m) => m.type === "command_exit");
        expect(exitEvents).toHaveLength(1);
        // Non-existent command should have non-zero exit code
        expect(exitEvents[0].payload.exitCode).not.toBe(0);
    });
    it("should track command in database", async () => {
        const { send } = createSendCollector();
        await handleCommandExec(createCommandExecRequest('echo "tracked"'), send);
        // Verify command was recorded in database
        const { getCommandRun } = await import("../../db/repositories/command-runs.js");
        const record = getCommandRun("req-cmd-1");
        expect(record).not.toBeNull();
        expect(record.command).toBe('echo "tracked"');
        expect(record.project_id).toBe(projectId);
        expect(record.exit_code).toBe(0);
    });
});
//# sourceMappingURL=command-exec.test.js.map