import { afterEach, describe, expect, it, vi } from 'vitest';
import { CodexSdkAdapter } from './CodexSdkAdapter.js';
let lastThread = null;
let lastCodexOptions;
let lastThreadOptions;
let runMessages = [];
let nextThreadPendingRun = null;
const POLL_PROMPT = '系统提醒：当前工作已完成或运行时已进入空闲状态。请立即调用 poll_message 工具阻塞等待新消息和流程线任务提醒；收到流程任务提醒后调用 my_task 查看详情，按需调用 start_task 开始处理；如果 start_task 失败且提示任务已被其他实例领取，请不要停下，继续调用 poll_message 等待新消息；完成任务后调用 submit_task_result 提交结果，然后继续 poll_message。';
class MockThread {
    constructor() {
        this.id = null;
        this.events = [];
        this.rejectOnRun = null;
        this.waitForAbort = false;
        this.pendingRun = null;
    }
    async runStreamed(_message, options) {
        runMessages.push(_message);
        this.id = 'codex-thread-1';
        if (this.rejectOnRun) {
            throw this.rejectOnRun;
        }
        if (this.waitForAbort) {
            await new Promise((_resolve, reject) => {
                options?.signal?.addEventListener('abort', () => reject(new Error('aborted')), { once: true });
            });
        }
        if (this.pendingRun) {
            await this.pendingRun;
        }
        const events = this.events;
        return {
            events: (async function* streamEvents() {
                for (const event of events) {
                    yield event;
                }
            })(),
        };
    }
}
vi.mock('@openai/codex-sdk', () => ({
    Codex: vi.fn().mockImplementation((options) => {
        lastCodexOptions = options;
        return {
            startThread: vi.fn().mockImplementation((options) => {
                lastThreadOptions = options;
                lastThread = new MockThread();
                lastThread.pendingRun = nextThreadPendingRun;
                nextThreadPendingRun = null;
                return lastThread;
            }),
            resumeThread: vi.fn().mockImplementation((_id, options) => {
                lastThreadOptions = options;
                lastThread = new MockThread();
                lastThread.pendingRun = nextThreadPendingRun;
                nextThreadPendingRun = null;
                return lastThread;
            }),
        };
    }),
}));
afterEach(() => {
    vi.useRealTimers();
    lastThread = null;
    lastCodexOptions = undefined;
    lastThreadOptions = undefined;
    runMessages = [];
    nextThreadPendingRun = null;
});
describe('CodexSdkAdapter', () => {
    it('should expose codex provider identity', () => {
        const adapter = new CodexSdkAdapter();
        expect(adapter.providerId).toBe('codex');
        expect(adapter.displayName).toBe('Codex');
    });
    it('should start with no active sessions', () => {
        const adapter = new CodexSdkAdapter();
        expect(adapter.hasSession('missing-session')).toBe(false);
        expect(adapter.getSessionStatus('missing-session')).toBeUndefined();
    });
    it('maps streamed text and turn completion into conversation state', async () => {
        const adapter = new CodexSdkAdapter();
        await adapter.startSession('session-1', {
            command: 'codex',
            workingDirectory: process.cwd(),
            autoAccept: true,
        });
        if (!lastThread)
            throw new Error('Mock Codex thread was not created');
        lastThread.events = [
            { type: 'thread.started', thread_id: 'codex-thread-1' },
            { type: 'turn.started' },
            { type: 'item.completed', item: { id: 'msg-1', type: 'agent_message', text: 'hello from codex' } },
            {
                type: 'turn.completed',
                usage: {
                    input_tokens: 3,
                    cached_input_tokens: 0,
                    output_tokens: 4,
                    reasoning_output_tokens: 0,
                },
            },
        ];
        await adapter.sendMessage('session-1', 'hello');
        expect(adapter.getProviderSessionId('session-1')).toBe('codex-thread-1');
        expect(adapter.getSessionStatus('session-1')).toBe('waiting_input');
        expect(adapter.getConversation('session-1').map((message) => message.role)).toEqual(['user', 'assistant']);
    });
    it('does not emit waiting_input after terminating an aborted turn', async () => {
        const adapter = new CodexSdkAdapter();
        const statuses = [];
        adapter.on('status-change', (_sessionId, status) => {
            statuses.push(status);
        });
        await adapter.startSession('session-1', {
            command: 'codex',
            workingDirectory: process.cwd(),
            autoAccept: true,
        });
        if (!lastThread)
            throw new Error('Mock Codex thread was not created');
        lastThread.waitForAbort = true;
        const sendPromise = adapter.sendMessage('session-1', 'long running task');
        await adapter.terminateSession('session-1');
        await sendPromise;
        expect(statuses.at(-1)).toBe('terminated');
        const terminatedIndex = statuses.lastIndexOf('terminated');
        expect(statuses.slice(terminatedIndex + 1)).not.toContain('waiting_input');
        expect(adapter.hasSession('session-1')).toBe(false);
    });
    it('starts a Codex session without waiting for the initial prompt turn to finish', async () => {
        const adapter = new CodexSdkAdapter();
        let releaseRun;
        nextThreadPendingRun = new Promise((resolve) => {
            releaseRun = resolve;
        });
        await adapter.startSession('session-1', {
            command: 'codex',
            workingDirectory: process.cwd(),
            autoAccept: true,
            initialPrompt: 'boot prompt',
        });
        if (!lastThread)
            throw new Error('Mock Codex thread was not created');
        await new Promise((resolve) => queueMicrotask(resolve));
        expect(adapter.hasSession('session-1')).toBe(true);
        expect(runMessages).toContain('boot prompt');
        releaseRun?.();
    });
    it('waits before sending idle commands after an already idle start', async () => {
        vi.useFakeTimers();
        const adapter = new CodexSdkAdapter();
        await adapter.startSession('session-1', {
            command: 'codex',
            workingDirectory: process.cwd(),
            autoAccept: true,
        });
        if (!lastThread)
            throw new Error('Mock Codex thread was not created');
        lastThread.events = [
            {
                type: 'turn.completed',
                usage: {
                    input_tokens: 1,
                    cached_input_tokens: 0,
                    output_tokens: 1,
                    reasoning_output_tokens: 0,
                },
            },
        ];
        adapter.setIdleCommands('session-1', {
            idleInputCommand: 'continue',
            nonInputCommand: '',
        });
        await vi.advanceTimersByTimeAsync(1000);
        expect(adapter.getConversation('session-1').some((message) => message.content === 'continue')).toBe(false);
        await vi.advanceTimersByTimeAsync(600);
        expect(runMessages).toContain('continue');
        expect(adapter.getConversation('session-1').some((message) => message.content === 'continue')).toBe(false);
    });
    it('starts a blocking poll turn for poll_message-style idle commands', async () => {
        vi.useFakeTimers();
        const adapter = new CodexSdkAdapter();
        await adapter.startSession('session-1', {
            command: 'codex',
            workingDirectory: process.cwd(),
            autoAccept: true,
        });
        if (!lastThread)
            throw new Error('Mock Codex thread was not created');
        lastThread.events = [
            {
                type: 'turn.completed',
                usage: {
                    input_tokens: 1,
                    cached_input_tokens: 0,
                    output_tokens: 1,
                    reasoning_output_tokens: 0,
                },
            },
        ];
        adapter.setIdleCommands('session-1', {
            idleInputCommand: 'system: 请保持工作循环：持续调用 poll_message 阻塞等待新消息和流程线任务提醒；收到流程任务提醒后调用 my_task 查看详情，按需调用 start_task 开始处理，完成后调用 submit_task_result 提交结果，然后继续 poll_message。',
            nonInputCommand: '',
        });
        await vi.advanceTimersByTimeAsync(1499);
        expect(runMessages).toEqual([]);
        await vi.advanceTimersByTimeAsync(1);
        expect(runMessages).toEqual([POLL_PROMPT]);
        expect(adapter.getSessionStatus('session-1')).toBe('waiting_input');
        const userMessages = adapter.getConversation('session-1').filter((message) => message.role === 'user');
        expect(userMessages).toHaveLength(0);
    });
    it('does not start duplicate poll turns while a Codex turn is already running', async () => {
        const adapter = new CodexSdkAdapter();
        let releaseRun;
        nextThreadPendingRun = new Promise((resolve) => {
            releaseRun = resolve;
        });
        await adapter.startSession('session-1', {
            command: 'codex',
            workingDirectory: process.cwd(),
            autoAccept: true,
        });
        const sendPromise = adapter.sendMessage('session-1', 'long task');
        adapter.setIdleCommands('session-1', {
            idleInputCommand: 'poll_message',
            nonInputCommand: '',
        });
        await new Promise((resolve) => queueMicrotask(resolve));
        expect(runMessages).toEqual(['long task']);
        releaseRun?.();
        await sendPromise;
    });
    it('maps codex command flags into SDK thread options instead of codexPathOverride', async () => {
        const adapter = new CodexSdkAdapter();
        await adapter.startSession('session-1', {
            command: 'codex --model gpt-5.5 --sandbox workspace-write --skip-git-repo-check --config model_reasoning_effort="high"',
            workingDirectory: process.cwd(),
            autoAccept: true,
        });
        expect(lastCodexOptions?.codexPathOverride).toBeUndefined();
        expect(lastThreadOptions).toMatchObject({
            model: 'gpt-5.5',
            sandboxMode: 'workspace-write',
            skipGitRepoCheck: true,
            modelReasoningEffort: 'high',
        });
    });
    it('treats npx codex command as the bundled SDK codex binary with parsed flags', async () => {
        const adapter = new CodexSdkAdapter();
        await adapter.startSession('session-1', {
            command: 'npx codex --model gpt-5-codex',
            workingDirectory: process.cwd(),
            autoAccept: true,
        });
        expect(lastCodexOptions?.codexPathOverride).toBeUndefined();
        expect(lastThreadOptions?.model).toBe('gpt-5-codex');
    });
    it('uses only a custom executable path as codexPathOverride', async () => {
        const adapter = new CodexSdkAdapter();
        await adapter.startSession('session-1', {
            command: 'C:/Tools/codex.exe --model gpt-5-codex',
            workingDirectory: process.cwd(),
            autoAccept: true,
        });
        expect(lastCodexOptions?.codexPathOverride).toBe('C:/Tools/codex.exe');
        expect(lastThreadOptions?.model).toBe('gpt-5-codex');
    });
});
