/**
 * @license
 * Copyright 2026 HopCode Team Team
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { HopCodeEventType } from '@hoptrendy/hopcode-core';
import { StreamJsonOutputAdapter } from './StreamJsonOutputAdapter.js';
function createMockConfig() {
    return {
        getSessionId: vi.fn().mockReturnValue('test-session-id'),
        getModel: vi.fn().mockReturnValue('test-model'),
    };
}
describe('StreamJsonOutputAdapter', () => {
    let adapter;
    let mockConfig;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    let stdoutWriteSpy;
    beforeEach(() => {
        mockConfig = createMockConfig();
        stdoutWriteSpy = vi
            .spyOn(process.stdout, 'write')
            .mockImplementation(() => true);
    });
    afterEach(() => {
        stdoutWriteSpy.mockRestore();
    });
    describe('with partial messages enabled', () => {
        beforeEach(() => {
            adapter = new StreamJsonOutputAdapter(mockConfig, true);
        });
        describe('startAssistantMessage', () => {
            it('should reset state for new message', () => {
                adapter.startAssistantMessage();
                adapter.processEvent({
                    type: HopCodeEventType.Content,
                    value: 'First',
                });
                adapter.finalizeAssistantMessage();
                adapter.startAssistantMessage();
                adapter.processEvent({
                    type: HopCodeEventType.Content,
                    value: 'Second',
                });
                const message = adapter.finalizeAssistantMessage();
                expect(message.message.content[0]).toMatchObject({
                    type: 'text',
                    text: 'Second',
                });
            });
        });
        describe('processEvent with stream events', () => {
            beforeEach(() => {
                adapter.startAssistantMessage();
            });
            it('should emit stream events for text deltas', () => {
                adapter.processEvent({
                    type: HopCodeEventType.Content,
                    value: 'Hello',
                });
                const calls = stdoutWriteSpy.mock.calls;
                expect(calls.length).toBeGreaterThan(0);
                const deltaEventCall = calls.find((call) => {
                    try {
                        const parsed = JSON.parse(call[0]);
                        return (parsed.type === 'stream_event' &&
                            parsed.event.type === 'content_block_delta');
                    }
                    catch {
                        return false;
                    }
                });
                expect(deltaEventCall).toBeDefined();
                const parsed = JSON.parse(deltaEventCall[0]);
                expect(parsed.event.type).toBe('content_block_delta');
                expect(parsed.event.delta).toMatchObject({
                    type: 'text_delta',
                    text: 'Hello',
                });
            });
            it('should emit message_start event on first content', () => {
                adapter.processEvent({
                    type: HopCodeEventType.Content,
                    value: 'First',
                });
                const calls = stdoutWriteSpy.mock.calls;
                const messageStartCall = calls.find((call) => {
                    try {
                        const parsed = JSON.parse(call[0]);
                        return (parsed.type === 'stream_event' &&
                            parsed.event.type === 'message_start');
                    }
                    catch {
                        return false;
                    }
                });
                expect(messageStartCall).toBeDefined();
            });
            it('should emit content_block_start for new blocks', () => {
                adapter.processEvent({
                    type: HopCodeEventType.Content,
                    value: 'Text',
                });
                const calls = stdoutWriteSpy.mock.calls;
                const blockStartCall = calls.find((call) => {
                    try {
                        const parsed = JSON.parse(call[0]);
                        return (parsed.type === 'stream_event' &&
                            parsed.event.type === 'content_block_start');
                    }
                    catch {
                        return false;
                    }
                });
                expect(blockStartCall).toBeDefined();
            });
            it('should emit thinking delta events', () => {
                adapter.processEvent({
                    type: HopCodeEventType.Thought,
                    value: {
                        subject: 'Planning',
                        description: 'Thinking',
                    },
                });
                const calls = stdoutWriteSpy.mock.calls;
                const deltaCall = calls.find((call) => {
                    try {
                        const parsed = JSON.parse(call[0]);
                        return (parsed.type === 'stream_event' &&
                            parsed.event.type === 'content_block_delta' &&
                            parsed.event.delta.type === 'thinking_delta');
                    }
                    catch {
                        return false;
                    }
                });
                expect(deltaCall).toBeDefined();
            });
            it('should emit message_stop on finalization', () => {
                adapter.processEvent({
                    type: HopCodeEventType.Content,
                    value: 'Text',
                });
                adapter.finalizeAssistantMessage();
                const calls = stdoutWriteSpy.mock.calls;
                const messageStopCall = calls.find((call) => {
                    try {
                        const parsed = JSON.parse(call[0]);
                        return (parsed.type === 'stream_event' &&
                            parsed.event.type === 'message_stop');
                    }
                    catch {
                        return false;
                    }
                });
                expect(messageStopCall).toBeDefined();
            });
        });
    });
    describe('with partial messages disabled', () => {
        beforeEach(() => {
            adapter = new StreamJsonOutputAdapter(mockConfig, false);
        });
        it('should not emit stream events', () => {
            adapter.startAssistantMessage();
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'Text',
            });
            const calls = stdoutWriteSpy.mock.calls;
            const streamEventCall = calls.find((call) => {
                try {
                    const parsed = JSON.parse(call[0]);
                    return parsed.type === 'stream_event';
                }
                catch {
                    return false;
                }
            });
            expect(streamEventCall).toBeUndefined();
        });
        it('should still emit final assistant message', () => {
            adapter.startAssistantMessage();
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'Text',
            });
            adapter.finalizeAssistantMessage();
            const calls = stdoutWriteSpy.mock.calls;
            const assistantCall = calls.find((call) => {
                try {
                    const parsed = JSON.parse(call[0]);
                    return parsed.type === 'assistant';
                }
                catch {
                    return false;
                }
            });
            expect(assistantCall).toBeDefined();
        });
    });
    describe('processEvent', () => {
        beforeEach(() => {
            adapter = new StreamJsonOutputAdapter(mockConfig, false);
            adapter.startAssistantMessage();
        });
        it('should append text content from Content events', () => {
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'Hello',
            });
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: ' World',
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.content).toHaveLength(1);
            expect(message.message.content[0]).toMatchObject({
                type: 'text',
                text: 'Hello World',
            });
        });
        it('should append citation content from Citation events', () => {
            adapter.processEvent({
                type: HopCodeEventType.Citation,
                value: 'Citation text',
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.content[0]).toMatchObject({
                type: 'text',
                text: expect.stringContaining('Citation text'),
            });
        });
        it('should ignore non-string citation values', () => {
            adapter.processEvent({
                type: HopCodeEventType.Citation,
                value: 123,
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.content).toHaveLength(0);
        });
        it('should append thinking from Thought events', () => {
            adapter.processEvent({
                type: HopCodeEventType.Thought,
                value: {
                    subject: 'Planning',
                    description: 'Thinking about the task',
                },
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.content).toHaveLength(1);
            expect(message.message.content[0]).toMatchObject({
                type: 'thinking',
                thinking: 'Planning: Thinking about the task',
                signature: 'Planning',
            });
        });
        it('should handle thinking with only subject', () => {
            adapter.processEvent({
                type: HopCodeEventType.Thought,
                value: {
                    subject: 'Planning',
                    description: '',
                },
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.content[0]).toMatchObject({
                type: 'thinking',
                signature: 'Planning',
            });
        });
        it('should preserve whitespace in thinking content (issue #1356)', () => {
            adapter.processEvent({
                type: HopCodeEventType.Thought,
                value: {
                    subject: '',
                    description: 'The user just said "Hello"',
                },
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.content).toHaveLength(1);
            const block = message.message.content[0];
            expect(block.type).toBe('thinking');
            expect(block.thinking).toBe('The user just said "Hello"');
            // Verify spaces are preserved
            expect(block.thinking).toContain('user just');
            expect(block.thinking).not.toContain('userjust');
        });
        it('should preserve whitespace when streaming multiple thinking fragments (issue #1356)', () => {
            // Simulate streaming thinking content in multiple events
            adapter.processEvent({
                type: HopCodeEventType.Thought,
                value: {
                    subject: '',
                    description: 'The user just',
                },
            });
            adapter.processEvent({
                type: HopCodeEventType.Thought,
                value: {
                    subject: '',
                    description: ' said "Hello"',
                },
            });
            adapter.processEvent({
                type: HopCodeEventType.Thought,
                value: {
                    subject: '',
                    description: '. This is a simple greeting',
                },
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.content).toHaveLength(1);
            const block = message.message.content[0];
            expect(block.thinking).toBe('The user just said "Hello". This is a simple greeting');
            // Verify specific spaces are preserved
            expect(block.thinking).toContain('user just ');
            expect(block.thinking).toContain(' said');
            expect(block.thinking).not.toContain('userjust');
            expect(block.thinking).not.toContain('justsaid');
        });
        it('should append tool use from ToolCallRequest events', () => {
            adapter.processEvent({
                type: HopCodeEventType.ToolCallRequest,
                value: {
                    callId: 'tool-call-1',
                    name: 'test_tool',
                    args: { param1: 'value1' },
                    isClientInitiated: false,
                    prompt_id: 'prompt-1',
                },
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.content).toHaveLength(1);
            expect(message.message.content[0]).toMatchObject({
                type: 'tool_use',
                id: 'tool-call-1',
                name: 'test_tool',
                input: { param1: 'value1' },
            });
        });
        it('should set stop_reason to tool_use when message contains only tool_use blocks', () => {
            adapter.processEvent({
                type: HopCodeEventType.ToolCallRequest,
                value: {
                    callId: 'tool-call-1',
                    name: 'test_tool',
                    args: { param1: 'value1' },
                    isClientInitiated: false,
                    prompt_id: 'prompt-1',
                },
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.stop_reason).toBe('tool_use');
        });
        it('should set stop_reason to null when message contains text blocks', () => {
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'Some text',
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.stop_reason).toBeNull();
        });
        it('should set stop_reason to null when message contains thinking blocks', () => {
            adapter.processEvent({
                type: HopCodeEventType.Thought,
                value: {
                    subject: 'Planning',
                    description: 'Thinking about the task',
                },
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.stop_reason).toBeNull();
        });
        it('should set stop_reason to tool_use when message contains multiple tool_use blocks', () => {
            adapter.processEvent({
                type: HopCodeEventType.ToolCallRequest,
                value: {
                    callId: 'tool-call-1',
                    name: 'test_tool_1',
                    args: { param1: 'value1' },
                    isClientInitiated: false,
                    prompt_id: 'prompt-1',
                },
            });
            adapter.processEvent({
                type: HopCodeEventType.ToolCallRequest,
                value: {
                    callId: 'tool-call-2',
                    name: 'test_tool_2',
                    args: { param2: 'value2' },
                    isClientInitiated: false,
                    prompt_id: 'prompt-1',
                },
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.content).toHaveLength(2);
            expect(message.message.content.every((block) => block.type === 'tool_use')).toBe(true);
            expect(message.message.stop_reason).toBe('tool_use');
        });
        it('should update usage from Finished event', () => {
            const usageMetadata = {
                promptTokenCount: 100,
                candidatesTokenCount: 50,
                cachedContentTokenCount: 10,
                totalTokenCount: 160,
            };
            adapter.processEvent({
                type: HopCodeEventType.Finished,
                value: {
                    reason: undefined,
                    usageMetadata,
                },
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.usage).toMatchObject({
                input_tokens: 100,
                output_tokens: 50,
                cache_read_input_tokens: 10,
                total_tokens: 160,
            });
        });
        it('should ignore events after finalization', () => {
            adapter.finalizeAssistantMessage();
            const originalContent = adapter.finalizeAssistantMessage().message.content;
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'Should be ignored',
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.content).toEqual(originalContent);
        });
    });
    describe('finalizeAssistantMessage', () => {
        beforeEach(() => {
            adapter = new StreamJsonOutputAdapter(mockConfig, false);
            adapter.startAssistantMessage();
        });
        it('should build and emit a complete assistant message', () => {
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'Test response',
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.type).toBe('assistant');
            expect(message.uuid).toBeTruthy();
            expect(message.session_id).toBe('test-session-id');
            expect(message.parent_tool_use_id).toBeNull();
            expect(message.message.role).toBe('assistant');
            expect(message.message.model).toBe('test-model');
            expect(message.message.content).toHaveLength(1);
        });
        it('should emit message to stdout immediately', () => {
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'Test',
            });
            stdoutWriteSpy.mockClear();
            adapter.finalizeAssistantMessage();
            expect(stdoutWriteSpy).toHaveBeenCalled();
            const output = stdoutWriteSpy.mock.calls[0][0];
            const parsed = JSON.parse(output);
            expect(parsed.type).toBe('assistant');
        });
        it('should store message in lastAssistantMessage', () => {
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'Test',
            });
            const message = adapter.finalizeAssistantMessage();
            // Access protected property for testing
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            expect(adapter.lastAssistantMessage).toEqual(message);
        });
        it('should return same message on subsequent calls', () => {
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'Test',
            });
            const message1 = adapter.finalizeAssistantMessage();
            const message2 = adapter.finalizeAssistantMessage();
            expect(message1).toEqual(message2);
        });
        it('should split different block types into separate assistant messages', () => {
            stdoutWriteSpy.mockClear();
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'Text',
            });
            adapter.processEvent({
                type: HopCodeEventType.Thought,
                value: { subject: 'Thinking', description: 'Thought' },
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.content).toHaveLength(1);
            expect(message.message.content[0].type).toBe('thinking');
            const assistantMessages = stdoutWriteSpy.mock.calls
                .map((call) => JSON.parse(call[0]))
                .filter((payload) => {
                if (typeof payload !== 'object' ||
                    payload === null ||
                    !('type' in payload) ||
                    payload.type !== 'assistant' ||
                    !('message' in payload)) {
                    return false;
                }
                const message = payload.message;
                if (typeof message !== 'object' ||
                    message === null ||
                    !('content' in message)) {
                    return false;
                }
                const content = message.content;
                return (Array.isArray(content) &&
                    content.length > 0 &&
                    content.every((block) => typeof block === 'object' &&
                        block !== null &&
                        'type' in block));
            });
            expect(assistantMessages).toHaveLength(2);
            const observedTypes = assistantMessages.map((payload) => payload.message.content[0]?.type ?? '');
            expect(observedTypes).toEqual(['text', 'thinking']);
            for (const payload of assistantMessages) {
                const uniqueTypes = new Set(payload.message.content.map((block) => block.type));
                expect(uniqueTypes.size).toBeLessThanOrEqual(1);
            }
        });
        it('should throw if message not started', () => {
            adapter = new StreamJsonOutputAdapter(mockConfig, false);
            expect(() => adapter.finalizeAssistantMessage()).toThrow('Message not started');
        });
        it('should not emit empty assistant message when started but no content processed', () => {
            stdoutWriteSpy.mockClear();
            adapter.finalizeAssistantMessage();
            const assistantCalls = stdoutWriteSpy.mock.calls.filter((call) => {
                try {
                    const parsed = JSON.parse(call[0]);
                    return parsed.type === 'assistant';
                }
                catch {
                    return false;
                }
            });
            expect(assistantCalls).toHaveLength(0);
        });
    });
    describe('emitResult', () => {
        beforeEach(() => {
            adapter = new StreamJsonOutputAdapter(mockConfig, false);
            adapter.startAssistantMessage();
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'Response text',
            });
            adapter.finalizeAssistantMessage();
        });
        it('should emit success result immediately', () => {
            stdoutWriteSpy.mockClear();
            adapter.emitResult({
                isError: false,
                durationMs: 1000,
                apiDurationMs: 800,
                numTurns: 1,
            });
            expect(stdoutWriteSpy).toHaveBeenCalled();
            const output = stdoutWriteSpy.mock.calls[0][0];
            const parsed = JSON.parse(output);
            expect(parsed.type).toBe('result');
            expect(parsed.is_error).toBe(false);
            expect(parsed.subtype).toBe('success');
            expect(parsed.result).toBe('Response text');
            expect(parsed.duration_ms).toBe(1000);
            expect(parsed.num_turns).toBe(1);
        });
        it('should emit error result', () => {
            stdoutWriteSpy.mockClear();
            adapter.emitResult({
                isError: true,
                errorMessage: 'Test error',
                durationMs: 500,
                apiDurationMs: 300,
                numTurns: 1,
            });
            const output = stdoutWriteSpy.mock.calls[0][0];
            const parsed = JSON.parse(output);
            expect(parsed.is_error).toBe(true);
            expect(parsed.subtype).toBe('error_during_execution');
            expect(parsed.error?.message).toBe('Test error');
        });
        it('should use provided summary over extracted text', () => {
            stdoutWriteSpy.mockClear();
            adapter.emitResult({
                isError: false,
                summary: 'Custom summary',
                durationMs: 1000,
                apiDurationMs: 800,
                numTurns: 1,
            });
            const output = stdoutWriteSpy.mock.calls[0][0];
            const parsed = JSON.parse(output);
            expect(parsed.result).toBe('Custom summary');
        });
        it('should include usage information', () => {
            const usage = {
                input_tokens: 100,
                output_tokens: 50,
                total_tokens: 150,
            };
            stdoutWriteSpy.mockClear();
            adapter.emitResult({
                isError: false,
                usage,
                durationMs: 1000,
                apiDurationMs: 800,
                numTurns: 1,
            });
            const output = stdoutWriteSpy.mock.calls[0][0];
            const parsed = JSON.parse(output);
            expect(parsed.usage).toEqual(usage);
        });
        it('should handle result without assistant message', () => {
            adapter = new StreamJsonOutputAdapter(mockConfig, false);
            stdoutWriteSpy.mockClear();
            adapter.emitResult({
                isError: false,
                durationMs: 1000,
                apiDurationMs: 800,
                numTurns: 1,
            });
            const output = stdoutWriteSpy.mock.calls[0][0];
            const parsed = JSON.parse(output);
            expect(parsed.result).toBe('');
        });
    });
    describe('emitUserMessage', () => {
        beforeEach(() => {
            adapter = new StreamJsonOutputAdapter(mockConfig, false);
        });
        it('should emit user message immediately', () => {
            stdoutWriteSpy.mockClear();
            const parts = [{ text: 'Hello user' }];
            adapter.emitUserMessage(parts);
            expect(stdoutWriteSpy).toHaveBeenCalled();
            const output = stdoutWriteSpy.mock.calls[0][0];
            const parsed = JSON.parse(output);
            expect(parsed.type).toBe('user');
            expect(Array.isArray(parsed.message.content)).toBe(true);
            if (Array.isArray(parsed.message.content)) {
                expect(parsed.message.content).toHaveLength(1);
                expect(parsed.message.content[0]).toEqual({
                    type: 'text',
                    text: 'Hello user',
                });
            }
        });
        it('should handle parent_tool_use_id', () => {
            const parts = [{ text: 'Tool response' }];
            adapter.emitUserMessage(parts);
            const output = stdoutWriteSpy.mock.calls[0][0];
            const parsed = JSON.parse(output);
            // emitUserMessage currently sets parent_tool_use_id to null
            expect(parsed.parent_tool_use_id).toBeNull();
        });
    });
    describe('emitToolResult', () => {
        beforeEach(() => {
            adapter = new StreamJsonOutputAdapter(mockConfig, false);
        });
        it('should emit tool result message immediately', () => {
            stdoutWriteSpy.mockClear();
            const request = {
                callId: 'tool-1',
                name: 'test_tool',
                args: {},
                isClientInitiated: false,
                prompt_id: 'prompt-1',
            };
            const response = {
                callId: 'tool-1',
                responseParts: [],
                resultDisplay: 'Tool executed successfully',
                error: undefined,
                errorType: undefined,
            };
            adapter.emitToolResult(request, response);
            expect(stdoutWriteSpy).toHaveBeenCalled();
            const output = stdoutWriteSpy.mock.calls[0][0];
            const parsed = JSON.parse(output);
            expect(parsed.type).toBe('user');
            expect(parsed.parent_tool_use_id).toBeNull();
            const block = parsed.message.content[0];
            expect(block).toMatchObject({
                type: 'tool_result',
                tool_use_id: 'tool-1',
                content: 'Tool executed successfully',
                is_error: false,
            });
        });
        it('should mark error tool results', () => {
            const request = {
                callId: 'tool-1',
                name: 'test_tool',
                args: {},
                isClientInitiated: false,
                prompt_id: 'prompt-1',
            };
            const response = {
                callId: 'tool-1',
                responseParts: [],
                resultDisplay: undefined,
                error: new Error('Tool failed'),
                errorType: undefined,
            };
            adapter.emitToolResult(request, response);
            const output = stdoutWriteSpy.mock.calls[0][0];
            const parsed = JSON.parse(output);
            const block = parsed.message.content[0];
            expect(block.is_error).toBe(true);
        });
    });
    describe('emitSystemMessage', () => {
        beforeEach(() => {
            adapter = new StreamJsonOutputAdapter(mockConfig, false);
        });
        it('should emit system message immediately', () => {
            stdoutWriteSpy.mockClear();
            adapter.emitSystemMessage('test_subtype', { data: 'value' });
            expect(stdoutWriteSpy).toHaveBeenCalled();
            const output = stdoutWriteSpy.mock.calls[0][0];
            const parsed = JSON.parse(output);
            expect(parsed.type).toBe('system');
            expect(parsed.subtype).toBe('test_subtype');
            expect(parsed.data).toEqual({ data: 'value' });
        });
    });
    describe('emitToolProgress', () => {
        const mockRequest = {
            callId: 'tool-call-1',
            name: 'mcp__echo-test__echo',
            args: {},
            isClientInitiated: false,
            prompt_id: '',
        };
        it('should emit tool_progress stream event when includePartialMessages is true', () => {
            adapter = new StreamJsonOutputAdapter(mockConfig, true);
            stdoutWriteSpy.mockClear();
            adapter.emitToolProgress(mockRequest, {
                type: 'mcp_tool_progress',
                progress: 1,
                total: 10,
                message: 'Echo: 1',
            });
            expect(stdoutWriteSpy).toHaveBeenCalledTimes(1);
            const output = stdoutWriteSpy.mock.calls[0][0];
            const parsed = JSON.parse(output);
            expect(parsed.type).toBe('stream_event');
            expect(parsed.parent_tool_use_id).toBeNull();
            expect(parsed.session_id).toBe('test-session-id');
            expect(parsed.uuid).toBeDefined();
            expect(parsed.event).toEqual({
                type: 'tool_progress',
                tool_use_id: 'tool-call-1',
                content: {
                    type: 'mcp_tool_progress',
                    progress: 1,
                    total: 10,
                    message: 'Echo: 1',
                },
            });
        });
        it('should not emit tool_progress when includePartialMessages is false', () => {
            adapter = new StreamJsonOutputAdapter(mockConfig, false);
            stdoutWriteSpy.mockClear();
            adapter.emitToolProgress(mockRequest, {
                type: 'mcp_tool_progress',
                progress: 1,
                total: 10,
                message: 'Echo: 1',
            });
            expect(stdoutWriteSpy).not.toHaveBeenCalled();
        });
        it('should emit multiple tool_progress events for sequential progress updates', () => {
            adapter = new StreamJsonOutputAdapter(mockConfig, true);
            stdoutWriteSpy.mockClear();
            adapter.emitToolProgress(mockRequest, {
                type: 'mcp_tool_progress',
                progress: 1,
                total: 3,
                message: 'Echo: 1',
            });
            adapter.emitToolProgress(mockRequest, {
                type: 'mcp_tool_progress',
                progress: 2,
                total: 3,
                message: 'Echo: 1, 2',
            });
            adapter.emitToolProgress(mockRequest, {
                type: 'mcp_tool_progress',
                progress: 3,
                total: 3,
                message: 'Echo: 1, 2, 3',
            });
            expect(stdoutWriteSpy).toHaveBeenCalledTimes(3);
            const events = stdoutWriteSpy.mock.calls.map((call) => JSON.parse(call[0]).event);
            expect(events[0].content).toEqual({
                type: 'mcp_tool_progress',
                progress: 1,
                total: 3,
                message: 'Echo: 1',
            });
            expect(events[1].content).toEqual({
                type: 'mcp_tool_progress',
                progress: 2,
                total: 3,
                message: 'Echo: 1, 2',
            });
            expect(events[2].content).toEqual({
                type: 'mcp_tool_progress',
                progress: 3,
                total: 3,
                message: 'Echo: 1, 2, 3',
            });
            // All events should share the same tool_use_id
            for (const event of events) {
                expect(event.type).toBe('tool_progress');
                expect(event.tool_use_id).toBe('tool-call-1');
            }
        });
    });
    describe('getSessionId and getModel', () => {
        beforeEach(() => {
            adapter = new StreamJsonOutputAdapter(mockConfig, false);
        });
        it('should return session ID from config', () => {
            expect(adapter.getSessionId()).toBe('test-session-id');
            expect(mockConfig.getSessionId).toHaveBeenCalled();
        });
        it('should return model from config', () => {
            expect(adapter.getModel()).toBe('test-model');
            expect(mockConfig.getModel).toHaveBeenCalled();
        });
    });
    describe('content_block event identification', () => {
        beforeEach(() => {
            adapter = new StreamJsonOutputAdapter(mockConfig, true);
            adapter.startAssistantMessage();
        });
        it('should not include message_id in content_block events', () => {
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'Text',
            });
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'More',
            });
            const calls = stdoutWriteSpy.mock.calls;
            const contentBlockCalls = calls.filter((call) => {
                try {
                    const parsed = JSON.parse(call[0]);
                    return (parsed.type === 'stream_event' &&
                        (parsed.event.type === 'content_block_start' ||
                            parsed.event.type === 'content_block_delta' ||
                            parsed.event.type === 'content_block_stop'));
                }
                catch {
                    return false;
                }
            });
            expect(contentBlockCalls.length).toBeGreaterThan(0);
            for (const call of contentBlockCalls) {
                const parsed = JSON.parse(call[0]);
                expect(parsed.event.message_id).toBeUndefined();
            }
        });
        it('should identify content_block events by session_id and index', () => {
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'Text',
            });
            const calls = stdoutWriteSpy.mock.calls;
            const blockStartCall = calls.find((call) => {
                try {
                    const parsed = JSON.parse(call[0]);
                    return (parsed.type === 'stream_event' &&
                        parsed.event.type === 'content_block_start');
                }
                catch {
                    return false;
                }
            });
            expect(blockStartCall).toBeDefined();
            const parsed = JSON.parse(blockStartCall[0]);
            expect(parsed.session_id).toBe('test-session-id');
            expect(typeof parsed.event.index).toBe('number');
        });
    });
    describe('multiple text blocks', () => {
        beforeEach(() => {
            adapter = new StreamJsonOutputAdapter(mockConfig, false);
            adapter.startAssistantMessage();
        });
        it('should split assistant messages when block types change repeatedly', () => {
            stdoutWriteSpy.mockClear();
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'Text content',
            });
            adapter.processEvent({
                type: HopCodeEventType.Thought,
                value: { subject: 'Thinking', description: 'Thought' },
            });
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'More text',
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.content).toHaveLength(1);
            expect(message.message.content[0]).toMatchObject({
                type: 'text',
                text: 'More text',
            });
            const assistantMessages = stdoutWriteSpy.mock.calls
                .map((call) => JSON.parse(call[0]))
                .filter((payload) => {
                if (typeof payload !== 'object' ||
                    payload === null ||
                    !('type' in payload) ||
                    payload.type !== 'assistant' ||
                    !('message' in payload)) {
                    return false;
                }
                const message = payload.message;
                if (typeof message !== 'object' ||
                    message === null ||
                    !('content' in message)) {
                    return false;
                }
                const content = message.content;
                return (Array.isArray(content) &&
                    content.length > 0 &&
                    content.every((block) => typeof block === 'object' &&
                        block !== null &&
                        'type' in block));
            });
            expect(assistantMessages).toHaveLength(3);
            const observedTypes = assistantMessages.map((msg) => msg.message.content[0]?.type ?? '');
            expect(observedTypes).toEqual(['text', 'thinking', 'text']);
            for (const msg of assistantMessages) {
                const uniqueTypes = new Set(msg.message.content.map((block) => block.type));
                expect(uniqueTypes.size).toBeLessThanOrEqual(1);
            }
        });
        it('should merge consecutive text fragments', () => {
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'Hello',
            });
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: ' ',
            });
            adapter.processEvent({
                type: HopCodeEventType.Content,
                value: 'World',
            });
            const message = adapter.finalizeAssistantMessage();
            expect(message.message.content).toHaveLength(1);
            expect(message.message.content[0]).toMatchObject({
                type: 'text',
                text: 'Hello World',
            });
        });
    });
});
//# sourceMappingURL=StreamJsonOutputAdapter.test.js.map