import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { EventEmitter } from 'events';
import { ClaudeBridge } from '../claude-bridge.js';
// Mock child_process.spawn
vi.mock('child_process', () => {
    const actual = vi.importActual('child_process');
    return {
        ...actual,
        spawn: vi.fn(),
    };
});
// Mock fs.existsSync to allow test cwds
vi.mock('node:fs', () => {
    const actual = vi.importActual('node:fs');
    return {
        ...actual,
        default: { ...actual.default, existsSync: vi.fn(() => true) },
        existsSync: vi.fn(() => true),
    };
});
import { spawn } from 'child_process';
const mockSpawn = vi.mocked(spawn);
function createMockProcess() {
    const proc = new EventEmitter();
    proc.stdout = new EventEmitter();
    proc.stderr = new EventEmitter();
    proc.stdin = { write: vi.fn(), end: vi.fn() };
    proc.kill = vi.fn();
    proc.pid = 12345;
    return proc;
}
/** Emit a stream-json assistant event with a text block */
function emitTextEvent(proc, text) {
    const evt = JSON.stringify({
        type: 'assistant',
        message: { content: [{ type: 'text', text }] },
    });
    proc.stdout.emit('data', Buffer.from(evt + '\n'));
}
/** Emit a stream-json assistant event with a tool_use block */
function emitToolUseEvent(proc, name, input = {}) {
    const evt = JSON.stringify({
        type: 'assistant',
        message: { content: [{ type: 'tool_use', name, input }] },
    });
    proc.stdout.emit('data', Buffer.from(evt + '\n'));
}
describe('ClaudeBridge', () => {
    let bridge;
    beforeEach(() => {
        bridge = new ClaudeBridge({ timeoutMs: 5000 });
        vi.useFakeTimers();
    });
    afterEach(() => {
        vi.useRealTimers();
        vi.restoreAllMocks();
    });
    it('should spawn claude with correct arguments and pipe prompt via stdin', () => {
        const mockProc = createMockProcess();
        mockSpawn.mockReturnValue(mockProc);
        bridge.send({
            prompt: 'Hello Claude',
            cwd: '/tmp/test-squad',
            onChunk: vi.fn(),
            onDone: vi.fn(),
            onError: vi.fn(),
        });
        expect(mockSpawn).toHaveBeenCalledWith(expect.any(String), ['-p', '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions'], expect.objectContaining({
            cwd: '/tmp/test-squad',
            stdio: ['pipe', 'pipe', 'pipe'],
        }));
        // Prompt should be written to stdin
        expect(mockProc.stdin.write).toHaveBeenCalledWith('Hello Claude');
        expect(mockProc.stdin.end).toHaveBeenCalled();
    });
    it('should add --continue flag when continueSession is true', () => {
        const mockProc = createMockProcess();
        mockSpawn.mockReturnValue(mockProc);
        bridge.send({
            prompt: 'Follow up',
            cwd: '/tmp/test-squad',
            continueSession: true,
            onChunk: vi.fn(),
            onDone: vi.fn(),
            onError: vi.fn(),
        });
        const args = mockSpawn.mock.calls[0][1];
        expect(args).toContain('--continue');
    });
    it('should add --resume flag with session ID when provided', () => {
        const mockProc = createMockProcess();
        mockSpawn.mockReturnValue(mockProc);
        bridge.send({
            prompt: 'Resume',
            cwd: '/tmp/test-squad',
            resumeSessionId: 'session-abc',
            onChunk: vi.fn(),
            onDone: vi.fn(),
            onError: vi.fn(),
        });
        const args = mockSpawn.mock.calls[0][1];
        expect(args).toContain('--resume');
        expect(args).toContain('session-abc');
    });
    it('should emit chunks from stdout (stream-json format)', () => {
        const mockProc = createMockProcess();
        mockSpawn.mockReturnValue(mockProc);
        const onChunk = vi.fn();
        const onDone = vi.fn();
        bridge.send({
            prompt: 'Hello',
            cwd: '/tmp/test-squad',
            onChunk,
            onDone,
            onError: vi.fn(),
        });
        emitTextEvent(mockProc, 'Hello ');
        emitTextEvent(mockProc, 'world');
        mockProc.emit('close', 0);
        expect(onChunk).toHaveBeenCalledTimes(2);
        expect(onChunk).toHaveBeenNthCalledWith(1, 'Hello ');
        expect(onChunk).toHaveBeenNthCalledWith(2, 'world');
        expect(onDone).toHaveBeenCalledWith('Hello world');
    });
    it('should call onError on non-zero exit', () => {
        const mockProc = createMockProcess();
        mockSpawn.mockReturnValue(mockProc);
        const onError = vi.fn();
        bridge.send({
            prompt: 'Hello',
            cwd: '/tmp/test-squad',
            onChunk: vi.fn(),
            onDone: vi.fn(),
            onError,
        });
        mockProc.stderr.emit('data', Buffer.from('Something went wrong'));
        mockProc.emit('close', 1);
        expect(onError).toHaveBeenCalledWith(expect.objectContaining({
            code: 1,
            message: expect.stringContaining('Something went wrong'),
        }));
    });
    it('should call onError on spawn error', () => {
        const mockProc = createMockProcess();
        mockSpawn.mockReturnValue(mockProc);
        const onError = vi.fn();
        bridge.send({
            prompt: 'Hello',
            cwd: '/tmp/test-squad',
            onChunk: vi.fn(),
            onDone: vi.fn(),
            onError,
        });
        mockProc.emit('error', new Error('spawn ENOENT'));
        expect(onError).toHaveBeenCalledWith(expect.objectContaining({
            message: expect.stringContaining('Claude Code CLI not available'),
        }));
    });
    it('should timeout and kill the process', () => {
        const mockProc = createMockProcess();
        mockSpawn.mockReturnValue(mockProc);
        const onError = vi.fn();
        const onDone = vi.fn();
        bridge.send({
            prompt: 'Hello',
            cwd: '/tmp/test-squad',
            onChunk: vi.fn(),
            onDone,
            onError,
        });
        emitTextEvent(mockProc, 'Partial response');
        // Advance past timeout
        vi.advanceTimersByTime(5000);
        expect(mockProc.kill).toHaveBeenCalledWith('SIGTERM');
        // Advance past SIGKILL grace period (5s)
        vi.advanceTimersByTime(5000);
        expect(mockProc.kill).toHaveBeenCalledWith('SIGKILL');
    });
    it('should report busy when a process is already running for a squad', () => {
        const mockProc = createMockProcess();
        mockSpawn.mockReturnValue(mockProc);
        const onError = vi.fn();
        bridge.send({
            prompt: 'First',
            cwd: '/tmp/squad-a',
            onChunk: vi.fn(),
            onDone: vi.fn(),
            onError: vi.fn(),
            squadName: 'squad-a',
        });
        bridge.send({
            prompt: 'Second',
            cwd: '/tmp/squad-a',
            onChunk: vi.fn(),
            onDone: vi.fn(),
            onError,
            squadName: 'squad-a',
        });
        expect(onError).toHaveBeenCalledWith(expect.objectContaining({
            message: expect.stringContaining('Pipeline is running'),
        }));
    });
    it('should call onOutputChunk with agentId, agentName and delta when all three are provided', () => {
        const mockProc = createMockProcess();
        mockSpawn.mockReturnValue(mockProc);
        const onChunk = vi.fn();
        const onOutputChunk = vi.fn();
        const onDone = vi.fn();
        bridge.send({
            prompt: 'Hello',
            cwd: '/tmp/test-squad',
            agentId: 'agent-001',
            agentName: 'Developer',
            onChunk,
            onOutputChunk,
            onDone,
            onError: vi.fn(),
        });
        emitTextEvent(mockProc, 'Hello ');
        emitTextEvent(mockProc, 'world');
        mockProc.emit('close', 0);
        expect(onChunk).toHaveBeenCalledTimes(2);
        expect(onOutputChunk).toHaveBeenCalledTimes(2);
        expect(onOutputChunk).toHaveBeenNthCalledWith(1, 'agent-001', 'Developer', 'Hello ');
        expect(onOutputChunk).toHaveBeenNthCalledWith(2, 'agent-001', 'Developer', 'world');
        expect(onDone).toHaveBeenCalledWith('Hello world');
    });
    it('should not call onOutputChunk when agentId or agentName is missing', () => {
        const mockProc = createMockProcess();
        mockSpawn.mockReturnValue(mockProc);
        const onOutputChunk = vi.fn();
        bridge.send({
            prompt: 'Hello',
            cwd: '/tmp/test-squad',
            agentId: 'agent-001',
            // agentName is intentionally missing
            onChunk: vi.fn(),
            onOutputChunk,
            onDone: vi.fn(),
            onError: vi.fn(),
        });
        emitTextEvent(mockProc, 'Some output');
        mockProc.emit('close', 0);
        expect(onOutputChunk).not.toHaveBeenCalled();
    });
    it('should not call onOutputChunk when callback is not provided', () => {
        const mockProc = createMockProcess();
        mockSpawn.mockReturnValue(mockProc);
        const onChunk = vi.fn();
        // No onOutputChunk provided - should not throw
        bridge.send({
            prompt: 'Hello',
            cwd: '/tmp/test-squad',
            agentId: 'agent-001',
            agentName: 'Developer',
            onChunk,
            onDone: vi.fn(),
            onError: vi.fn(),
        });
        emitTextEvent(mockProc, 'Some output');
        mockProc.emit('close', 0);
        expect(onChunk).toHaveBeenCalledWith('Some output');
    });
    it('should call onActivity when tool_use events arrive', () => {
        const mockProc = createMockProcess();
        mockSpawn.mockReturnValue(mockProc);
        const onActivity = vi.fn();
        bridge.send({
            prompt: 'Hello',
            cwd: '/tmp/test-squad',
            onChunk: vi.fn(),
            onDone: vi.fn(),
            onError: vi.fn(),
            onActivity,
        });
        emitToolUseEvent(mockProc, 'Read', { file_path: 'src/app.ts' });
        emitToolUseEvent(mockProc, 'Bash', { command: 'npm test' });
        mockProc.emit('close', 0);
        expect(onActivity).toHaveBeenCalledTimes(2);
        expect(onActivity).toHaveBeenNthCalledWith(1, 'Read', 'src/app.ts');
        expect(onActivity).toHaveBeenNthCalledWith(2, 'Bash', 'npm test');
    });
    it('should pass full process environment', () => {
        const mockProc = createMockProcess();
        mockSpawn.mockReturnValue(mockProc);
        bridge.send({
            prompt: 'Hello',
            cwd: '/tmp/test-squad',
            onChunk: vi.fn(),
            onDone: vi.fn(),
            onError: vi.fn(),
        });
        const spawnCall = mockSpawn.mock.calls[0];
        const opts = spawnCall[2];
        const envKeys = Object.keys(opts.env);
        expect(envKeys).toContain('PATH');
        expect(envKeys).toContain('HOME');
    });
});
//# sourceMappingURL=claude-bridge.test.js.map