"""Unit tests for navari_cli.chat module (AG-UI SSE protocol)."""

import json
from unittest.mock import AsyncMock, patch

import httpx
import pytest
from navari_cli.chat import ChatSession, _print_todo_summary

# ============================================================================
# _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


# ============================================================================
# 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._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):
        """Server returns 403 — shows access denied message."""
        mock_response = AsyncMock()
        mock_response.status_code = 403
        mock_stream = AsyncMock()
        mock_stream.__aenter__ = AsyncMock(return_value=mock_response)
        mock_stream.__aexit__ = AsyncMock(return_value=False)
        mock_client = AsyncMock()
        mock_client.stream = lambda *a, **kw: mock_stream
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)

        with patch("navari_cli.chat.httpx.AsyncClient", return_value=mock_client):
            await session._send_query("hello")
        output = capsys.readouterr().out
        assert "Access denied" in output

    async def test_connect_error(self, session, capsys):
        """Server unreachable — shows connection error."""
        mock_client = AsyncMock()
        mock_client.__aenter__ = AsyncMock(side_effect=httpx.ConnectError("refused"))
        mock_client.__aexit__ = AsyncMock(return_value=False)

        with patch("navari_cli.chat.httpx.AsyncClient", return_value=mock_client):
            await session._send_query("hello")
        output = capsys.readouterr().out
        assert "Could not connect" in output

    async def test_timeout(self, session, capsys):
        """Request times out."""
        mock_client = AsyncMock()
        mock_client.__aenter__ = AsyncMock(side_effect=httpx.TimeoutException("timeout"))
        mock_client.__aexit__ = AsyncMock(return_value=False)

        with patch("navari_cli.chat.httpx.AsyncClient", return_value=mock_client):
            await session._send_query("hello")
        output = capsys.readouterr().out
        assert "timed out" in output


# ============================================================================
# ChatSession._stream_sse_response
# ============================================================================


def _make_sse_response(events):
    """Create a mock httpx streaming response that yields SSE data lines."""

    class MockResponse:
        def __init__(self, events):
            self._lines = []
            for event in events:
                self._lines.append(f"data: {json.dumps(event)}")

        async def aiter_lines(self):
            for line in self._lines:
                yield line

        async def aclose(self):
            pass

    return MockResponse(events)


class TestStreamSSEResponse:
    """Test _stream_sse_response with AG-UI 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,
        )

    async def test_text_message_streaming(self, session, capsys):
        """TEXT_MESSAGE events display agent text output."""
        events = [
            {"type": "RUN_STARTED"},
            {"type": "TEXT_MESSAGE_START", "messageId": "m1"},
            {"type": "TEXT_MESSAGE_CONTENT", "messageId": "m1", "delta": "Hello "},
            {"type": "TEXT_MESSAGE_CONTENT", "messageId": "m1", "delta": "world"},
            {"type": "TEXT_MESSAGE_END", "messageId": "m1"},
            {"type": "RUN_FINISHED", "result": None},
        ]
        await session._stream_sse_response(_make_sse_response(events))
        output = capsys.readouterr().out
        assert "Navari>" in output
        assert "Hello " in output
        assert "world" in output

    async def test_tool_call_card(self, session, capsys):
        """TOOL_CALL_START shows tool card."""
        events = [
            {"type": "RUN_STARTED"},
            {"type": "TOOL_CALL_START", "toolCallId": "tc1", "toolCallName": "web_search"},
            {"type": "TOOL_CALL_ARGS", "toolCallId": "tc1", "delta": '{"query": "test"}'},
            {"type": "TOOL_CALL_END", "toolCallId": "tc1"},
            {"type": "RUN_FINISHED", "result": None},
        ]
        await session._stream_sse_response(_make_sse_response(events))
        output = capsys.readouterr().out
        assert 'use tool "web_search"' in output

    async def test_ask_user_shows_question(self, session, capsys):
        """ask_user tool displays question from args."""
        args = json.dumps({"question": "What do you want?", "examples": ["Option A", "Option B"]})
        events = [
            {"type": "RUN_STARTED"},
            {"type": "TOOL_CALL_START", "toolCallId": "tc1", "toolCallName": "ask_user_with_examples"},
            {"type": "TOOL_CALL_ARGS", "toolCallId": "tc1", "delta": args},
            {"type": "TOOL_CALL_END", "toolCallId": "tc1"},
            {"type": "RUN_FINISHED", "result": None},
        ]
        await session._stream_sse_response(_make_sse_response(events))
        output = capsys.readouterr().out
        assert "What do you want?" in output
        assert "1. Option A" in output
        assert "2. Option B" in output
        # ask_user tools show tool card (consistent with frontend)
        assert 'use tool "ask_user_with_examples"' in output

    async def test_write_to_do_file(self, session, capsys):
        """write_to_do_file shows todo summary."""
        args = json.dumps({"content": "# Tasks\n- [x] Done\n- [ ] Pending", "to_do_path": "/workspace/todo.md"})
        events = [
            {"type": "RUN_STARTED"},
            {"type": "TOOL_CALL_START", "toolCallId": "tc1", "toolCallName": "write_to_do_file"},
            {"type": "TOOL_CALL_ARGS", "toolCallId": "tc1", "delta": args},
            {"type": "TOOL_CALL_END", "toolCallId": "tc1"},
            {"type": "RUN_FINISHED", "result": None},
        ]
        await session._stream_sse_response(_make_sse_response(events))
        output = capsys.readouterr().out
        assert "Tasks" in output
        assert "done" in output

    async def test_report_file_created(self, session, capsys):
        """report_file_created shows filename."""
        args = json.dumps({"file_path": "/workspace/output/analysis.pdf"})
        result = json.dumps(
            {
                "success": True,
                "file_path": "/workspace/output/analysis.pdf",
                "file_url": "/workspaces/user/conv/output/analysis.pdf",
                "file_name": "analysis.pdf",
                "display_path": "/workspace/output/analysis.pdf",
            }
        )
        events = [
            {"type": "RUN_STARTED"},
            {"type": "TOOL_CALL_START", "toolCallId": "tc1", "toolCallName": "report_file_created"},
            {"type": "TOOL_CALL_ARGS", "toolCallId": "tc1", "delta": args},
            {"type": "TOOL_CALL_END", "toolCallId": "tc1"},
            {"type": "TOOL_CALL_RESULT", "toolCallId": "tc1", "content": result},
            {"type": "RUN_FINISHED", "result": None},
        ]
        await session._stream_sse_response(_make_sse_response(events))
        output = capsys.readouterr().out
        assert "file created: analysis.pdf" in output
        assert "/workspace/output/analysis.pdf" in output
        # Verify workspace file was tracked
        assert len(session.workspace_files) == 1
        assert session.workspace_files[0]["name"] == "analysis.pdf"

    async def test_run_error(self, session, capsys):
        """RUN_ERROR displays error message."""
        events = [
            {"type": "RUN_STARTED"},
            {"type": "RUN_ERROR", "message": "Something went wrong"},
        ]
        await session._stream_sse_response(_make_sse_response(events))
        output = capsys.readouterr().out
        assert "Something went wrong" in output

    async def test_custom_mode_changed(self, session, capsys):
        """CUSTOM event for mode change displays mode."""
        events = [
            {"type": "RUN_STARTED"},
            {"type": "CUSTOM", "name": "mode_changed", "value": {"mode": "research"}},
            {"type": "RUN_FINISHED", "result": None},
        ]
        await session._stream_sse_response(_make_sse_response(events))
        output = capsys.readouterr().out
        assert "[mode: research]" in output

    async def test_custom_compaction_debug(self, debug_session, capsys):
        """CUSTOM compaction event shown in debug mode."""
        events = [
            {"type": "RUN_STARTED"},
            {"type": "CUSTOM", "name": "compaction", "value": {"original_messages": 50, "compacted_messages": 10}},
            {"type": "RUN_FINISHED", "result": None},
        ]
        await debug_session._stream_sse_response(_make_sse_response(events))
        output = capsys.readouterr().out
        assert "compaction: 50 -> 10" in output

    async def test_approval_flow(self, session, capsys):
        """RUN_FINISHED with DeferredToolRequests triggers approval prompt."""
        events = [
            {"type": "RUN_STARTED"},
            {"type": "TOOL_CALL_START", "toolCallId": "tc1", "toolCallName": "ask_user"},
            {"type": "TOOL_CALL_ARGS", "toolCallId": "tc1", "delta": '{"question": "What next?"}'},
            {"type": "TOOL_CALL_END", "toolCallId": "tc1"},
            {
                "type": "RUN_FINISHED",
                "result": {
                    "approvals": [
                        {"tool_call_id": "tc1", "tool_name": "ask_user", "metadata": {"question": "What next?"}}
                    ]
                },
            },
        ]

        with patch.object(session, "_send_approval", new_callable=AsyncMock) as mock_approval:
            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_sse_response(_make_sse_response(events))

        mock_approval.assert_called_once_with("tc1", "my answer")

    async def test_keepalive_comments_ignored(self, session, capsys):
        """SSE keepalive comments are silently ignored."""

        class ResponseWithKeepalive:
            async def aiter_lines(self):
                yield ": keepalive"
                yield 'data: {"type": "RUN_STARTED"}'
                yield ""  # empty line
                yield ": keepalive"
                yield 'data: {"type": "TEXT_MESSAGE_START", "messageId": "m1"}'
                yield 'data: {"type": "TEXT_MESSAGE_CONTENT", "messageId": "m1", "delta": "OK"}'
                yield 'data: {"type": "TEXT_MESSAGE_END", "messageId": "m1"}'
                yield 'data: {"type": "RUN_FINISHED", "result": null}'

            async def aclose(self):
                pass

        await session._stream_sse_response(ResponseWithKeepalive())
        output = capsys.readouterr().out
        assert "OK" in output

    async def test_activity_snapshot(self, session, capsys):
        """ACTIVITY_SNAPSHOT shows A2A progress."""
        events = [
            {"type": "RUN_STARTED"},
            {"type": "ACTIVITY_SNAPSHOT", "activity": {"title": "Searching documents..."}},
            {"type": "RUN_FINISHED", "result": None},
        ]
        await session._stream_sse_response(_make_sse_response(events))
        output = capsys.readouterr().out
        assert "Searching documents..." in output

    async def test_malformed_json_ignored(self, session, capsys):
        """Invalid JSON in SSE data line is silently ignored."""

        class ResponseWithBadJson:
            async def aiter_lines(self):
                yield "data: not-json"
                yield 'data: {"type": "RUN_STARTED"}'
                yield 'data: {"type": "TEXT_MESSAGE_START", "messageId": "m1"}'
                yield 'data: {"type": "TEXT_MESSAGE_CONTENT", "messageId": "m1", "delta": "OK"}'
                yield 'data: {"type": "TEXT_MESSAGE_END", "messageId": "m1"}'
                yield 'data: {"type": "RUN_FINISHED", "result": null}'

            async def aclose(self):
                pass

        await session._stream_sse_response(ResponseWithBadJson())
        output = capsys.readouterr().out
        assert "OK" in output
