"""Tests for the mcp.py module in serena."""

import pytest
from mcp.server.fastmcp.tools.base import Tool as MCPTool

from serena.agent import Tool, ToolRegistry
from serena.config.context_mode import SerenaAgentContext
from serena.mcp import SerenaMCPFactory

make_tool = SerenaMCPFactory.make_mcp_tool


# Create a mock agent for tool initialization
class MockAgent:
    def __init__(self):
        self.project_config = None
        self.serena_config = None

    @staticmethod
    def get_context() -> SerenaAgentContext:
        return SerenaAgentContext.load_default()


class BaseMockTool(Tool):
    """A mock Tool class for testing."""

    def __init__(self):
        super().__init__(MockAgent())  # type: ignore


class BasicTool(BaseMockTool):
    """A mock Tool class for testing."""

    def apply(self, name: str, age: int = 0) -> str:
        """This is a test function.

        :param name: The person's name
        :param age: The person's age
        :return: A greeting message
        """
        return f"Hello {name}, you are {age} years old!"

    def apply_ex(
        self,
        log_call: bool = True,
        catch_exceptions: bool = True,
        **kwargs,
    ) -> str:
        """Mock implementation of apply_ex."""
        return self.apply(**kwargs)


def test_make_tool_basic() -> None:
    """Test that make_tool correctly creates an MCP tool from a Tool object."""
    mock_tool = BasicTool()

    mcp_tool = make_tool(mock_tool)

    # Test that the MCP tool has the correct properties
    assert isinstance(mcp_tool, MCPTool)
    assert mcp_tool.name == "basic"
    assert "This is a test function. Returns A greeting message." in mcp_tool.description

    # Test that the parameters were correctly processed
    parameters = mcp_tool.parameters
    assert "properties" in parameters
    assert "name" in parameters["properties"]
    assert "age" in parameters["properties"]
    assert parameters["properties"]["name"]["description"] == "The person's name."
    assert parameters["properties"]["age"]["description"] == "The person's age."


def test_make_tool_execution() -> None:
    """Test that the execution function created by make_tool works correctly."""
    mock_tool = BasicTool()
    mcp_tool = make_tool(mock_tool)

    # Execute the MCP tool function
    result = mcp_tool.fn(name="Alice", age=30)

    assert result == "Hello Alice, you are 30 years old!"


def test_make_tool_no_params() -> None:
    """Test make_tool with a function that has no parameters."""

    class NoParamsTool(BaseMockTool):
        def apply(self) -> str:
            """This is a test function with no parameters.

            :return: A simple result
            """
            return "Simple result"

        def apply_ex(self, *args, **kwargs) -> str:
            return self.apply()

    tool = NoParamsTool()
    mcp_tool = make_tool(tool)

    assert mcp_tool.name == "no_params"
    assert "This is a test function with no parameters. Returns A simple result." in mcp_tool.description
    assert mcp_tool.parameters["properties"] == {}


def test_make_tool_no_return_description() -> None:
    """Test make_tool with a function that has no return description."""

    class NoReturnTool(BaseMockTool):
        def apply(self, param: str) -> str:
            """This is a test function.

            :param param: The parameter
            """
            return f"Processed: {param}"

        def apply_ex(self, *args, **kwargs) -> str:
            return self.apply(**kwargs)

    tool = NoReturnTool()
    mcp_tool = make_tool(tool)

    assert mcp_tool.name == "no_return"
    assert mcp_tool.description == "This is a test function."
    assert mcp_tool.parameters["properties"]["param"]["description"] == "The parameter."


def test_make_tool_parameter_not_in_docstring() -> None:
    """Test make_tool when a parameter in properties is not in the docstring."""

    class MissingParamTool(BaseMockTool):
        def apply(self, name: str, missing_param: str = "") -> str:
            """This is a test function.

            :param name: The person's name
            """
            return f"Hello {name}! Missing param: {missing_param}"

        def apply_ex(self, *args, **kwargs) -> str:
            return self.apply(**kwargs)

    tool = MissingParamTool()
    mcp_tool = make_tool(tool)

    assert "name" in mcp_tool.parameters["properties"]
    assert "missing_param" in mcp_tool.parameters["properties"]
    assert mcp_tool.parameters["properties"]["name"]["description"] == "The person's name."
    assert "description" not in mcp_tool.parameters["properties"]["missing_param"]


def test_make_tool_multiline_docstring() -> None:
    """Test make_tool with a complex multi-line docstring."""

    class ComplexDocTool(BaseMockTool):
        def apply(self, project_file_path: str, host: str, port: int) -> str:
            """Create an MCP server.

            This function creates and configures a Model Context Protocol server
            with the specified settings.

            :param project_file_path: The path to the project file, or None
            :param host: The host to bind to
            :param port: The port to bind to
            :return: A configured FastMCP server instance
            """
            return f"Server config: {project_file_path}, {host}:{port}"

        def apply_ex(self, *args, **kwargs) -> str:
            return self.apply(**kwargs)

    tool = ComplexDocTool()
    mcp_tool = make_tool(tool)

    assert "Create an MCP server" in mcp_tool.description
    assert "Returns A configured FastMCP server instance" in mcp_tool.description
    assert mcp_tool.parameters["properties"]["project_file_path"]["description"] == "The path to the project file, or None."
    assert mcp_tool.parameters["properties"]["host"]["description"] == "The host to bind to."
    assert mcp_tool.parameters["properties"]["port"]["description"] == "The port to bind to."


def test_make_tool_capitalization_and_periods() -> None:
    """Test that make_tool properly handles capitalization and periods in descriptions."""

    class FormatTool(BaseMockTool):
        def apply(self, param1: str, param2: str, param3: str) -> str:
            """Test function.

            :param param1: lowercase description
            :param param2: description with period.
            :param param3: description with Capitalized word.
            """
            return f"Formatted: {param1}, {param2}, {param3}"

        def apply_ex(self, *args, **kwargs) -> str:
            return self.apply(**kwargs)

    tool = FormatTool()
    mcp_tool = make_tool(tool)

    assert mcp_tool.parameters["properties"]["param1"]["description"] == "Lowercase description."
    assert mcp_tool.parameters["properties"]["param2"]["description"] == "Description with period."
    assert mcp_tool.parameters["properties"]["param3"]["description"] == "Description with Capitalized word."


def test_make_tool_missing_apply() -> None:
    """Test make_tool with a tool that doesn't have an apply method."""

    class BadTool(BaseMockTool):
        pass

    tool = BadTool()

    with pytest.raises(AttributeError):
        make_tool(tool)


@pytest.mark.parametrize(
    "docstring, expected_description",
    [
        (
            """This is a test function.

            :param param: The parameter
            :return: A result
            """,
            "This is a test function. Returns A result.",
        ),
        (
            """
            :param param: The parameter
            :return: A result
            """,
            "Returns A result.",
        ),
        (
            """
            :param param: The parameter
            """,
            "",
        ),
        ("Description without params.", "Description without params."),
    ],
)
def test_make_tool_descriptions(docstring, expected_description) -> None:
    """Test make_tool with various docstring formats."""

    class TestTool(BaseMockTool):
        def apply(self, param: str) -> str:
            return f"Result: {param}"

        def apply_ex(self, *args, **kwargs) -> str:
            return self.apply(**kwargs)

    # Dynamically set the docstring
    TestTool.apply.__doc__ = docstring

    tool = TestTool()
    mcp_tool = make_tool(tool)

    assert mcp_tool.name == "test"
    assert mcp_tool.description == expected_description


def is_test_mock_class(tool_class: type) -> bool:
    """Check if a class is a test mock class."""
    # Check if the class is defined in a test module
    module_name = tool_class.__module__
    return (
        module_name.startswith(("test.", "tests."))
        or "test_" in module_name
        or tool_class.__name__
        in [
            "BaseMockTool",
            "BasicTool",
            "BadTool",
            "NoParamsTool",
            "NoReturnTool",
            "MissingParamTool",
            "ComplexDocTool",
            "FormatTool",
            "NoDescriptionTool",
        ]
    )


@pytest.mark.parametrize("tool_class", ToolRegistry().get_all_tool_classes())
def test_make_tool_all_tools(tool_class) -> None:
    """Test that make_tool works for all tools in the codebase."""
    # Create an instance of the tool
    tool_instance = tool_class(MockAgent())

    # Try to create an MCP tool from it
    mcp_tool = make_tool(tool_instance)

    # Basic validation
    assert isinstance(mcp_tool, MCPTool)
    assert mcp_tool.name == tool_class.get_name_from_cls()

    # The description should be a string (either from docstring or default)
    assert isinstance(mcp_tool.description, str)
