"""Tests for gfp_mcp.tools.base module.

These tests verify the ToolHandler base class and EndpointMapping.
"""

from __future__ import annotations

import json
from typing import Any
from unittest.mock import AsyncMock, MagicMock

import pytest
from mcp.types import TextContent, Tool

from gfp_mcp.tools.base import EndpointMapping, ToolHandler


class TestEndpointMapping:
    """Tests for EndpointMapping dataclass."""

    def test_create_mapping(self) -> None:
        """Test creating a basic endpoint mapping."""
        mapping = EndpointMapping(method="GET", path="/api/test")

        assert mapping.method == "GET"
        assert mapping.path == "/api/test"

    def test_post_mapping(self) -> None:
        """Test creating a POST endpoint mapping."""
        mapping = EndpointMapping(method="POST", path="/api/build-cells")

        assert mapping.method == "POST"
        assert mapping.path == "/api/build-cells"

    def test_mapping_with_path_params(self) -> None:
        """Test mapping with path parameters."""
        mapping = EndpointMapping(method="POST", path="/api/sax/{name}/simulate")

        assert mapping.method == "POST"
        assert "{name}" in mapping.path


class ConcreteToolHandler(ToolHandler):
    """Concrete implementation of ToolHandler for testing."""

    def __init__(
        self,
        tool_name: str = "test_tool",
        endpoint_mapping: EndpointMapping | None = None,
    ) -> None:
        self._name = tool_name
        self._mapping = endpoint_mapping

    @property
    def name(self) -> str:
        return self._name

    @property
    def definition(self) -> Tool:
        return Tool(
            name=self._name,
            description=f"Test tool: {self._name}",
            inputSchema={
                "type": "object",
                "properties": {
                    "param1": {"type": "string", "description": "Test parameter"},
                },
                "required": ["param1"],
            },
        )

    @property
    def mapping(self) -> EndpointMapping | None:
        return self._mapping


class CustomTransformHandler(ConcreteToolHandler):
    """Handler with custom request/response transformers."""

    def transform_request(self, args: dict[str, Any]) -> dict[str, Any]:
        return {
            "params": {"name": args.get("param1", "default")},
            "json_data": {"extra": "data"},
        }

    def transform_response(self, response: Any) -> dict[str, Any]:
        return {"transformed": True, "original": response}


class TestToolHandler:
    """Tests for ToolHandler abstract base class."""

    def test_name_property(self) -> None:
        """Test the name property."""
        handler = ConcreteToolHandler(tool_name="my_tool")
        assert handler.name == "my_tool"

    def test_definition_property(self) -> None:
        """Test the definition property returns valid Tool."""
        handler = ConcreteToolHandler(tool_name="test_tool")
        definition = handler.definition

        assert isinstance(definition, Tool)
        assert definition.name == "test_tool"
        assert "Test tool" in definition.description
        assert definition.inputSchema["type"] == "object"

    def test_mapping_default_none(self) -> None:
        """Test that mapping returns None by default."""
        handler = ConcreteToolHandler()
        assert handler.mapping is None

    def test_mapping_with_value(self) -> None:
        """Test handler with endpoint mapping."""
        mapping = EndpointMapping(method="GET", path="/api/test")
        handler = ConcreteToolHandler(endpoint_mapping=mapping)

        assert handler.mapping is not None
        assert handler.mapping.method == "GET"
        assert handler.mapping.path == "/api/test"

    def test_default_transform_request(self) -> None:
        """Test default transform_request returns empty dict."""
        handler = ConcreteToolHandler()
        result = handler.transform_request({"param1": "value"})
        assert result == {}

    def test_default_transform_response(self) -> None:
        """Test default transform_response returns input unchanged."""
        handler = ConcreteToolHandler()
        response = {"key": "value", "nested": {"data": 123}}
        result = handler.transform_response(response)
        assert result == response

    def test_custom_transform_request(self) -> None:
        """Test custom transform_request implementation."""
        handler = CustomTransformHandler()
        result = handler.transform_request({"param1": "test_value"})

        assert result == {
            "params": {"name": "test_value"},
            "json_data": {"extra": "data"},
        }

    def test_custom_transform_response(self) -> None:
        """Test custom transform_response implementation."""
        handler = CustomTransformHandler()
        result = handler.transform_response({"original": "data"})

        assert result == {
            "transformed": True,
            "original": {"original": "data"},
        }

    @pytest.mark.anyio
    async def test_handle_no_mapping(self) -> None:
        """Test handle returns error when no mapping defined."""
        handler = ConcreteToolHandler()
        mock_client = MagicMock()

        result = await handler.handle({"param1": "value"}, mock_client)

        assert len(result) == 1
        assert isinstance(result[0], TextContent)
        response_data = json.loads(result[0].text)
        assert "error" in response_data
        assert "no endpoint mapping" in response_data["error"]

    @pytest.mark.anyio
    async def test_handle_successful_request(self) -> None:
        """Test handle makes successful HTTP request."""
        mapping = EndpointMapping(method="GET", path="/api/test")
        handler = CustomTransformHandler(endpoint_mapping=mapping)

        mock_client = AsyncMock()
        mock_client.request = AsyncMock(return_value={"status": "ok"})

        result = await handler.handle({"param1": "value"}, mock_client)

        assert len(result) == 1
        assert isinstance(result[0], TextContent)
        response_data = json.loads(result[0].text)

        # Should have transformed response
        assert response_data["transformed"] is True
        assert response_data["original"] == {"status": "ok"}

        # Verify request was made correctly
        mock_client.request.assert_called_once_with(
            method="GET",
            path="/api/test",
            params={"name": "value"},
            json_data={"extra": "data"},
            data=None,
            project=None,
        )

    @pytest.mark.anyio
    async def test_handle_with_project_routing(self) -> None:
        """Test handle passes project parameter for routing."""
        mapping = EndpointMapping(method="POST", path="/api/build")
        handler = ConcreteToolHandler(endpoint_mapping=mapping)

        mock_client = AsyncMock()
        mock_client.request = AsyncMock(return_value={"built": True})

        result = await handler.handle(
            {"param1": "value", "project": "my_project"}, mock_client
        )

        assert len(result) == 1
        # Verify project was passed to request
        call_kwargs = mock_client.request.call_args.kwargs
        assert call_kwargs["project"] == "my_project"

    @pytest.mark.anyio
    async def test_handle_http_error(self) -> None:
        """Test handle returns error on HTTP failure."""
        import httpx

        mapping = EndpointMapping(method="GET", path="/api/test")
        handler = ConcreteToolHandler(endpoint_mapping=mapping)

        mock_response = MagicMock()
        mock_response.status_code = 500
        mock_response.text = "Internal Server Error"

        mock_client = AsyncMock()
        mock_client.request = AsyncMock(
            side_effect=httpx.HTTPStatusError(
                "Server Error", request=MagicMock(), response=mock_response
            )
        )

        result = await handler.handle({"param1": "value"}, mock_client)

        assert len(result) == 1
        response_data = json.loads(result[0].text)
        assert "error" in response_data

    @pytest.mark.anyio
    async def test_handle_404_with_fallback(self) -> None:
        """Test handle uses fallback endpoint on 404."""
        import httpx

        mapping = EndpointMapping(method="POST", path="/api/new-endpoint")
        handler = ConcreteToolHandler(endpoint_mapping=mapping)

        # Override transform_request to include fallback
        def custom_transform(args: dict[str, Any]) -> dict[str, Any]:
            return {
                "json_data": {"name": args.get("param1")},
                "fallback_path": "/api/old-endpoint",
                "fallback_json_data": {"legacy_name": args.get("param1")},
            }

        handler.transform_request = custom_transform

        mock_response_404 = MagicMock()
        mock_response_404.status_code = 404

        mock_client = AsyncMock()
        # First call raises 404, second call succeeds
        mock_client.request = AsyncMock(
            side_effect=[
                httpx.HTTPStatusError(
                    "Not Found", request=MagicMock(), response=mock_response_404
                ),
                {"fallback": "success"},
            ]
        )

        result = await handler.handle({"param1": "value"}, mock_client)

        assert len(result) == 1
        response_data = json.loads(result[0].text)
        assert response_data == {"fallback": "success"}

        # Verify fallback was called
        assert mock_client.request.call_count == 2
        second_call = mock_client.request.call_args_list[1]
        assert second_call.kwargs["path"] == "/api/old-endpoint"
        assert second_call.kwargs["json_data"] == {"legacy_name": "value"}


class TestToolHandlerInheritance:
    """Tests for proper inheritance behavior."""

    def test_abstract_methods_required(self) -> None:
        """Test that abstract methods must be implemented."""

        class IncompleteHandler(ToolHandler):
            pass

        with pytest.raises(TypeError):
            IncompleteHandler()  # type: ignore[abstract]

    def test_partial_implementation_fails(self) -> None:
        """Test that partial implementation still fails."""

        class PartialHandler(ToolHandler):
            @property
            def name(self) -> str:
                return "partial"

            # Missing definition property

        with pytest.raises(TypeError):
            PartialHandler()  # type: ignore[abstract]


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
