import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { ClaudeSdkAdapter } from './ClaudeSdkAdapter.js';
const POLL_PROMPT = '系统提醒：当前工作已完成或运行时已进入空闲状态。请立即调用 poll_message 工具阻塞等待新消息和流程线任务提醒；收到流程任务提醒后调用 my_task 查看已由后台静默抢占的任务详情并直接处理；完成任务后调用 submit_task_result 提交结果，然后继续 poll_message。';
describe('ClaudeSdkAdapter', () => {
    afterEach(() => {
        vi.useRealTimers();
        vi.restoreAllMocks();
    });
    it('resolves claude from PATH without shelling out to which or where', () => {
        const adapter = new ClaudeSdkAdapter();
        const originalPath = process.env.PATH;
        const originalPathExt = process.env.PATHEXT;
        const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-path-'));
        const commandPath = path.join(commandDirectory, process.platform === 'win32' ? 'claude.EXE' : 'claude');
        fs.writeFileSync(commandPath, '#!/usr/bin/env node\n');
        if (process.platform !== 'win32') {
            fs.chmodSync(commandPath, 0o755);
        }
        process.env.PATH = commandDirectory;
        process.env.PATHEXT = '.EXE;.CMD';
        try {
            const result = adapter.findClaudeExecutable('claude');
            expect(result).toBe(commandPath);
        }
        finally {
            process.env.PATH = originalPath;
            process.env.PATHEXT = originalPathExt;
            fs.rmSync(commandDirectory, { recursive: true, force: true });
        }
    });
    it('extracts the executable before resolving commands that include flags', () => {
        const adapter = new ClaudeSdkAdapter();
        const originalPath = process.env.PATH;
        const originalPathExt = process.env.PATHEXT;
        const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-flags-'));
        const commandPath = path.join(commandDirectory, process.platform === 'win32' ? 'claude.EXE' : 'claude');
        const unexpectedCommandWithFlags = path.join(commandDirectory, process.platform === 'win32' ? 'claude --model sonnet.EXE' : 'claude --model sonnet');
        fs.writeFileSync(commandPath, '#!/usr/bin/env node\n');
        if (process.platform !== 'win32') {
            fs.chmodSync(commandPath, 0o755);
        }
        process.env.PATH = commandDirectory;
        process.env.PATHEXT = '.EXE;.CMD';
        try {
            const result = adapter.findClaudeExecutable('claude --model sonnet');
            expect(result).toBe(commandPath);
            expect(fs.existsSync(unexpectedCommandWithFlags)).toBe(false);
        }
        finally {
            process.env.PATH = originalPath;
            process.env.PATHEXT = originalPathExt;
            fs.rmSync(commandDirectory, { recursive: true, force: true });
        }
    });
    it.skipIf(process.platform !== 'win32')('resolves uppercase Windows wrapper extensions before returning PATH candidate', () => {
        const adapter = new ClaudeSdkAdapter();
        const originalPath = process.env.PATH;
        const originalPathExt = process.env.PATHEXT;
        const originalLocalAppData = process.env.LOCALAPPDATA;
        const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-wrapper-'));
        const localAppDataDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-localappdata-'));
        const wrapperPath = path.join(commandDirectory, 'claude.CMD');
        const cliPath = path.join(localAppDataDirectory, 'Volta', 'tools', 'shared', '@anthropic-ai', 'claude-code', 'cli.js');
        fs.mkdirSync(path.dirname(cliPath), { recursive: true });
        fs.writeFileSync(wrapperPath, 'volta run %~n0 %*\n');
        fs.writeFileSync(cliPath, '#!/usr/bin/env node\n');
        process.env.PATH = commandDirectory;
        process.env.PATHEXT = '.EXE;.CMD;.PS1';
        process.env.LOCALAPPDATA = localAppDataDirectory;
        try {
            const result = adapter.findClaudeExecutable('claude');
            expect(result).toBe(cliPath);
        }
        finally {
            process.env.PATH = originalPath;
            process.env.PATHEXT = originalPathExt;
            process.env.LOCALAPPDATA = originalLocalAppData;
            fs.rmSync(commandDirectory, { recursive: true, force: true });
            fs.rmSync(localAppDataDirectory, { recursive: true, force: true });
        }
    });
    it.skipIf(process.platform !== 'win32')('resolves npm command wrappers to the Claude Code cli.js file', () => {
        const adapter = new ClaudeSdkAdapter();
        const originalPath = process.env.PATH;
        const originalPathExt = process.env.PATHEXT;
        const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-npm-wrapper-'));
        const wrapperPath = path.join(commandDirectory, process.platform === 'win32' ? 'claude.CMD' : 'claude.ps1');
        const cliPath = path.join(commandDirectory, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
        fs.mkdirSync(path.dirname(cliPath), { recursive: true });
        fs.writeFileSync(wrapperPath, '@ECHO off\nnode "%~dp0\\node_modules\\@anthropic-ai\\claude-code\\cli.js" %*\n');
        fs.writeFileSync(cliPath, '#!/usr/bin/env node\n');
        process.env.PATH = commandDirectory;
        process.env.PATHEXT = '.EXE;.CMD;.PS1';
        try {
            const result = adapter.findClaudeExecutable('claude');
            expect(result).toBe(cliPath);
        }
        finally {
            process.env.PATH = originalPath;
            process.env.PATHEXT = originalPathExt;
            fs.rmSync(commandDirectory, { recursive: true, force: true });
        }
    });
    it('skips unresolved command wrappers instead of returning them to the SDK', () => {
        const adapter = new ClaudeSdkAdapter();
        const originalPath = process.env.PATH;
        const originalPathExt = process.env.PATHEXT;
        const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-unresolved-wrapper-'));
        const wrapperPath = path.join(commandDirectory, 'claude.CMD');
        fs.writeFileSync(wrapperPath, '@ECHO off\nnode missing-cli.js %*\n');
        process.env.PATH = commandDirectory;
        process.env.PATHEXT = '.EXE;.CMD;.PS1';
        try {
            const result = adapter.findClaudeExecutable('claude');
            expect(result).not.toBe(wrapperPath);
        }
        finally {
            process.env.PATH = originalPath;
            process.env.PATHEXT = originalPathExt;
            fs.rmSync(commandDirectory, { recursive: true, force: true });
        }
    });
    it('resolves absolute npm command wrappers before passing them to the SDK', () => {
        const adapter = new ClaudeSdkAdapter();
        const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-absolute-wrapper-'));
        const wrapperPath = path.join(commandDirectory, 'claude.cmd');
        const cliPath = path.join(commandDirectory, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
        fs.mkdirSync(path.dirname(cliPath), { recursive: true });
        fs.writeFileSync(wrapperPath, '@ECHO off\nnode "%~dp0\\node_modules\\@anthropic-ai\\claude-code\\cli.js" %*\n');
        fs.writeFileSync(cliPath, '#!/usr/bin/env node\n');
        try {
            const result = adapter.findClaudeExecutable(wrapperPath);
            expect(result).toBe(cliPath);
        }
        finally {
            fs.rmSync(commandDirectory, { recursive: true, force: true });
        }
    });
    it('does not pass unresolved absolute wrapper scripts to the SDK', () => {
        const adapter = new ClaudeSdkAdapter();
        const commandDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-absolute-unresolved-wrapper-'));
        const wrapperPath = path.join(commandDirectory, 'claude.bat');
        fs.writeFileSync(wrapperPath, '@ECHO off\nnode missing-cli.js %*\n');
        try {
            const result = adapter.findClaudeExecutable(wrapperPath);
            expect(result).toBeUndefined();
        }
        finally {
            fs.rmSync(commandDirectory, { recursive: true, force: true });
        }
    });
    it('auto-wakes Claude immediately when workflow idle commands include poll_message and my_task guidance', async () => {
        const adapter = new ClaudeSdkAdapter();
        const sentPrompts = [];
        Reflect.set(adapter, 'sessions', new Map([
            [
                'session-1',
                {
                    sessionId: 'session-1',
                    status: 'waiting_input',
                    messages: [],
                    createdAt: new Date().toISOString(),
                    totalUsage: { inputTokens: 0, outputTokens: 0 },
                },
            ],
        ]));
        const sendIdlePrompt = vi
            .spyOn(adapter, 'sendIdlePrompt')
            .mockImplementation((_sessionId, prompt) => {
            sentPrompts.push(prompt);
        });
        adapter.setIdleCommands('session-1', {
            idleInputCommand: 'system: 请保持工作循环：持续调用 poll_message 阻塞等待新消息和流程线任务提醒；收到流程任务提醒后调用 my_task 查看已由后台静默抢占的任务详情并直接处理，完成后调用 submit_task_result 提交结果，然后继续 poll_message。',
            nonInputCommand: '',
        });
        expect(sendIdlePrompt).toHaveBeenCalledTimes(1);
        expect(sentPrompts).toEqual([POLL_PROMPT]);
        expect(adapter.getSessionStatus('session-1')).toBe('waiting_input');
    });
    it('starts a blocking poll turn for poll_message-style idle commands', async () => {
        const adapter = new ClaudeSdkAdapter();
        const sentPrompts = [];
        Reflect.set(adapter, 'sessions', new Map([
            [
                'session-1',
                {
                    sessionId: 'session-1',
                    status: 'waiting_input',
                    messages: [],
                    createdAt: new Date().toISOString(),
                    totalUsage: { inputTokens: 0, outputTokens: 0 },
                },
            ],
        ]));
        vi.spyOn(adapter, 'sendIdlePrompt')
            .mockImplementation((_sessionId, prompt) => {
            sentPrompts.push(prompt);
        });
        adapter.setIdleCommands('session-1', {
            idleInputCommand: 'system: 请保持工作循环：持续调用 poll_message 阻塞等待新消息和流程线任务提醒；收到流程任务提醒后调用 my_task 查看已由后台静默抢占的任务详情并直接处理，完成后调用 submit_task_result 提交结果，然后继续 poll_message。',
            nonInputCommand: '',
        });
        expect(sentPrompts).toEqual([POLL_PROMPT]);
        expect(adapter.getSessionStatus('session-1')).toBe('waiting_input');
    });
    it('starts poll_message idle loop when idle commands are configured before startup reaches waiting_input', () => {
        const adapter = new ClaudeSdkAdapter();
        const sentPrompts = [];
        Reflect.set(adapter, 'sessions', new Map([
            [
                'session-1',
                {
                    sessionId: 'session-1',
                    status: 'starting',
                    messages: [],
                    createdAt: new Date().toISOString(),
                    totalUsage: { inputTokens: 0, outputTokens: 0 },
                },
            ],
        ]));
        vi.spyOn(adapter, 'sendIdlePrompt')
            .mockImplementation((_sessionId, prompt) => {
            sentPrompts.push(prompt);
        });
        adapter.setIdleCommands('session-1', {
            idleInputCommand: 'system: 请保持工作循环：持续调用 poll_message 阻塞等待新消息和流程线任务提醒；收到流程任务提醒后调用 my_task 查看已由后台静默抢占的任务详情并直接处理，完成后调用 submit_task_result 提交结果，然后继续 poll_message。',
            nonInputCommand: '',
        });
        expect(sentPrompts).toEqual([]);
        const session = adapter.sessions.get('session-1');
        session.status = 'waiting_input';
        adapter.startIdleDetection('session-1');
        expect(sentPrompts).toEqual([POLL_PROMPT]);
        expect(adapter.getSessionStatus('session-1')).toBe('waiting_input');
    });
    it('still auto-wakes Claude for regular idle commands', async () => {
        vi.useFakeTimers();
        const adapter = new ClaudeSdkAdapter();
        const sentPrompts = [];
        Reflect.set(adapter, 'sessions', new Map([
            [
                'session-1',
                {
                    sessionId: 'session-1',
                    status: 'waiting_input',
                    messages: [],
                    createdAt: new Date().toISOString(),
                    totalUsage: { inputTokens: 0, outputTokens: 0 },
                },
            ],
        ]));
        vi.spyOn(adapter, 'sendIdlePrompt')
            .mockImplementation((_sessionId, prompt) => {
            sentPrompts.push(prompt);
        });
        adapter.setIdleCommands('session-1', {
            idleInputCommand: 'continue',
            nonInputCommand: '',
        });
        await vi.advanceTimersByTimeAsync(1000);
        expect(sentPrompts).toHaveLength(0);
        await vi.advanceTimersByTimeAsync(600);
        expect(sentPrompts).toEqual(['continue']);
    });
    it('applies assistant message usage before final result usage is available', () => {
        const adapter = new ClaudeSdkAdapter();
        const emittedEvents = [];
        const statusChanges = [];
        const session = {
            sessionId: 'session-1',
            status: 'thinking',
            messages: [],
            createdAt: new Date().toISOString(),
            totalUsage: { inputTokens: 0, outputTokens: 0 },
        };
        vi.spyOn(adapter, 'emitEvent')
            .mockImplementation((_sessionId, event) => {
            emittedEvents.push(event);
        });
        vi.spyOn(adapter, 'emit')
            .mockImplementation((_eventName, _sessionId, status, actionInfo) => {
            statusChanges.push({ status, usage: actionInfo?.usage });
            return true;
        });
        const handler = adapter;
        handler.handleSdkMessage('session-1', session, {
            type: 'assistant',
            message: {
                id: 'msg-1',
                usage: { input_tokens: 123, output_tokens: 45 },
                content: [],
            },
        });
        handler.handleSdkMessage('session-1', session, {
            type: 'result',
            subtype: 'success',
            result: 'done',
        });
        expect(emittedEvents.find((event) => event.type === 'turn_complete')?.data?.usage).toEqual({
            inputTokens: 123,
            outputTokens: 45,
        });
        expect(statusChanges.at(-1)).toEqual({
            status: 'waiting_input',
            usage: { inputTokens: 123, outputTokens: 45 },
        });
    });
});
