"""Unit tests for tool registry and create_tool dispatcher.

Generated by our friend.
"""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from ax_prover.tools.lean_search import SearchLeanSearchConfig
from ax_prover.tools.registry import (
    TOOL_REGISTRY,
    ToolRegistration,
    create_tool,
    register_tool,
)
from ax_prover.tools.web_search import SearchWebConfig


class TestRegisterToolDecorator:
    """Tests for the @register_tool decorator."""

    def test_register_tool_adds_to_registry(self):
        """Test that @register_tool populates TOOL_REGISTRY."""
        # The imports in tools/__init__.py trigger the decorators
        assert "search_web" in TOOL_REGISTRY
        assert "search_lean_search" in TOOL_REGISTRY

    def test_registered_entry_has_correct_config_class(self):
        """Test that registered entries have the correct config class."""
        assert TOOL_REGISTRY["search_web"].config_class is SearchWebConfig
        assert TOOL_REGISTRY["search_lean_search"].config_class is SearchLeanSearchConfig

    def test_registered_entry_has_callable_factory(self):
        """Test that registered entries have callable factories."""
        assert callable(TOOL_REGISTRY["search_web"].factory)
        assert callable(TOOL_REGISTRY["search_lean_search"].factory)

    def test_duplicate_registration_raises(self):
        """Test that registering the same tool_type twice raises ValueError."""
        with pytest.raises(ValueError, match="Duplicate tool registration"):

            @register_tool("search_web", SearchWebConfig)
            def duplicate_factory(config):
                pass


class TestCreateTool:
    """Tests for the create_tool dispatcher."""

    @pytest.mark.asyncio
    async def test_create_tool_missing_tool_type(self):
        """Test that missing tool_type raises ValueError."""
        with pytest.raises(ValueError, match="missing 'tool_type'"):
            await create_tool({"max_results": 3})

    @pytest.mark.asyncio
    async def test_create_tool_unknown_tool_type(self):
        """Test that unknown tool_type raises ValueError."""
        with pytest.raises(ValueError, match="Unknown tool_type: 'nonexistent'"):
            await create_tool({"tool_type": "nonexistent"})

    @pytest.mark.asyncio
    async def test_create_tool_builds_typed_config(self):
        """Test that create_tool constructs the typed config from dict."""
        mock_tool = MagicMock()
        mock_factory = MagicMock(return_value=mock_tool)

        with patch.dict(
            TOOL_REGISTRY,
            {"test_tool": ToolRegistration(factory=mock_factory, config_class=SearchWebConfig)},
        ):
            result = await create_tool(
                {"tool_type": "test_tool", "max_results": 5, "timeout": 20},
            )

        assert result is mock_tool
        # Verify factory was called with a typed config
        config = mock_factory.call_args[0][0]
        assert isinstance(config, SearchWebConfig)
        assert config.max_results == 5
        assert config.timeout == 20

    @pytest.mark.asyncio
    async def test_create_tool_handles_async_factory(self):
        """Test that create_tool awaits async factories."""
        mock_tool = MagicMock()
        mock_factory = AsyncMock(return_value=mock_tool)

        with patch.dict(
            TOOL_REGISTRY,
            {"test_tool": ToolRegistration(factory=mock_factory, config_class=SearchWebConfig)},
        ):
            result = await create_tool({"tool_type": "test_tool"})

        assert result is mock_tool
        mock_factory.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_create_tool_returns_none_from_factory(self):
        """Test that create_tool returns None when factory returns None."""
        mock_factory = AsyncMock(return_value=None)

        with patch.dict(
            TOOL_REGISTRY,
            {"test_tool": ToolRegistration(factory=mock_factory, config_class=SearchWebConfig)},
        ):
            result = await create_tool({"tool_type": "test_tool"})

        assert result is None

    @pytest.mark.asyncio
    async def test_create_tool_invalid_config_params(self):
        """Test that invalid config parameters raise TypeError."""
        with patch.dict(
            TOOL_REGISTRY,
            {"test_tool": ToolRegistration(factory=MagicMock(), config_class=SearchWebConfig)},
        ):
            with pytest.raises(TypeError):
                await create_tool({"tool_type": "test_tool", "nonexistent_param": 42})

    @pytest.mark.asyncio
    async def test_create_tool_uses_config_defaults(self):
        """Test that omitted config fields use dataclass defaults."""
        mock_tool = MagicMock()
        mock_factory = MagicMock(return_value=mock_tool)

        with patch.dict(
            TOOL_REGISTRY,
            {"test_tool": ToolRegistration(factory=mock_factory, config_class=SearchWebConfig)},
        ):
            await create_tool({"tool_type": "test_tool"})

        config = mock_factory.call_args[0][0]
        assert isinstance(config, SearchWebConfig)
        # Defaults from SearchWebConfig dataclass
        assert config.max_results == 3
        assert config.timeout == 10
        assert config.max_content_length == 3000

    @pytest.mark.asyncio
    async def test_create_tool_does_not_mutate_input(self):
        """Test that create_tool does not modify the input dict."""
        mock_factory = MagicMock(return_value=MagicMock())
        original = {"tool_type": "test_tool", "max_results": 5}
        input_copy = dict(original)

        with patch.dict(
            TOOL_REGISTRY,
            {"test_tool": ToolRegistration(factory=mock_factory, config_class=SearchWebConfig)},
        ):
            await create_tool(input_copy)

        assert input_copy == original
