"""
Tests for MCPClient.call_tool() result parsing logic.

The parsing logic is extracted and tested directly since the mcp package
may not be installed in the test environment. This tests the exact same
branching logic from client.py:303-331.
"""

import asyncio
from unittest.mock import AsyncMock, Mock

import pytest

# Skip if mcp package not available
try:
    import mcp  # noqa: F401

    MCP_AVAILABLE = True
except ImportError:
    MCP_AVAILABLE = False


def _parse_tool_result(result):
    """
    Extract the parsing logic from MCPClient.call_tool() (lines 303-331).

    This is the exact logic from client.py, isolated for unit testing.
    """
    if result.isError:
        return {
            "status": "error",
            "error": str(result.content),
        }

    content = []
    if result.content is not None and hasattr(result.content, "__iter__"):
        for item in result.content:
            if hasattr(item, "text"):
                content.append(item.text)
            elif hasattr(item, "data"):
                content.append(str(item.data))
            else:
                content.append(str(item))
    elif result.content is not None:
        content.append(str(result.content))

    return {
        "status": "success",
        "content": "\n".join(content) if content else "",
        "raw": result.content,
    }


def _make_result(is_error=False, content=None):
    """Create a mock tool result."""
    result = Mock()
    result.isError = is_error
    result.content = content
    return result


class TestErrorResult:
    """Tests for error result handling."""

    def test_error_returns_status_error(self):
        """isError=True → status=error dict."""
        result = _make_result(is_error=True, content=[Mock(text="bad")])
        output = _parse_tool_result(result)
        assert output["status"] == "error"
        assert "error" in output

    def test_error_content_stringified(self):
        """Error content is converted to string."""
        result = _make_result(is_error=True, content="timeout occurred")
        output = _parse_tool_result(result)
        assert "timeout occurred" in output["error"]


class TestTextContent:
    """Tests for success results with .text attributes."""

    def test_text_content_joined_with_newlines(self):
        """Multiple .text items → joined with newlines."""
        items = [
            Mock(text="line1", spec=["text"]),
            Mock(text="line2", spec=["text"]),
        ]
        result = _make_result(content=items)
        output = _parse_tool_result(result)
        assert output["status"] == "success"
        assert output["content"] == "line1\nline2"

    def test_single_text_item(self):
        """Single .text item → correct extraction."""
        items = [Mock(text="only", spec=["text"])]
        result = _make_result(content=items)
        output = _parse_tool_result(result)
        assert output["content"] == "only"


class TestDataContent:
    """Tests for success results with .data attributes."""

    def test_data_content_stringified(self):
        """Items with .data → str(data) joined."""
        item1 = Mock(spec=["data"])
        item1.data = b"bytes1"
        item2 = Mock(spec=["data"])
        item2.data = b"bytes2"
        result = _make_result(content=[item1, item2])
        output = _parse_tool_result(result)
        assert "bytes1" in output["content"]
        assert "bytes2" in output["content"]


class TestMixedContent:
    """Tests for mixed content types."""

    def test_mixed_text_and_data(self):
        """Mixed .text and .data items → all extracted."""
        text_item = Mock(spec=["text"])
        text_item.text = "hello"
        data_item = Mock(spec=["data"])
        data_item.data = "world"
        result = _make_result(content=[text_item, data_item])
        output = _parse_tool_result(result)
        assert "hello" in output["content"]
        assert "world" in output["content"]

    def test_item_without_text_or_data_uses_str(self):
        """Item without .text/.data → str() fallback."""
        result = _make_result(content=["plain string"])
        output = _parse_tool_result(result)
        assert output["content"] == "plain string"


class TestEdgeCases:
    """Edge cases for result parsing."""

    def test_none_content_returns_empty(self):
        """None content → empty string."""
        result = _make_result(content=None)
        output = _parse_tool_result(result)
        assert output["status"] == "success"
        assert output["content"] == ""

    def test_empty_list_returns_empty(self):
        """Empty list → empty string."""
        result = _make_result(content=[])
        output = _parse_tool_result(result)
        assert output["content"] == ""

    def test_non_iterable_content_str_fallback(self):
        """Non-iterable (int) → str conversion."""
        result = _make_result(content=42)
        output = _parse_tool_result(result)
        assert output["content"] == "42"

    def test_bare_string_content_iterates_characters(self):
        """Bare string content iterates char-by-char (documents production behavior).

        A bare string has __iter__, so the code enters the iterable branch
        and iterates character by character. Each char lacks .text/.data,
        so str(char) is used → characters joined with newlines.

        This documents the current behavior. If the production code is fixed
        to check `isinstance(content, str)` first, update this test.
        """
        result = _make_result(content="hello")
        output = _parse_tool_result(result)
        # Current behavior: iterates characters, joins with \n
        assert output["content"] == "h\ne\nl\nl\no"

    def test_result_has_raw_key(self):
        """Output includes 'raw' with original content."""
        items = [Mock(text="data", spec=["text"])]
        result = _make_result(content=items)
        output = _parse_tool_result(result)
        assert output["raw"] is items


@pytest.mark.skipif(not MCP_AVAILABLE, reason="MCP package not installed")
class TestCallToolIntegration:
    """Integration tests that require the actual MCPClient class."""

    @pytest.mark.asyncio
    async def test_not_connected_raises_error(self):
        """Not connected → MCPClientError."""
        from local_deep_research.mcp.client import MCPClient, MCPClientError

        client = MCPClient.__new__(MCPClient)
        client._connected = False
        client._session = None

        with pytest.raises(MCPClientError, match="Not connected"):
            await client.call_tool("tool", {})

    @pytest.mark.asyncio
    async def test_timeout_raises_mcp_error(self):
        """Timeout → MCPClientError."""
        from local_deep_research.mcp.client import MCPClient, MCPClientError

        client = MCPClient.__new__(MCPClient)
        client.name = "test"
        client.timeout = 0.01
        client._connected = True
        client._session = AsyncMock()
        client._session.call_tool = AsyncMock(
            side_effect=asyncio.TimeoutError()
        )

        with pytest.raises(MCPClientError, match="timed out"):
            await client.call_tool("tool", {})
