"""Unit tests for navari_cli.chat module."""

import json
import ssl
from unittest.mock import AsyncMock, patch

import pytest
import websockets.exceptions
from navari_cli.chat import (
    ChatSession,
    _build_ssl_context,
    _build_ws_url,
    _extract_json_string_value,
    _print_todo_summary,
)
from websockets.datastructures import Headers
from websockets.http11 import Response as WsResponse

# ============================================================================
# _extract_json_string_value
# ============================================================================


class TestExtractJsonStringValue:
    def test_complete_json(self):
        assert _extract_json_string_value('{"message": "Hello world"}', "message") == "Hello world"

    def test_partial_json(self):
        assert _extract_json_string_value('{"message": "Hello wor', "message") == "Hello wor"

    def test_no_key_yet(self):
        assert _extract_json_string_value('{"mes', "message") is None

    def test_empty_value(self):
        assert _extract_json_string_value('{"message": ""}', "message") == ""

    def test_escaped_newline(self):
        assert _extract_json_string_value('{"message": "line1\\nline2"}', "message") == "line1\nline2"

    def test_escaped_quote(self):
        assert _extract_json_string_value('{"message": "say \\"hello\\""}', "message") == 'say "hello"'

    def test_escaped_backslash(self):
        assert _extract_json_string_value('{"message": "path\\\\to"}', "message") == "path\\to"

    def test_escaped_tab(self):
        assert _extract_json_string_value('{"message": "col1\\tcol2"}', "message") == "col1\tcol2"

    def test_no_space_after_colon(self):
        assert _extract_json_string_value('{"message":"compact"}', "message") == "compact"

    def test_different_key(self):
        assert _extract_json_string_value('{"question": "What?"}', "question") == "What?"

    def test_wrong_key(self):
        assert _extract_json_string_value('{"message": "Hello"}', "question") is None

    def test_empty_input(self):
        assert _extract_json_string_value("", "message") is None

    def test_partial_escape_at_end(self):
        # Backslash at the very end with no following char — treated as literal
        assert _extract_json_string_value('{"message": "end\\', "message") == "end\\"


# ============================================================================
# _print_todo_summary
# ============================================================================


class TestPrintTodoSummary:
    def test_basic_todo(self, capsys):
        content = """# My Tasks
- [x] Done task
- [ ] Pending task
"""
        _print_todo_summary(content)
        output = capsys.readouterr().out
        assert "My Tasks" in output
        assert "Done task" in output
        assert "Pending task" in output
        assert "1/2 done" in output

    def test_all_done(self, capsys):
        content = """# Complete
- [x] Task 1
- [x] Task 2
"""
        _print_todo_summary(content)
        output = capsys.readouterr().out
        assert "2/2 done" in output

    def test_none_done(self, capsys):
        content = """# Backlog
- [ ] Task A
- [ ] Task B
- [ ] Task C
"""
        _print_todo_summary(content)
        output = capsys.readouterr().out
        assert "0/3 done" in output

    def test_no_items(self, capsys):
        content = "# Empty\nSome description"
        _print_todo_summary(content)
        output = capsys.readouterr().out
        assert "Empty" in output
        assert "done" not in output

    def test_subitems_ignored(self, capsys):
        content = """# Project
- [x] Main task
  - Sub-item detail
- [ ] Another task
"""
        _print_todo_summary(content)
        output = capsys.readouterr().out
        assert "1/2 done" in output


# ============================================================================
# _build_ws_url / _build_ssl_context
# ============================================================================


class TestBuildWsUrl:
    def test_https_to_wss(self):
        url = _build_ws_url("https://example.com/navari", "conv-123", "tok")
        assert url == "wss://example.com/navari/api/ws/conversations/conv-123/start?token=tok"

    def test_http_to_ws(self):
        url = _build_ws_url("http://localhost:8000", "c1", "t1")
        assert url == "ws://localhost:8000/api/ws/conversations/c1/start?token=t1"


class TestBuildSslContext:
    def test_verify_true_returns_none(self):
        assert _build_ssl_context(True) is None

    def test_verify_false_returns_context(self):
        ctx = _build_ssl_context(False)
        assert isinstance(ctx, ssl.SSLContext)
        assert ctx.check_hostname is False
        assert ctx.verify_mode == ssl.CERT_NONE


# ============================================================================
# ChatSession._handle_files_list
# ============================================================================


class TestHandleFilesList:
    @pytest.fixture()
    def session(self):
        return ChatSession(
            base_url="http://localhost",
            conversation_id="test-conv",
            token="test-token",
        )

    def test_no_files(self, session, capsys):
        session._handle_files_list()
        output = capsys.readouterr().out
        assert "No files attached" in output

    def test_with_files(self, session, capsys):
        session.pending_file_ids = ["file-abc", "file-def"]
        session._handle_files_list()
        output = capsys.readouterr().out
        assert "Attached files (2)" in output
        assert "file-abc" in output
        assert "file-def" in output


class TestRunQuit:
    """Test that /quit in the REPL prints ending message with conversation ID."""

    @pytest.fixture()
    def session(self):
        return ChatSession(
            base_url="http://localhost",
            conversation_id="test-conv-123",
            token="test-token",
        )

    async def test_quit_shows_conversation_id(self, session, capsys):
        with patch("navari_cli.chat.console") as mock_console:
            mock_console.input.return_value = "/quit"
            mock_console.print = lambda *a, **_kw: print(*a)
            await session.run()

        output = capsys.readouterr().out
        assert "Ending chat session (test-conv-123)" in output


# ============================================================================
# ChatSession._wait_for_connection
# ============================================================================


class TestWaitForConnection:
    @pytest.fixture()
    def session(self):
        return ChatSession(
            base_url="http://localhost",
            conversation_id="test-conv",
            token="test-token",
            debug=True,
        )

    @staticmethod
    def _make_recv_ws(messages):
        """Create a mock WS with a recv() method that returns messages in sequence."""
        ws = AsyncMock()
        ws.recv = AsyncMock(side_effect=[json.dumps(m) for m in messages])
        return ws

    async def test_connection_established(self, session):
        ws = self._make_recv_ws(
            [
                {"event_kind": "initializing"},
                {"event_kind": "connection_established"},
            ]
        )
        result = await session._wait_for_connection(ws)
        assert result is True

    async def test_error_during_init(self, session, capsys):
        ws = self._make_recv_ws(
            [
                {"event_kind": "error", "message": "Server overloaded"},
            ]
        )
        result = await session._wait_for_connection(ws)
        assert result is False
        output = capsys.readouterr().out
        assert "Server overloaded" in output

    async def test_timeout_during_init(self, session, capsys):
        ws = AsyncMock()
        ws.recv = AsyncMock(side_effect=TimeoutError)
        result = await session._wait_for_connection(ws)
        assert result is False
        output = capsys.readouterr().out
        assert "Timed out" in output

    async def test_unexpected_event_skipped(self, session, capsys):
        ws = self._make_recv_ws(
            [
                {"event_kind": "something_unexpected"},
                {"event_kind": "connection_established"},
            ]
        )
        result = await session._wait_for_connection(ws)
        assert result is True
        # debug=True session logs unexpected events
        output = capsys.readouterr().out
        assert "Unexpected event" in output


# ============================================================================
# ChatSession._send_query error paths
# ============================================================================


class TestSendQueryErrors:
    @pytest.fixture()
    def session(self):
        return ChatSession(
            base_url="http://localhost",
            conversation_id="test-conv",
            token="test-token",
            debug=True,
        )

    async def test_403_error(self, session, capsys):
        exc = websockets.exceptions.InvalidStatus(WsResponse(403, "Forbidden", Headers()))
        with patch("navari_cli.chat.websockets.connect", side_effect=exc):
            await session._send_query("hello")
        output = capsys.readouterr().out
        assert "Access denied" in output

    async def test_500_error(self, session, capsys):
        exc = websockets.exceptions.InvalidStatus(WsResponse(500, "Internal Server Error", Headers()))
        with patch("navari_cli.chat.websockets.connect", side_effect=exc):
            await session._send_query("hello")
        output = capsys.readouterr().out
        assert "HTTP 500" in output

    async def test_connection_refused(self, session, capsys):
        with patch("navari_cli.chat.websockets.connect", side_effect=ConnectionRefusedError("refused")):
            await session._send_query("hello")
        output = capsys.readouterr().out
        assert "Connection lost" in output

    async def test_timeout(self, session, capsys):
        with patch("navari_cli.chat.websockets.connect", side_effect=TimeoutError):
            await session._send_query("hello")
        output = capsys.readouterr().out
        assert "timed out" in output

    async def test_generic_error(self, session, capsys):
        with patch("navari_cli.chat.websockets.connect", side_effect=RuntimeError("something broke")):
            await session._send_query("hello")
        output = capsys.readouterr().out
        assert "something broke" in output


# ============================================================================
# ChatSession._stream_response
# ============================================================================


class TestStreamResponse:
    """Test _stream_response by feeding mock WebSocket events."""

    @pytest.fixture()
    def session(self):
        return ChatSession(
            base_url="http://localhost",
            conversation_id="test-conv",
            token="test-token",
            verify_ssl=False,
            debug=False,
        )

    @pytest.fixture()
    def debug_session(self):
        return ChatSession(
            base_url="http://localhost",
            conversation_id="test-conv",
            token="test-token",
            verify_ssl=False,
            debug=True,
        )

    @staticmethod
    def _make_ws(events):
        """Create a mock WebSocket that yields JSON events via recv()."""

        class MockWS:
            def __init__(self, events):
                self._events = iter(events)
                self.sent = []

            async def recv(self):
                try:
                    event = next(self._events)
                    return json.dumps(event)
                except StopIteration:
                    raise websockets.exceptions.ConnectionClosed(None, None) from None

            async def send(self, data):
                self.sent.append(data)

        return MockWS(events)

    async def test_send_message_to_user(self, session, capsys):
        events = [
            {
                "event_kind": "function_tool_call",
                "part": {"tool_name": "send_message_to_user", "args": {"message": "Hello from test"}},
            },
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "Hello from test" in output

    async def test_ask_user_tool_card(self, session, capsys):
        events = [
            {"event_kind": "part_start", "part": {"tool_name": "ask_user", "part_kind": "tool-call"}},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert 'use tool "ask_user"' in output

    async def test_generic_tool_card(self, session, capsys):
        events = [
            {"event_kind": "part_start", "part": {"tool_name": "web_search", "part_kind": "tool-call"}},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert 'use tool "web_search"' in output

    async def test_send_message_not_shown_as_tool(self, session, capsys):
        events = [
            {"event_kind": "part_start", "part": {"tool_name": "send_message_to_user", "part_kind": "tool-call"}},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "use tool" not in output

    async def test_processing_started_and_finished(self, session, capsys):
        events = [
            {"event_kind": "processing_started"},
            {
                "event_kind": "function_tool_call",
                "part": {"tool_name": "send_message_to_user", "args": {"message": "Hi"}},
            },
            {"event_kind": "processing_finished"},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "Hi" in output

    async def test_agent_result(self, session, capsys):
        events = [
            {"event_kind": "agent_result", "summary": "Task completed successfully"},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "Task completed successfully" in output

    async def test_error_event(self, session, capsys):
        events = [
            {"event_kind": "error", "message": "Something went wrong"},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "Something went wrong" in output

    async def test_error_event_stops_streaming(self, session, capsys):
        """Events after an error should be ignored (break)."""
        events = [
            {"event_kind": "error", "message": "Server failed"},
            {"event_kind": "agent_result", "summary": "This should NOT appear"},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "Server failed" in output
        assert "This should NOT appear" not in output

    async def test_ask_user_with_examples(self, session, capsys):
        args = json.dumps({"question": "Choose:", "examples": ["Option A", "Option B"]})
        events = [
            {"event_kind": "function_tool_call", "part": {"tool_name": "ask_user_with_examples", "args": args}},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "Choose:" in output
        assert "1. Option A" in output
        assert "2. Option B" in output

    async def test_write_to_do_file(self, session, capsys):
        events = [
            {
                "event_kind": "function_tool_call",
                "part": {
                    "tool_name": "write_to_do_file",
                    "args": json.dumps({"content": "# Tasks\n- [x] Done\n- [ ] Pending"}),
                },
            },
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "Tasks" in output
        assert "done" in output

    async def test_content_delta_streaming(self, session, capsys):
        events = [
            {"event_kind": "part_delta", "delta": {"content_delta": "Hello "}},
            {"event_kind": "part_delta", "delta": {"content_delta": "world"}},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "Hello " in output
        assert "world" in output

    async def test_malformed_json_ignored(self, session, capsys):
        """Non-JSON messages should be silently ignored."""

        class MixedWS:
            def __init__(self):
                self._msgs = iter(["not json", '{"event_kind": "agent_result", "summary": "OK"}'])

            async def recv(self):
                try:
                    return next(self._msgs)
                except StopIteration:
                    raise websockets.exceptions.ConnectionClosed(None, None) from None

        await session._stream_response(MixedWS())
        output = capsys.readouterr().out
        assert "OK" in output

    # --- Additional coverage for streaming, events, and edge cases ---

    async def test_processing_cancelled(self, session, capsys):
        events = [
            {"event_kind": "processing_cancelled"},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "Cancelled" in output

    async def test_tool_progress(self, session, capsys):
        events = [
            {"event_kind": "tool_progress", "message": "Searching documents..."},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "Searching documents..." in output

    async def test_mode_changed(self, session, capsys):
        events = [
            {"event_kind": "mode_changed", "mode": "research"},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "[mode: research]" in output

    async def test_compaction_debug(self, debug_session, capsys):
        events = [
            {"event_kind": "compaction", "original_messages": 50, "compacted_messages": 10},
        ]
        await debug_session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "compaction: 50 -> 10" in output

    async def test_debug_logging(self, debug_session, capsys):
        events = [
            {"event_kind": "processing_started"},
        ]
        await debug_session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "processing_started" in output

    async def test_args_delta_streaming_send_message(self, session, capsys):
        """Test real-time streaming of send_message_to_user via part_delta."""
        events = [
            {"event_kind": "part_start", "part": {"tool_name": "send_message_to_user"}},
            {"event_kind": "part_delta", "delta": {"args_delta": '{"message": "Hel'}},
            {"event_kind": "part_delta", "delta": {"args_delta": "lo wor"}},
            {"event_kind": "part_delta", "delta": {"args_delta": 'ld"}'}},
            {"event_kind": "part_end"},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "Navari>" in output
        assert "Hel" in output
        assert "lo wor" in output

    async def test_args_delta_dict_form(self, session):
        """Test args_delta arriving as a dict instead of string."""
        events = [
            {"event_kind": "part_start", "part": {"tool_name": "web_search"}},
            {"event_kind": "part_delta", "delta": {"args_delta": {"query": "test"}}},
            {"event_kind": "part_end"},
        ]
        await session._stream_response(self._make_ws(events))
        # Should not crash — dict delta is handled gracefully

    async def test_part_end_resets_streaming_state(self, session, capsys):
        """After part_end, a new send_message should get its own header."""
        events = [
            {"event_kind": "part_start", "part": {"tool_name": "send_message_to_user"}},
            {"event_kind": "part_delta", "delta": {"args_delta": '{"message": "First"}'}},
            {"event_kind": "part_end"},
            {"event_kind": "part_start", "part": {"tool_name": "send_message_to_user"}},
            {"event_kind": "part_delta", "delta": {"args_delta": '{"message": "Second"}'}},
            {"event_kind": "part_end"},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "First" in output
        assert "Second" in output

    async def test_function_tool_call_json_string_args(self, session, capsys):
        """Test function_tool_call with args as a JSON string (not dict)."""
        args = json.dumps({"question": "Continue?"})
        events = [
            {"event_kind": "function_tool_call", "part": {"tool_name": "ask_user", "args": args}},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "Continue?" in output

    async def test_function_tool_call_empty_args(self, session, capsys):
        """Test function_tool_call with empty args string."""
        events = [
            {"event_kind": "function_tool_call", "part": {"tool_name": "some_tool", "args": ""}},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert 'use tool "some_tool"' in output

    async def test_function_tool_call_invalid_json_args(self, session, capsys):
        """Test function_tool_call with invalid JSON in args."""
        events = [
            {"event_kind": "function_tool_call", "part": {"tool_name": "some_tool", "args": "not-json"}},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert 'use tool "some_tool"' in output

    async def test_generic_tool_via_function_tool_call(self, session, capsys):
        """Generic tool shown when function_tool_call has different name from part_start."""
        events = [
            {"event_kind": "function_tool_call", "part": {"tool_name": "python_exec", "args": "{}"}},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert 'use tool "python_exec"' in output

    async def test_tool_card_printed_once_with_streaming(self, session, capsys):
        """Full streaming sequence: part_start + function_tool_call prints card only once."""
        events = [
            {"event_kind": "part_start", "part": {"tool_name": "web_search", "part_kind": "tool-call"}},
            {"event_kind": "part_delta", "delta": {"args_delta": '{"query": "test"}'}},
            {"event_kind": "function_tool_call", "part": {"tool_name": "web_search", "args": {"query": "test"}}},
            {"event_kind": "part_end"},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert output.count('use tool "web_search"') == 1

    async def test_tool_card_printed_once_part_end_before_call(self, session, capsys):
        """If part_end arrives before function_tool_call, card should still appear only once."""
        events = [
            {"event_kind": "part_start", "part": {"tool_name": "browse_url", "part_kind": "tool-call"}},
            {"event_kind": "part_delta", "delta": {"args_delta": '{"url": "http://example.com"}'}},
            {"event_kind": "part_end"},
            {
                "event_kind": "function_tool_call",
                "part": {"tool_name": "browse_url", "args": {"url": "http://example.com"}},
            },
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert output.count('use tool "browse_url"') == 1

    async def test_report_file_created_shows_filename(self, session, capsys):
        """report_file_created shows the extracted filename, not bare tool card."""
        events = [
            {
                "event_kind": "function_tool_call",
                "part": {
                    "tool_name": "report_file_created",
                    "args": {"file_path": "/workspace/output/analysis.pdf"},
                },
            },
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert "file created: analysis.pdf" in output
        assert 'use tool "report_file_created"' not in output

    async def test_report_file_created_with_streaming(self, session, capsys):
        """Streaming report_file_created shows tool card and filename."""
        events = [
            {"event_kind": "part_start", "part": {"tool_name": "report_file_created", "part_kind": "tool-call"}},
            {"event_kind": "part_delta", "delta": {"args_delta": '{"file_path": "/workspace/output/analysis.pdf"}'}},
            {
                "event_kind": "function_tool_call",
                "part": {
                    "tool_name": "report_file_created",
                    "args": {"file_path": "/workspace/output/analysis.pdf"},
                },
            },
            {"event_kind": "part_end"},
        ]
        await session._stream_response(self._make_ws(events))
        output = capsys.readouterr().out
        assert 'use tool "report_file_created"' in output
        assert "file created: analysis.pdf" in output

    async def test_connection_closed_during_stream(self, session):
        """ConnectionClosed during streaming is handled gracefully."""

        class ClosingWS:
            async def recv(self):
                raise websockets.exceptions.ConnectionClosed(None, None)

        await session._stream_response(ClosingWS())
        # Should not raise — ConnectionClosed is normal at end of query

    async def test_keyboard_interrupt_during_stream(self, session, capsys):
        """KeyboardInterrupt during streaming sends stop and returns."""
        sent_messages = []

        class InterruptingWS:
            def __init__(self):
                self._raised = False

            async def recv(self):
                if not self._raised:
                    self._raised = True
                    raise KeyboardInterrupt
                raise websockets.exceptions.ConnectionClosed(None, None)

            async def send(self, msg):
                sent_messages.append(msg)

        await session._stream_response(InterruptingWS())
        output = capsys.readouterr().out
        assert "cancelled" in output.lower()
        assert len(sent_messages) == 1
        assert json.loads(sent_messages[0]) == {"stop": True}

    async def test_waiting_for_input(self, session, capsys):
        """waiting_for_input prompts user and sends response."""
        sent_messages = []

        class InputWS:
            def __init__(self):
                self._events = iter(
                    [
                        json.dumps({"event_kind": "waiting_for_input"}),
                    ]
                )

            async def recv(self):
                try:
                    return next(self._events)
                except StopIteration:
                    raise websockets.exceptions.ConnectionClosed(None, None) from None

            async def send(self, msg):
                sent_messages.append(msg)

        with patch("navari_cli.chat.console") as mock_console:
            mock_console.input.return_value = "my answer"
            mock_console.print = lambda *a, **_kw: print(*a)
            await session._stream_response(InputWS())

        assert len(sent_messages) == 1
        assert json.loads(sent_messages[0]) == {"response": "my answer"}

    async def test_waiting_for_input_reconnects_on_closed_ws(self, session, capsys):
        """If WS dropped during waiting_for_input, reconnects via /attach and resends."""
        sent_on_original = []
        sent_on_new = []

        class ClosedWS:
            """WS that delivers waiting_for_input then fails on send."""

            def __init__(self):
                self._events = iter([json.dumps({"event_kind": "waiting_for_input"})])

            async def recv(self):
                try:
                    return next(self._events)
                except StopIteration:
                    raise websockets.exceptions.ConnectionClosed(None, None) from None

            async def send(self, msg):
                sent_on_original.append(msg)
                raise websockets.exceptions.ConnectionClosed(None, None)

        class ReconnectedWS:
            """Mock WS returned by websockets.connect for /attach."""

            def __init__(self):
                self._events = iter(
                    [
                        json.dumps({"event_kind": "connection_established"}),
                        json.dumps({"event_kind": "processing_finished"}),
                    ]
                )

            async def recv(self):
                try:
                    return next(self._events)
                except StopIteration:
                    raise websockets.exceptions.ConnectionClosed(None, None) from None

            async def send(self, msg):
                sent_on_new.append(msg)

            def __aiter__(self):
                return self

            async def __anext__(self):
                try:
                    return await self.recv()
                except websockets.exceptions.ConnectionClosed:
                    raise StopAsyncIteration from None

        new_ws = ReconnectedWS()

        async def mock_connect(*args, **kwargs):
            return new_ws

        with (
            patch("navari_cli.chat.console") as mock_console,
            patch("navari_cli.chat.websockets.connect", side_effect=mock_connect),
        ):
            mock_console.input.return_value = "my answer"
            mock_console.print = lambda *a, **_kw: print(*a)
            await session._stream_response(ClosedWS())

        # Original send should have been attempted
        assert len(sent_on_original) == 1
        # Response should have been resent on the new connection
        assert len(sent_on_new) == 1
        assert json.loads(sent_on_new[0]) == {"response": "my answer"}

    async def test_waiting_for_input_reconnect_failure(self, session, capsys):
        """If /attach reconnection fails, error is shown and exception propagates."""

        class ClosedWS:
            def __init__(self):
                self._events = iter([json.dumps({"event_kind": "waiting_for_input"})])

            async def recv(self):
                try:
                    return next(self._events)
                except StopIteration:
                    raise websockets.exceptions.ConnectionClosed(None, None) from None

            async def send(self, msg):
                raise websockets.exceptions.ConnectionClosed(None, None)

        async def mock_connect(*args, **kwargs):
            raise TimeoutError("attach timed out")

        with (
            patch("navari_cli.chat.console") as mock_console,
            patch("navari_cli.chat.websockets.connect", side_effect=mock_connect),
        ):
            mock_console.input.return_value = "my answer"
            mock_console.print = lambda *a, **_kw: print(*a)
            with pytest.raises(TimeoutError, match="attach timed out"):
                await session._stream_response(ClosedWS())
