"""Tests for ToolsResource."""

from __future__ import annotations

from typing import Any
from unittest.mock import AsyncMock
from uuid import uuid4

import pytest

from mcpgateway_sdk.models.tool import ToolExecuteResult, ToolLookup, ToolSearchResult
from mcpgateway_sdk.resources.tools import ToolsResource


@pytest.fixture
def http() -> AsyncMock:
    """Mock HTTPClient."""
    return AsyncMock()


@pytest.fixture
def tools(http: AsyncMock) -> ToolsResource:
    """ToolsResource with mocked HTTP."""
    return ToolsResource(http)


def _tool_lookup_data() -> dict[str, Any]:
    return {
        "tool_id": str(uuid4()),
        "tool_name": "get_weather",
        "description": "Get current weather",
        "server_id": str(uuid4()),
        "server_name": "weather-server",
        "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}},
    }


def _search_result_data(score: float = 0.95) -> dict[str, Any]:
    return {
        "tool_id": str(uuid4()),
        "tool_name": "get_weather",
        "description": "Get current weather",
        "server_id": str(uuid4()),
        "server_name": "weather-server",
        "score": score,
        "input_schema": {},
    }


def _execute_result_data() -> dict[str, Any]:
    return {
        "result": {"temperature": 72, "unit": "fahrenheit"},
        "server_id": str(uuid4()),
        "tool_name": "get_weather",
        "duration_ms": 150,
        "error": None,
    }


class TestEndpointsDict:
    """Tests for _endpoints class variable."""

    def test_endpoints_has_get_by_name(self) -> None:
        assert ToolsResource._endpoints["get_by_name"] == ("GET", "/api/v1/tools/by-name")

    def test_endpoints_has_search(self) -> None:
        assert ToolsResource._endpoints["search"] == ("POST", "/api/v1/tools/search")

    def test_endpoints_has_execute(self) -> None:
        assert ToolsResource._endpoints["execute"] == ("POST", "/api/v1/tools/execute")

    def test_endpoints_has_exactly_three_entries(self) -> None:
        assert len(ToolsResource._endpoints) == 3


class TestGetByName:
    """Tests for ToolsResource.get_by_name."""

    async def test_sends_get_with_server_and_tool_name_params(
        self, tools: ToolsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _tool_lookup_data()

        # Act
        await tools.get_by_name("weather-server", "get_weather")

        # Assert
        http.request.assert_awaited_once_with(
            "GET",
            "/api/v1/tools/by-name",
            params={"server_name": "weather-server", "tool_name": "get_weather"},
        )

    async def test_returns_tool_lookup_model(self, tools: ToolsResource, http: AsyncMock) -> None:
        # Arrange
        data = _tool_lookup_data()
        http.request.return_value = data

        # Act
        result = await tools.get_by_name("weather-server", "get_weather")

        # Assert
        assert isinstance(result, ToolLookup)
        assert result.tool_name == "get_weather"
        assert result.description == "Get current weather"
        assert result.server_name == "weather-server"


class TestSearch:
    """Tests for ToolsResource.search."""

    async def test_sends_post_with_query_and_limit(
        self, tools: ToolsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {"tools": []}

        # Act
        await tools.search("weather forecast", limit=10)

        # Assert
        http.request.assert_awaited_once_with(
            "POST", "/api/v1/tools/search", json={"query": "weather forecast", "limit": 10}
        )

    async def test_default_limit_is_five(self, tools: ToolsResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = {"tools": []}

        # Act
        await tools.search("weather")

        # Assert
        http.request.assert_awaited_once_with(
            "POST", "/api/v1/tools/search", json={"query": "weather", "limit": 5}
        )

    async def test_returns_list_of_search_results(
        self, tools: ToolsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {
            "tools": [_search_result_data(0.95), _search_result_data(0.80)]
        }

        # Act
        results = await tools.search("weather")

        # Assert
        assert len(results) == 2
        assert all(isinstance(r, ToolSearchResult) for r in results)
        assert results[0].score == 0.95
        assert results[1].score == 0.80

    async def test_empty_results_returns_empty_list(
        self, tools: ToolsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {"tools": []}

        # Act
        results = await tools.search("nonexistent")

        # Assert
        assert results == []

    async def test_missing_tools_key_returns_empty_list(
        self, tools: ToolsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {}

        # Act
        results = await tools.search("weather")

        # Assert
        assert results == []


class TestExecute:
    """Tests for ToolsResource.execute."""

    async def test_sends_post_with_server_name_and_arguments(
        self, tools: ToolsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _execute_result_data()

        # Act
        await tools.execute(
            "get_weather", arguments={"city": "NYC"}, server_name="weather-server"
        )

        # Assert
        http.request.assert_awaited_once_with(
            "POST",
            "/api/v1/tools/execute",
            json={
                "tool_name": "get_weather",
                "arguments": {"city": "NYC"},
                "server_name": "weather-server",
            },
        )

    async def test_sends_post_with_server_id(
        self, tools: ToolsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _execute_result_data()
        sid = uuid4()

        # Act
        await tools.execute("get_weather", arguments={"city": "NYC"}, server_id=sid)

        # Assert
        http.request.assert_awaited_once_with(
            "POST",
            "/api/v1/tools/execute",
            json={
                "tool_name": "get_weather",
                "arguments": {"city": "NYC"},
                "server_id": str(sid),
            },
        )

    async def test_none_arguments_sends_empty_dict(
        self, tools: ToolsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _execute_result_data()

        # Act
        await tools.execute("get_time", server_name="time-server")

        # Assert
        http.request.assert_awaited_once_with(
            "POST",
            "/api/v1/tools/execute",
            json={
                "tool_name": "get_time",
                "arguments": {},
                "server_name": "time-server",
            },
        )

    async def test_returns_execute_result_model(
        self, tools: ToolsResource, http: AsyncMock
    ) -> None:
        # Arrange
        data = _execute_result_data()
        http.request.return_value = data

        # Act
        result = await tools.execute(
            "get_weather", arguments={"city": "NYC"}, server_name="weather-server"
        )

        # Assert
        assert isinstance(result, ToolExecuteResult)
        assert result.tool_name == "get_weather"
        assert result.duration_ms == 150
        assert result.result == {"temperature": 72, "unit": "fahrenheit"}
        assert result.error is None

    async def test_execute_with_error_response(
        self, tools: ToolsResource, http: AsyncMock
    ) -> None:
        # Arrange
        data = _execute_result_data()
        data["error"] = "Tool execution timed out"
        data["result"] = None
        http.request.return_value = data

        # Act
        result = await tools.execute(
            "get_weather", arguments={"city": "NYC"}, server_name="weather-server"
        )

        # Assert
        assert result.error == "Tool execution timed out"
        assert result.result is None

    async def test_raises_if_both_server_id_and_server_name(
        self, tools: ToolsResource, http: AsyncMock
    ) -> None:
        with pytest.raises(ValueError, match="not both"):
            await tools.execute(
                "get_weather", server_id=uuid4(), server_name="weather-server"
            )

    async def test_raises_if_neither_server_id_nor_server_name(
        self, tools: ToolsResource, http: AsyncMock
    ) -> None:
        with pytest.raises(ValueError, match="exactly one"):
            await tools.execute("get_weather")

    async def test_server_id_string_accepted(
        self, tools: ToolsResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _execute_result_data()
        sid = str(uuid4())

        # Act
        await tools.execute("get_weather", server_id=sid)

        # Assert
        body = http.request.call_args.kwargs["json"]
        assert body["server_id"] == sid
