import json
import logging
from dataclasses import dataclass
from typing import Any, TypedDict

import pytest
from mcp_types import CallToolResult, TextContent, ToolAnnotations
from pydantic import BaseModel

from mcp.server.context import LifespanContextT, RequestT
from mcp.server.mcpserver import Context, MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.server.mcpserver.tools import Tool, ToolManager
from mcp.server.mcpserver.utilities.func_metadata import ArgModelBase, FuncMetadata


class TestAddTools:
    def test_basic_function(self):
        """Test registering and running a basic function."""

        def sum(a: int, b: int) -> int:  # pragma: no cover
            """Add two numbers."""
            return a + b

        manager = ToolManager()
        manager.add_tool(sum)

        tool = manager.get_tool("sum")
        assert tool is not None
        assert tool.name == "sum"
        assert tool.description == "Add two numbers."
        assert tool.is_async is False
        assert tool.parameters["properties"]["a"]["type"] == "integer"
        assert tool.parameters["properties"]["b"]["type"] == "integer"

    def test_init_with_tools(self, caplog: pytest.LogCaptureFixture):
        def sum(a: int, b: int) -> int:  # pragma: no cover
            return a + b

        class AddArguments(ArgModelBase):
            a: int
            b: int

        fn_metadata = FuncMetadata(arg_model=AddArguments)

        original_tool = Tool(
            name="sum",
            title="Add Tool",
            description="Add two numbers.",
            fn=sum,
            fn_metadata=fn_metadata,
            is_async=False,
            parameters=AddArguments.model_json_schema(),
            context_kwarg=None,
            annotations=None,
        )
        manager = ToolManager(tools=[original_tool])
        saved_tool = manager.get_tool("sum")
        assert saved_tool == original_tool

        # warn on duplicate tools
        with caplog.at_level(logging.WARNING):
            manager = ToolManager(True, tools=[original_tool, original_tool])
            assert "Tool already exists: sum" in caplog.text

    @pytest.mark.anyio
    async def test_async_function(self):
        """Test registering and running an async function."""

        async def fetch_data(url: str) -> str:  # pragma: no cover
            """Fetch data from URL."""
            return f"Data from {url}"

        manager = ToolManager()
        manager.add_tool(fetch_data)

        tool = manager.get_tool("fetch_data")
        assert tool is not None
        assert tool.name == "fetch_data"
        assert tool.description == "Fetch data from URL."
        assert tool.is_async is True
        assert tool.parameters["properties"]["url"]["type"] == "string"

    def test_pydantic_model_function(self):
        """Test registering a function that takes a Pydantic model."""

        class UserInput(BaseModel):
            name: str
            age: int

        def create_user(user: UserInput, flag: bool) -> dict[str, Any]:  # pragma: no cover
            """Create a new user."""
            return {"id": 1, **user.model_dump()}

        manager = ToolManager()
        manager.add_tool(create_user)

        tool = manager.get_tool("create_user")
        assert tool is not None
        assert tool.name == "create_user"
        assert tool.description == "Create a new user."
        assert tool.is_async is False
        assert "name" in tool.parameters["$defs"]["UserInput"]["properties"]
        assert "age" in tool.parameters["$defs"]["UserInput"]["properties"]
        assert "flag" in tool.parameters["properties"]

    def test_add_callable_object(self):
        """Test registering a callable object."""

        class MyTool:
            def __init__(self):
                self.__name__ = "MyTool"

            def __call__(self, x: int) -> int:  # pragma: no cover
                return x * 2

        manager = ToolManager()
        tool = manager.add_tool(MyTool())
        assert tool.name == "MyTool"
        assert tool.is_async is False
        assert tool.parameters["properties"]["x"]["type"] == "integer"

    @pytest.mark.anyio
    async def test_add_async_callable_object(self):
        """Test registering an async callable object."""

        class MyAsyncTool:
            def __init__(self):
                self.__name__ = "MyAsyncTool"

            async def __call__(self, x: int) -> int:  # pragma: no cover
                return x * 2

        manager = ToolManager()
        tool = manager.add_tool(MyAsyncTool())
        assert tool.name == "MyAsyncTool"
        assert tool.is_async is True
        assert tool.parameters["properties"]["x"]["type"] == "integer"

    def test_add_invalid_tool(self):
        manager = ToolManager()
        with pytest.raises(AttributeError):
            manager.add_tool(1)  # type: ignore

    def test_add_lambda(self):
        manager = ToolManager()
        tool = manager.add_tool(lambda x: x, name="my_tool")  # type: ignore[reportUnknownLambdaType]
        assert tool.name == "my_tool"

    def test_add_lambda_with_no_name(self):
        manager = ToolManager()
        with pytest.raises(ValueError, match="You must provide a name for lambda functions"):
            manager.add_tool(lambda x: x)  # type: ignore[reportUnknownLambdaType]

    def test_warn_on_duplicate_tools(self, caplog: pytest.LogCaptureFixture):
        """Test warning on duplicate tools."""

        def f(x: int) -> int:  # pragma: no cover
            return x

        manager = ToolManager()
        manager.add_tool(f)
        with caplog.at_level(logging.WARNING):
            manager.add_tool(f)
            assert "Tool already exists: f" in caplog.text

    def test_disable_warn_on_duplicate_tools(self, caplog: pytest.LogCaptureFixture):
        """Test disabling warning on duplicate tools."""

        def f(x: int) -> int:  # pragma: no cover
            return x

        manager = ToolManager()
        manager.add_tool(f)
        manager.warn_on_duplicate_tools = False
        with caplog.at_level(logging.WARNING):
            manager.add_tool(f)
            assert "Tool already exists: f" not in caplog.text


class TestCallTools:
    @pytest.mark.anyio
    async def test_call_tool(self):
        def sum(a: int, b: int) -> int:
            """Add two numbers."""
            return a + b

        manager = ToolManager()
        manager.add_tool(sum)
        result = await manager.call_tool("sum", {"a": 1, "b": 2}, Context())
        assert result == 3

    @pytest.mark.anyio
    async def test_call_async_tool(self):
        async def double(n: int) -> int:
            """Double a number."""
            return n * 2

        manager = ToolManager()
        manager.add_tool(double)
        result = await manager.call_tool("double", {"n": 5}, Context())
        assert result == 10

    @pytest.mark.anyio
    async def test_call_object_tool(self):
        class MyTool:
            def __init__(self):
                self.__name__ = "MyTool"

            def __call__(self, x: int) -> int:
                return x * 2

        manager = ToolManager()
        tool = manager.add_tool(MyTool())
        result = await tool.run({"x": 5}, Context())
        assert result == 10

    @pytest.mark.anyio
    async def test_call_async_object_tool(self):
        class MyAsyncTool:
            def __init__(self):
                self.__name__ = "MyAsyncTool"

            async def __call__(self, x: int) -> int:
                return x * 2

        manager = ToolManager()
        tool = manager.add_tool(MyAsyncTool())
        result = await tool.run({"x": 5}, Context())
        assert result == 10

    @pytest.mark.anyio
    async def test_call_tool_with_default_args(self):
        def sum(a: int, b: int = 1) -> int:
            """Add two numbers."""
            return a + b

        manager = ToolManager()
        manager.add_tool(sum)
        result = await manager.call_tool("sum", {"a": 1}, Context())
        assert result == 2

    @pytest.mark.anyio
    async def test_call_tool_with_missing_args(self):
        def sum(a: int, b: int) -> int:  # pragma: no cover
            """Add two numbers."""
            return a + b

        manager = ToolManager()
        manager.add_tool(sum)
        with pytest.raises(ToolError):
            await manager.call_tool("sum", {"a": 1}, Context())

    @pytest.mark.anyio
    async def test_call_unknown_tool(self):
        manager = ToolManager()
        with pytest.raises(ToolError):
            await manager.call_tool("unknown", {"a": 1}, Context())

    @pytest.mark.anyio
    async def test_call_tool_with_list_int_input(self):
        def sum_vals(vals: list[int]) -> int:
            return sum(vals)

        manager = ToolManager()
        manager.add_tool(sum_vals)
        # Try both with plain list and with JSON list
        result = await manager.call_tool("sum_vals", {"vals": "[1, 2, 3]"}, Context())
        assert result == 6
        result = await manager.call_tool("sum_vals", {"vals": [1, 2, 3]}, Context())
        assert result == 6

    @pytest.mark.anyio
    async def test_call_tool_with_list_str_or_str_input(self):
        def concat_strs(vals: list[str] | str) -> str:
            return vals if isinstance(vals, str) else "".join(vals)

        manager = ToolManager()
        manager.add_tool(concat_strs)
        # Try both with plain python object and with JSON list
        result = await manager.call_tool("concat_strs", {"vals": ["a", "b", "c"]}, Context())
        assert result == "abc"
        result = await manager.call_tool("concat_strs", {"vals": '["a", "b", "c"]'}, Context())
        assert result == "abc"
        result = await manager.call_tool("concat_strs", {"vals": "a"}, Context())
        assert result == "a"
        result = await manager.call_tool("concat_strs", {"vals": '"a"'}, Context())
        assert result == '"a"'

    @pytest.mark.anyio
    async def test_call_tool_with_complex_model(self):
        class MyShrimpTank(BaseModel):
            class Shrimp(BaseModel):
                name: str

            shrimp: list[Shrimp]
            x: None

        def name_shrimp(tank: MyShrimpTank) -> list[str]:
            return [x.name for x in tank.shrimp]

        manager = ToolManager()
        manager.add_tool(name_shrimp)
        result = await manager.call_tool(
            "name_shrimp",
            {"tank": {"x": None, "shrimp": [{"name": "rex"}, {"name": "gertrude"}]}},
            Context(),
        )
        assert result == ["rex", "gertrude"]
        result = await manager.call_tool(
            "name_shrimp",
            {"tank": '{"x": null, "shrimp": [{"name": "rex"}, {"name": "gertrude"}]}'},
            Context(),
        )
        assert result == ["rex", "gertrude"]


class TestToolSchema:
    @pytest.mark.anyio
    async def test_context_arg_excluded_from_schema(self):
        def something(a: int, ctx: Context) -> int:  # pragma: no cover
            return a

        manager = ToolManager()
        tool = manager.add_tool(something)
        assert "ctx" not in json.dumps(tool.parameters)
        assert "Context" not in json.dumps(tool.parameters)
        assert "ctx" not in tool.fn_metadata.arg_model.model_fields


class TestContextHandling:
    """Test context handling in the tool manager."""

    def test_context_parameter_detection(self):
        """Test that context parameters are properly detected in
        Tool.from_function()."""

        def tool_with_context(x: int, ctx: Context) -> str:  # pragma: no cover
            return str(x)

        manager = ToolManager()
        tool = manager.add_tool(tool_with_context)
        assert tool.context_kwarg == "ctx"

        def tool_without_context(x: int) -> str:  # pragma: no cover
            return str(x)

        tool = manager.add_tool(tool_without_context)
        assert tool.context_kwarg is None

        def tool_with_parametrized_context(x: int, ctx: Context[LifespanContextT, RequestT]) -> str:  # pragma: no cover
            return str(x)

        tool = manager.add_tool(tool_with_parametrized_context)
        assert tool.context_kwarg == "ctx"

    @pytest.mark.anyio
    async def test_context_injection(self):
        """Test that context is properly injected during tool execution."""

        def tool_with_context(x: int, ctx: Context) -> str:
            assert isinstance(ctx, Context)
            return str(x)

        manager = ToolManager()
        manager.add_tool(tool_with_context)

        result = await manager.call_tool("tool_with_context", {"x": 42}, context=Context())
        assert result == "42"

    @pytest.mark.anyio
    async def test_context_injection_async(self):
        """Test that context is properly injected in async tools."""

        async def async_tool(x: int, ctx: Context) -> str:
            assert isinstance(ctx, Context)
            return str(x)

        manager = ToolManager()
        manager.add_tool(async_tool)

        result = await manager.call_tool("async_tool", {"x": 42}, context=Context())
        assert result == "42"

    @pytest.mark.anyio
    async def test_context_error_handling(self):
        """Test error handling when context injection fails."""

        def tool_with_context(x: int, ctx: Context) -> str:
            raise ValueError("Test error")

        manager = ToolManager()
        manager.add_tool(tool_with_context)

        with pytest.raises(ToolError, match="Error executing tool tool_with_context"):
            await manager.call_tool("tool_with_context", {"x": 42}, context=Context())


class TestToolAnnotations:
    def test_tool_annotations(self):
        """Test that tool annotations are correctly added to tools."""

        def read_data(path: str) -> str:  # pragma: no cover
            """Read data from a file."""
            return f"Data from {path}"

        annotations = ToolAnnotations(
            title="File Reader",
            read_only_hint=True,
            open_world_hint=False,
        )

        manager = ToolManager()
        tool = manager.add_tool(read_data, annotations=annotations)

        assert tool.annotations is not None
        assert tool.annotations.title == "File Reader"
        assert tool.annotations.read_only_hint is True
        assert tool.annotations.open_world_hint is False

    @pytest.mark.anyio
    async def test_tool_annotations_in_mcpserver(self):
        """Test that tool annotations are included in MCPTool conversion."""

        app = MCPServer()

        @app.tool(annotations=ToolAnnotations(title="Echo Tool", read_only_hint=True))
        def echo(message: str) -> str:  # pragma: no cover
            """Echo a message back."""
            return message

        tools = await app.list_tools()
        assert len(tools) == 1
        assert tools[0].annotations is not None
        assert tools[0].annotations.title == "Echo Tool"
        assert tools[0].annotations.read_only_hint is True


class TestStructuredOutput:
    """Test structured output functionality in tools."""

    @pytest.mark.anyio
    async def test_tool_with_basemodel_output(self):
        """Test tool with BaseModel return type."""

        class UserOutput(BaseModel):
            name: str
            age: int

        def get_user(user_id: int) -> UserOutput:
            """Get user by ID."""
            return UserOutput(name="John", age=30)

        manager = ToolManager()
        manager.add_tool(get_user)
        result = await manager.call_tool("get_user", {"user_id": 1}, Context(), convert_result=True)
        # don't test unstructured output here, just the structured conversion
        assert isinstance(result, CallToolResult)
        assert result.structured_content == {"name": "John", "age": 30}

    @pytest.mark.anyio
    async def test_tool_with_primitive_output(self):
        """Test tool with primitive return type."""

        def double_number(n: int) -> int:
            """Double a number."""
            return 10

        manager = ToolManager()
        manager.add_tool(double_number)
        result = await manager.call_tool("double_number", {"n": 5}, Context())
        assert result == 10
        result = await manager.call_tool("double_number", {"n": 5}, Context(), convert_result=True)
        assert isinstance(result, CallToolResult)
        assert isinstance(result.content[0], TextContent) and result.structured_content == {"result": 10}

    @pytest.mark.anyio
    async def test_tool_with_typeddict_output(self):
        """Test tool with TypedDict return type."""

        class UserDict(TypedDict):
            name: str
            age: int

        expected_output = {"name": "Alice", "age": 25}

        def get_user_dict(user_id: int) -> UserDict:
            """Get user as dict."""
            return UserDict(name="Alice", age=25)

        manager = ToolManager()
        manager.add_tool(get_user_dict)
        result = await manager.call_tool("get_user_dict", {"user_id": 1}, Context())
        assert result == expected_output

    @pytest.mark.anyio
    async def test_tool_with_dataclass_output(self):
        """Test tool with dataclass return type."""

        @dataclass
        class Person:
            name: str
            age: int

        expected_output = {"name": "Bob", "age": 40}

        def get_person() -> Person:
            """Get a person."""
            return Person("Bob", 40)

        manager = ToolManager()
        manager.add_tool(get_person)
        result = await manager.call_tool("get_person", {}, Context(), convert_result=True)
        # don't test unstructured output here, just the structured conversion
        assert isinstance(result, CallToolResult)
        assert result.structured_content == expected_output

    @pytest.mark.anyio
    async def test_tool_with_list_output(self):
        """Test tool with list return type."""

        expected_list = [1, 2, 3, 4, 5]
        expected_output = {"result": expected_list}

        def get_numbers() -> list[int]:
            """Get a list of numbers."""
            return expected_list

        manager = ToolManager()
        manager.add_tool(get_numbers)
        result = await manager.call_tool("get_numbers", {}, Context())
        assert result == expected_list
        result = await manager.call_tool("get_numbers", {}, Context(), convert_result=True)
        assert isinstance(result, CallToolResult)
        assert isinstance(result.content[0], TextContent) and result.structured_content == expected_output

    @pytest.mark.anyio
    async def test_tool_without_structured_output(self):
        """Test that tools work normally when structured_output=False."""

        def get_dict() -> dict[str, Any]:
            """Get a dict."""
            return {"key": "value"}

        manager = ToolManager()
        manager.add_tool(get_dict, structured_output=False)
        result = await manager.call_tool("get_dict", {}, Context())
        assert isinstance(result, dict)
        assert result == {"key": "value"}

    def test_tool_output_schema_property(self):
        """Test that Tool.output_schema property works correctly."""

        class UserOutput(BaseModel):
            name: str
            age: int

        def get_user() -> UserOutput:  # pragma: no cover
            return UserOutput(name="Test", age=25)

        manager = ToolManager()
        tool = manager.add_tool(get_user)

        # Test that output_schema is populated
        expected_schema = {
            "properties": {"name": {"type": "string", "title": "Name"}, "age": {"type": "integer", "title": "Age"}},
            "required": ["name", "age"],
            "title": "UserOutput",
            "type": "object",
        }
        assert tool.output_schema == expected_schema

    @pytest.mark.anyio
    async def test_tool_with_dict_str_any_output(self):
        """Test tool with dict[str, Any] return type."""

        def get_config() -> dict[str, Any]:
            """Get configuration"""
            return {"debug": True, "port": 8080, "features": ["auth", "logging"]}

        manager = ToolManager()
        tool = manager.add_tool(get_config)

        # Check output schema
        assert tool.output_schema is not None
        assert tool.output_schema["type"] == "object"
        assert "properties" not in tool.output_schema  # dict[str, Any] has no constraints

        # Test raw result
        result = await manager.call_tool("get_config", {}, Context())
        expected = {"debug": True, "port": 8080, "features": ["auth", "logging"]}
        assert result == expected

        # Test converted result
        result = await manager.call_tool("get_config", {}, Context())
        assert result == expected

    @pytest.mark.anyio
    async def test_tool_with_dict_str_typed_output(self):
        """Test tool with dict[str, T] return type for specific T."""

        def get_scores() -> dict[str, int]:
            """Get player scores"""
            return {"alice": 100, "bob": 85, "charlie": 92}

        manager = ToolManager()
        tool = manager.add_tool(get_scores)

        # Check output schema
        assert tool.output_schema is not None
        assert tool.output_schema["type"] == "object"
        assert tool.output_schema["additionalProperties"]["type"] == "integer"

        # Test raw result
        result = await manager.call_tool("get_scores", {}, Context())
        expected = {"alice": 100, "bob": 85, "charlie": 92}
        assert result == expected

        # Test converted result
        result = await manager.call_tool("get_scores", {}, Context())
        assert result == expected


class TestToolMetadata:
    """Test tool metadata functionality."""

    def test_add_tool_with_metadata(self):
        """Test adding a tool with metadata via ToolManager."""

        def process_data(input_data: str) -> str:  # pragma: no cover
            """Process some data."""
            return f"Processed: {input_data}"

        metadata = {"ui": {"type": "form", "fields": ["input"]}, "version": "1.0"}

        manager = ToolManager()
        tool = manager.add_tool(process_data, meta=metadata)

        assert tool.meta is not None
        assert tool.meta == metadata
        assert tool.meta["ui"]["type"] == "form"
        assert tool.meta["version"] == "1.0"

    def test_add_tool_without_metadata(self):
        """Test that tools without metadata have None as meta value."""

        def simple_tool(x: int) -> int:  # pragma: no cover
            """Simple tool."""
            return x * 2

        manager = ToolManager()
        tool = manager.add_tool(simple_tool)

        assert tool.meta is None

    @pytest.mark.anyio
    async def test_metadata_in_mcpserver_decorator(self):
        """Test that metadata is correctly added via MCPServer.tool decorator."""

        app = MCPServer()

        metadata = {"client": {"ui_component": "file_picker"}, "priority": "high"}

        @app.tool(meta=metadata)
        def upload_file(filename: str) -> str:  # pragma: no cover
            """Upload a file."""
            return f"Uploaded: {filename}"

        # Get the tool from the tool manager
        tool = app._tool_manager.get_tool("upload_file")
        assert tool is not None
        assert tool.meta is not None
        assert tool.meta == metadata
        assert tool.meta["client"]["ui_component"] == "file_picker"
        assert tool.meta["priority"] == "high"

    @pytest.mark.anyio
    async def test_metadata_in_list_tools(self):
        """Test that metadata is included in MCPTool when listing tools."""

        app = MCPServer()

        metadata = {
            "ui": {"input_type": "textarea", "rows": 5},
            "tags": ["text", "processing"],
        }

        @app.tool(meta=metadata)
        def analyze_text(text: str) -> dict[str, Any]:  # pragma: no cover
            """Analyze text content."""
            return {"length": len(text), "words": len(text.split())}

        tools = await app.list_tools()
        assert len(tools) == 1
        assert tools[0].meta is not None
        assert tools[0].meta == metadata

    @pytest.mark.anyio
    async def test_multiple_tools_with_different_metadata(self):
        """Test multiple tools with different metadata values."""

        app = MCPServer()

        metadata1 = {"ui": "form", "version": 1}
        metadata2 = {"ui": "picker", "experimental": True}

        @app.tool(meta=metadata1)
        def tool1(x: int) -> int:  # pragma: no cover
            """First tool."""
            return x

        @app.tool(meta=metadata2)
        def tool2(y: str) -> str:  # pragma: no cover
            """Second tool."""
            return y

        @app.tool()
        def tool3(z: bool) -> bool:  # pragma: no cover
            """Third tool without metadata."""
            return z

        tools = await app.list_tools()
        assert len(tools) == 3

        # Find tools by name and check metadata
        tools_by_name = {t.name: t for t in tools}

        assert tools_by_name["tool1"].meta == metadata1
        assert tools_by_name["tool2"].meta == metadata2
        assert tools_by_name["tool3"].meta is None

    def test_metadata_with_complex_structure(self):
        """Test metadata with complex nested structures."""

        def complex_tool(data: str) -> str:  # pragma: no cover
            """Tool with complex metadata."""
            return data

        metadata = {
            "ui": {
                "components": [
                    {"type": "input", "name": "field1", "validation": {"required": True, "minLength": 5}},
                    {"type": "select", "name": "field2", "options": ["a", "b", "c"]},
                ],
                "layout": {"columns": 2, "responsive": True},
            },
            "permissions": ["read", "write"],
            "tags": ["data-processing", "user-input"],
            "version": 2,
        }

        manager = ToolManager()
        tool = manager.add_tool(complex_tool, meta=metadata)

        assert tool.meta is not None
        assert tool.meta["ui"]["components"][0]["validation"]["minLength"] == 5
        assert tool.meta["ui"]["layout"]["columns"] == 2
        assert "read" in tool.meta["permissions"]
        assert "data-processing" in tool.meta["tags"]

    def test_metadata_empty_dict(self):
        """Test that empty dict metadata is preserved."""

        def tool_with_empty_meta(x: int) -> int:  # pragma: no cover
            """Tool with empty metadata."""
            return x

        manager = ToolManager()
        tool = manager.add_tool(tool_with_empty_meta, meta={})

        assert tool.meta is not None
        assert tool.meta == {}

    @pytest.mark.anyio
    async def test_metadata_with_annotations(self):
        """Test that metadata and annotations can coexist."""

        app = MCPServer()

        metadata = {"custom": "value"}
        annotations = ToolAnnotations(title="Combined Tool", read_only_hint=True)

        @app.tool(meta=metadata, annotations=annotations)
        def combined_tool(data: str) -> str:  # pragma: no cover
            """Tool with both metadata and annotations."""
            return data

        tools = await app.list_tools()
        assert len(tools) == 1
        assert tools[0].meta == metadata
        assert tools[0].annotations is not None
        assert tools[0].annotations.title == "Combined Tool"
        assert tools[0].annotations.read_only_hint is True


class TestRemoveTools:
    """Test tool removal functionality in the tool manager."""

    def test_remove_existing_tool(self):
        """Test removing an existing tool."""

        def add(a: int, b: int) -> int:  # pragma: no cover
            """Add two numbers."""
            return a + b

        manager = ToolManager()
        manager.add_tool(add)

        # Verify tool exists
        assert manager.get_tool("add") is not None
        assert len(manager.list_tools()) == 1

        # Remove the tool - should not raise any exception
        manager.remove_tool("add")

        # Verify tool is removed
        assert manager.get_tool("add") is None
        assert len(manager.list_tools()) == 0

    def test_remove_nonexistent_tool(self):
        """Test removing a non-existent tool raises ToolError."""
        manager = ToolManager()

        with pytest.raises(ToolError, match="Unknown tool: nonexistent"):
            manager.remove_tool("nonexistent")

    def test_remove_tool_from_multiple_tools(self):
        """Test removing one tool when multiple tools exist."""

        def add(a: int, b: int) -> int:  # pragma: no cover
            """Add two numbers."""
            return a + b

        def multiply(a: int, b: int) -> int:  # pragma: no cover
            """Multiply two numbers."""
            return a * b

        def divide(a: int, b: int) -> float:  # pragma: no cover
            """Divide two numbers."""
            return a / b

        manager = ToolManager()
        manager.add_tool(add)
        manager.add_tool(multiply)
        manager.add_tool(divide)

        # Verify all tools exist
        assert len(manager.list_tools()) == 3
        assert manager.get_tool("add") is not None
        assert manager.get_tool("multiply") is not None
        assert manager.get_tool("divide") is not None

        # Remove middle tool
        manager.remove_tool("multiply")

        # Verify only multiply is removed
        assert len(manager.list_tools()) == 2
        assert manager.get_tool("add") is not None
        assert manager.get_tool("multiply") is None
        assert manager.get_tool("divide") is not None

    @pytest.mark.anyio
    async def test_call_removed_tool_raises_error(self):
        """Test that calling a removed tool raises ToolError."""

        def greet(name: str) -> str:
            """Greet someone."""
            return f"Hello, {name}!"

        manager = ToolManager()
        manager.add_tool(greet)

        # Verify tool works before removal
        result = await manager.call_tool("greet", {"name": "World"}, Context())
        assert result == "Hello, World!"

        # Remove the tool
        manager.remove_tool("greet")

        # Verify calling removed tool raises error
        with pytest.raises(ToolError, match="Unknown tool: greet"):
            await manager.call_tool("greet", {"name": "World"}, Context())

    def test_remove_tool_case_sensitive(self):
        """Test that tool removal is case-sensitive."""

        def test_func() -> str:  # pragma: no cover
            """Test function."""
            return "test"

        manager = ToolManager()
        manager.add_tool(test_func)

        # Verify tool exists
        assert manager.get_tool("test_func") is not None

        # Try to remove with different case - should raise ToolError
        with pytest.raises(ToolError, match="Unknown tool: Test_Func"):
            manager.remove_tool("Test_Func")

        # Verify original tool still exists
        assert manager.get_tool("test_func") is not None

        # Remove with correct case
        manager.remove_tool("test_func")
        assert manager.get_tool("test_func") is None
