/**
 * @license
 * Copyright 2026 HopCode Team
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { HistoryReplayer } from './HistoryReplayer.js';
describe('HistoryReplayer', () => {
    let mockContext;
    let sendUpdateSpy;
    let replayer;
    beforeEach(() => {
        sendUpdateSpy = vi.fn().mockResolvedValue(undefined);
        const mockToolRegistry = {
            getTool: vi.fn().mockReturnValue(null),
        };
        mockContext = {
            sessionId: 'test-session-id',
            config: {
                getToolRegistry: () => mockToolRegistry,
            },
            sendUpdate: sendUpdateSpy,
        };
        replayer = new HistoryReplayer(mockContext);
    });
    const toEpochMs = (ts) => new Date(ts).getTime();
    const createUserRecord = (text) => ({
        uuid: 'user-uuid',
        parentUuid: null,
        sessionId: 'test-session',
        timestamp: new Date().toISOString(),
        type: 'user',
        cwd: '/test',
        version: '1.0.0',
        message: {
            role: 'user',
            parts: [{ text }],
        },
    });
    const createAssistantRecord = (text, thought = false) => ({
        uuid: 'assistant-uuid',
        parentUuid: 'user-uuid',
        sessionId: 'test-session',
        timestamp: new Date().toISOString(),
        type: 'assistant',
        cwd: '/test',
        version: '1.0.0',
        message: {
            role: 'model',
            parts: [{ text, thought }],
        },
    });
    const createToolResultRecord = (toolName, resultDisplay, hasError = false) => ({
        uuid: 'tool-uuid',
        parentUuid: 'assistant-uuid',
        sessionId: 'test-session',
        timestamp: new Date().toISOString(),
        type: 'tool_result',
        cwd: '/test',
        version: '1.0.0',
        message: {
            role: 'user',
            parts: [
                {
                    functionResponse: {
                        name: toolName,
                        response: { result: 'ok' },
                    },
                },
            ],
        },
        toolCallResult: {
            callId: 'call-123',
            responseParts: [],
            resultDisplay,
            error: hasError ? new Error('Tool failed') : undefined,
            errorType: undefined,
        },
    });
    describe('replay', () => {
        it('should replay empty records array', async () => {
            await replayer.replay([]);
            expect(sendUpdateSpy).not.toHaveBeenCalled();
        });
        it('should replay records in order', async () => {
            const records = [
                createUserRecord('Hello'),
                createAssistantRecord('Hi there'),
            ];
            await replayer.replay(records);
            expect(sendUpdateSpy).toHaveBeenCalledTimes(2);
            expect(sendUpdateSpy.mock.calls[0][0].sessionUpdate).toBe('user_message_chunk');
            expect(sendUpdateSpy.mock.calls[1][0].sessionUpdate).toBe('agent_message_chunk');
        });
    });
    describe('user message replay', () => {
        it('should emit user_message_chunk for user records', async () => {
            const record = createUserRecord('Hello, world!');
            const records = [record];
            await replayer.replay(records);
            expect(sendUpdateSpy).toHaveBeenCalledWith({
                sessionUpdate: 'user_message_chunk',
                content: { type: 'text', text: 'Hello, world!' },
                _meta: { timestamp: toEpochMs(record.timestamp) },
            });
        });
        it('should skip user records without message', async () => {
            const record = {
                ...createUserRecord('test'),
                message: undefined,
            };
            await replayer.replay([record]);
            expect(sendUpdateSpy).not.toHaveBeenCalled();
        });
    });
    describe('assistant message replay', () => {
        it('should emit agent_message_chunk for assistant records', async () => {
            const record = createAssistantRecord('I can help with that.');
            const records = [record];
            await replayer.replay(records);
            expect(sendUpdateSpy).toHaveBeenCalledWith({
                sessionUpdate: 'agent_message_chunk',
                content: { type: 'text', text: 'I can help with that.' },
                _meta: { timestamp: toEpochMs(record.timestamp) },
            });
        });
        it('should emit agent_thought_chunk for thought parts', async () => {
            const record = createAssistantRecord('Thinking about this...', true);
            const records = [record];
            await replayer.replay(records);
            expect(sendUpdateSpy).toHaveBeenCalledWith({
                sessionUpdate: 'agent_thought_chunk',
                content: { type: 'text', text: 'Thinking about this...' },
                _meta: { timestamp: toEpochMs(record.timestamp) },
            });
        });
        it('should handle assistant records with multiple parts', async () => {
            const record = {
                ...createAssistantRecord('First'),
                message: {
                    role: 'model',
                    parts: [
                        { text: 'First part' },
                        { text: 'Second part', thought: true },
                        { text: 'Third part' },
                    ],
                },
            };
            await replayer.replay([record]);
            expect(sendUpdateSpy).toHaveBeenCalledTimes(3);
            expect(sendUpdateSpy.mock.calls[0][0]).toEqual({
                sessionUpdate: 'agent_message_chunk',
                content: { type: 'text', text: 'First part' },
                _meta: { timestamp: toEpochMs(record.timestamp) },
            });
            expect(sendUpdateSpy.mock.calls[1][0]).toEqual({
                sessionUpdate: 'agent_thought_chunk',
                content: { type: 'text', text: 'Second part' },
                _meta: { timestamp: toEpochMs(record.timestamp) },
            });
            expect(sendUpdateSpy.mock.calls[2][0]).toEqual({
                sessionUpdate: 'agent_message_chunk',
                content: { type: 'text', text: 'Third part' },
                _meta: { timestamp: toEpochMs(record.timestamp) },
            });
        });
    });
    describe('function call replay', () => {
        it('should emit tool_call for function call parts', async () => {
            const record = {
                ...createAssistantRecord(''),
                message: {
                    role: 'model',
                    parts: [
                        {
                            functionCall: {
                                name: 'read_file',
                                args: { path: '/test.ts' },
                            },
                        },
                    ],
                },
            };
            await replayer.replay([record]);
            expect(sendUpdateSpy).toHaveBeenCalledWith(expect.objectContaining({
                sessionUpdate: 'tool_call',
                status: 'in_progress',
                title: 'read_file',
                rawInput: { path: '/test.ts' },
                _meta: {
                    toolName: 'read_file',
                    timestamp: toEpochMs(record.timestamp),
                },
            }));
        });
        it('should use function call id as callId when available', async () => {
            const record = {
                ...createAssistantRecord(''),
                message: {
                    role: 'model',
                    parts: [
                        {
                            functionCall: {
                                id: 'custom-call-id',
                                name: 'read_file',
                                args: {},
                            },
                        },
                    ],
                },
            };
            await replayer.replay([record]);
            expect(sendUpdateSpy).toHaveBeenCalledWith(expect.objectContaining({
                toolCallId: 'custom-call-id',
            }));
        });
    });
    describe('tool result replay', () => {
        it('should emit tool_call_update for tool result records', async () => {
            const record = createToolResultRecord('read_file', 'File contents here');
            const records = [record];
            await replayer.replay(records);
            expect(sendUpdateSpy).toHaveBeenCalledWith({
                sessionUpdate: 'tool_call_update',
                toolCallId: 'call-123',
                status: 'completed',
                content: [
                    {
                        type: 'content',
                        // Content comes from functionResponse.response (stringified)
                        content: { type: 'text', text: '{"result":"ok"}' },
                    },
                ],
                // resultDisplay is included as rawOutput
                rawOutput: 'File contents here',
                _meta: {
                    toolName: 'read_file',
                    timestamp: toEpochMs(record.timestamp),
                },
            });
        });
        it('should emit failed status for tool results with errors', async () => {
            const records = [createToolResultRecord('failing_tool', undefined, true)];
            await replayer.replay(records);
            expect(sendUpdateSpy).toHaveBeenCalledWith(expect.objectContaining({
                sessionUpdate: 'tool_call_update',
                status: 'failed',
            }));
        });
        it('should emit plan update for TodoWriteTool results', async () => {
            const todoDisplay = {
                type: 'todo_list',
                todos: [
                    { id: '1', content: 'Task 1', status: 'pending' },
                    { id: '2', content: 'Task 2', status: 'completed' },
                ],
            };
            const record = createToolResultRecord('todo_write', todoDisplay);
            // Override the function response name
            record.message = {
                role: 'user',
                parts: [
                    {
                        functionResponse: {
                            name: 'todo_write',
                            response: { result: 'ok' },
                        },
                    },
                ],
            };
            await replayer.replay([record]);
            expect(sendUpdateSpy).toHaveBeenCalledWith({
                sessionUpdate: 'plan',
                entries: [
                    { content: 'Task 1', priority: 'medium', status: 'pending' },
                    { content: 'Task 2', priority: 'medium', status: 'completed' },
                ],
            });
        });
        it('should use record uuid as callId when toolCallResult.callId is missing', async () => {
            const record = {
                ...createToolResultRecord('test_tool'),
                uuid: 'fallback-uuid',
                toolCallResult: {
                    callId: undefined,
                    responseParts: [],
                    resultDisplay: 'Result',
                    error: undefined,
                    errorType: undefined,
                },
            };
            await replayer.replay([record]);
            expect(sendUpdateSpy).toHaveBeenCalledWith(expect.objectContaining({
                toolCallId: 'fallback-uuid',
            }));
        });
    });
    describe('system records', () => {
        it('should skip system records', async () => {
            const systemRecord = {
                uuid: 'system-uuid',
                parentUuid: null,
                sessionId: 'test-session',
                timestamp: new Date().toISOString(),
                type: 'system',
                subtype: 'chat_compression',
                cwd: '/test',
                version: '1.0.0',
            };
            await replayer.replay([systemRecord]);
            expect(sendUpdateSpy).not.toHaveBeenCalled();
        });
    });
    describe('mixed record types', () => {
        it('should handle a complete conversation replay', async () => {
            const records = [
                createUserRecord('Read the file test.ts'),
                {
                    ...createAssistantRecord(''),
                    message: {
                        role: 'model',
                        parts: [
                            { text: "I'll read that file for you.", thought: true },
                            {
                                functionCall: {
                                    id: 'call-read',
                                    name: 'read_file',
                                    args: { path: 'test.ts' },
                                },
                            },
                        ],
                    },
                },
                createToolResultRecord('read_file', 'export const x = 1;'),
                createAssistantRecord('The file contains a simple export.'),
            ];
            await replayer.replay(records);
            // Verify order and types of updates
            const updateTypes = sendUpdateSpy.mock.calls.map((call) => call[0].sessionUpdate);
            expect(updateTypes).toEqual([
                'user_message_chunk',
                'agent_thought_chunk',
                'tool_call',
                'tool_call_update',
                'agent_message_chunk',
            ]);
        });
    });
    describe('usage metadata replay', () => {
        it('should emit usage metadata after assistant message content', async () => {
            const record = {
                uuid: 'assistant-uuid',
                parentUuid: 'user-uuid',
                sessionId: 'test-session',
                timestamp: new Date().toISOString(),
                type: 'assistant',
                cwd: '/test',
                version: '1.0.0',
                message: {
                    role: 'model',
                    parts: [{ text: 'Hello!' }],
                },
                usageMetadata: {
                    promptTokenCount: 100,
                    candidatesTokenCount: 50,
                    totalTokenCount: 150,
                },
            };
            await replayer.replay([record]);
            expect(sendUpdateSpy).toHaveBeenCalledTimes(2);
            expect(sendUpdateSpy).toHaveBeenNthCalledWith(1, {
                sessionUpdate: 'agent_message_chunk',
                content: { type: 'text', text: 'Hello!' },
                _meta: { timestamp: toEpochMs(record.timestamp) },
            });
            expect(sendUpdateSpy).toHaveBeenNthCalledWith(2, {
                sessionUpdate: 'agent_message_chunk',
                content: { type: 'text', text: '' },
                _meta: {
                    usage: {
                        inputTokens: 100,
                        outputTokens: 50,
                        totalTokens: 150,
                        thoughtTokens: undefined,
                        cachedReadTokens: undefined,
                    },
                },
            });
        });
    });
});
//# sourceMappingURL=HistoryReplayer.test.js.map