"""Tests for the inspect.py module."""

import importlib.metadata

from mcp.server.fastmcp import FastMCP as FastMCP1x

import fastmcp
from fastmcp import Client, FastMCP
from fastmcp.utilities.inspect import (
    FastMCPInfo,
    InspectFormat,
    ToolInfo,
    format_fastmcp_info,
    format_info,
    format_mcp_info,
    inspect_fastmcp,
    inspect_fastmcp_v1,
)


class TestFastMCPInfo:
    """Tests for the FastMCPInfo dataclass."""

    def test_fastmcp_info_creation(self):
        """Test that FastMCPInfo can be created with all required fields."""
        tool = ToolInfo(
            key="tool1",
            name="tool1",
            description="Test tool",
            input_schema={},
            output_schema={
                "type": "object",
                "properties": {"result": {"type": "string"}},
            },
        )
        info = FastMCPInfo(
            name="TestServer",
            instructions="Test instructions",
            fastmcp_version="1.0.0",
            mcp_version="1.0.0",
            server_generation=2,
            version="1.0.0",
            website_url=None,
            icons=None,
            tools=[tool],
            prompts=[],
            resources=[],
            templates=[],
            capabilities={"tools": {"listChanged": True}},
        )

        assert info.name == "TestServer"
        assert info.instructions == "Test instructions"
        assert info.fastmcp_version == "1.0.0"
        assert info.mcp_version == "1.0.0"
        assert info.server_generation == 2
        assert info.version == "1.0.0"
        assert len(info.tools) == 1
        assert info.tools[0].name == "tool1"
        assert info.capabilities == {"tools": {"listChanged": True}}

    def test_fastmcp_info_with_none_instructions(self):
        """Test that FastMCPInfo works with None instructions."""
        info = FastMCPInfo(
            name="TestServer",
            instructions=None,
            fastmcp_version="1.0.0",
            mcp_version="1.0.0",
            server_generation=2,
            version="1.0.0",
            website_url=None,
            icons=None,
            tools=[],
            prompts=[],
            resources=[],
            templates=[],
            capabilities={},
        )

        assert info.instructions is None


class TestGetFastMCPInfo:
    """Tests for the get_fastmcp_info function."""

    async def test_empty_server(self):
        """Test get_fastmcp_info with an empty server."""
        mcp = FastMCP("EmptyServer")

        info = await inspect_fastmcp(mcp)

        assert info.name == "EmptyServer"
        assert info.instructions is None
        assert info.fastmcp_version == fastmcp.__version__
        assert info.mcp_version == importlib.metadata.version("mcp")
        assert info.server_generation == 2  # v2 server
        assert info.version == fastmcp.__version__
        assert info.tools == []
        assert info.prompts == []
        assert info.resources == []
        assert info.templates == []
        assert "tools" in info.capabilities
        assert "resources" in info.capabilities
        assert "prompts" in info.capabilities
        assert "logging" in info.capabilities

    async def test_server_with_instructions(self):
        """Test get_fastmcp_info with a server that has instructions."""
        mcp = FastMCP("InstructionsServer", instructions="Test instructions")
        info = await inspect_fastmcp(mcp)
        assert info.instructions == "Test instructions"

    async def test_server_with_version(self):
        """Test get_fastmcp_info with a server that has a version."""
        mcp = FastMCP("VersionServer", version="1.2.3")
        info = await inspect_fastmcp(mcp)
        assert info.version == "1.2.3"

    async def test_server_with_tools(self):
        """Test get_fastmcp_info with a server that has tools."""
        mcp = FastMCP("ToolServer")

        @mcp.tool
        def add_numbers(a: int, b: int) -> int:
            return a + b

        @mcp.tool
        def greet(name: str) -> str:
            return f"Hello, {name}!"

        info = await inspect_fastmcp(mcp)

        assert info.name == "ToolServer"
        assert len(info.tools) == 2
        tool_names = [tool.name for tool in info.tools]
        assert "add_numbers" in tool_names
        assert "greet" in tool_names

    async def test_server_with_resources(self):
        """Test get_fastmcp_info with a server that has resources."""
        mcp = FastMCP("ResourceServer")

        @mcp.resource("resource://static")
        def get_static_data() -> str:
            return "Static data"

        @mcp.resource("resource://dynamic/{param}")
        def get_dynamic_data(param: str) -> str:
            return f"Dynamic data: {param}"

        info = await inspect_fastmcp(mcp)

        assert info.name == "ResourceServer"
        assert len(info.resources) == 1  # Static resource
        assert len(info.templates) == 1  # Dynamic resource becomes template
        resource_uris = [res.uri for res in info.resources]
        template_uris = [tmpl.uri_template for tmpl in info.templates]
        assert "resource://static" in resource_uris
        assert "resource://dynamic/{param}" in template_uris

    async def test_server_with_prompts(self):
        """Test get_fastmcp_info with a server that has prompts."""
        mcp = FastMCP("PromptServer")

        @mcp.prompt
        def analyze_data(data: str) -> list:
            return [{"role": "user", "content": f"Analyze: {data}"}]

        @mcp.prompt("custom_prompt")
        def custom_analysis(text: str) -> list:
            return [{"role": "user", "content": f"Custom: {text}"}]

        info = await inspect_fastmcp(mcp)

        assert info.name == "PromptServer"
        assert len(info.prompts) == 2
        prompt_names = [prompt.name for prompt in info.prompts]
        assert "analyze_data" in prompt_names
        assert "custom_prompt" in prompt_names

    async def test_comprehensive_server(self):
        """Test get_fastmcp_info with a server that has all component types."""
        mcp = FastMCP("ComprehensiveServer", instructions="A server with everything")

        # Add a tool
        @mcp.tool
        def calculate(x: int, y: int) -> int:
            return x * y

        # Add a resource
        @mcp.resource("resource://data")
        def get_data() -> str:
            return "Some data"

        # Add a template
        @mcp.resource("resource://item/{id}")
        def get_item(id: str) -> str:
            return f"Item {id}"

        # Add a prompt
        @mcp.prompt
        def analyze(content: str) -> list:
            return [{"role": "user", "content": content}]

        info = await inspect_fastmcp(mcp)

        assert info.name == "ComprehensiveServer"
        assert info.instructions == "A server with everything"
        assert info.fastmcp_version == fastmcp.__version__

        # Check all components are present
        assert len(info.tools) == 1
        tool_names = [tool.name for tool in info.tools]
        assert "calculate" in tool_names

        assert len(info.resources) == 1
        resource_uris = [res.uri for res in info.resources]
        assert "resource://data" in resource_uris

        assert len(info.templates) == 1
        template_uris = [tmpl.uri_template for tmpl in info.templates]
        assert "resource://item/{id}" in template_uris

        assert len(info.prompts) == 1
        prompt_names = [prompt.name for prompt in info.prompts]
        assert "analyze" in prompt_names

        # Check capabilities
        assert "tools" in info.capabilities
        assert "resources" in info.capabilities
        assert "prompts" in info.capabilities
        assert "logging" in info.capabilities

    async def test_server_no_instructions(self):
        """Test get_fastmcp_info with a server that has no instructions."""
        mcp = FastMCP("NoInstructionsServer")

        info = await inspect_fastmcp(mcp)

        assert info.name == "NoInstructionsServer"
        assert info.instructions is None

    async def test_server_with_client_integration(self):
        """Test that the extracted info matches what a client would see."""
        mcp = FastMCP("IntegrationServer")

        @mcp.tool
        def test_tool() -> str:
            return "test"

        @mcp.resource("resource://test")
        def test_resource() -> str:
            return "test resource"

        @mcp.prompt
        def test_prompt() -> list:
            return [{"role": "user", "content": "test"}]

        # Get info using our function
        info = await inspect_fastmcp(mcp)

        # Verify using client
        async with Client(mcp) as client:
            tools = await client.list_tools()
            resources = await client.list_resources()
            prompts = await client.list_prompts()

            assert len(info.tools) == len(tools)
            assert len(info.resources) == len(resources)
            assert len(info.prompts) == len(prompts)

            assert info.tools[0].name == tools[0].name
            assert info.resources[0].uri == str(resources[0].uri)
            assert info.prompts[0].name == prompts[0].name

    async def test_inspect_respects_tag_filtering(self):
        """Test that inspect omits components filtered out by include_tags/exclude_tags.

        Regression test for Issue #2032: inspect command was showing components
        that were filtered out by tag rules, causing confusion when those
        components weren't actually available to clients.
        """
        # Create server with include_tags that will filter out untagged components
        mcp = FastMCP(
            "FilteredServer",
            include_tags={"fetch", "analyze", "create"},
        )

        # Add tools with and without matching tags
        @mcp.tool(tags={"fetch"})
        def tagged_tool() -> str:
            """Tool with matching tag - should be visible."""
            return "visible"

        @mcp.tool
        def untagged_tool() -> str:
            """Tool without tags - should be filtered out."""
            return "hidden"

        # Add resources with and without matching tags
        @mcp.resource("resource://tagged", tags={"analyze"})
        def tagged_resource() -> str:
            """Resource with matching tag - should be visible."""
            return "visible resource"

        @mcp.resource("resource://untagged")
        def untagged_resource() -> str:
            """Resource without tags - should be filtered out."""
            return "hidden resource"

        # Add templates with and without matching tags
        @mcp.resource("resource://tagged/{id}", tags={"create"})
        def tagged_template(id: str) -> str:
            """Template with matching tag - should be visible."""
            return f"visible template {id}"

        @mcp.resource("resource://untagged/{id}")
        def untagged_template(id: str) -> str:
            """Template without tags - should be filtered out."""
            return f"hidden template {id}"

        # Add prompts with and without matching tags
        @mcp.prompt(tags={"fetch"})
        def tagged_prompt() -> list:
            """Prompt with matching tag - should be visible."""
            return [{"role": "user", "content": "visible prompt"}]

        @mcp.prompt
        def untagged_prompt() -> list:
            """Prompt without tags - should be filtered out."""
            return [{"role": "user", "content": "hidden prompt"}]

        # Get inspect info
        info = await inspect_fastmcp(mcp)

        # Verify only tagged components are visible
        assert len(info.tools) == 1
        assert info.tools[0].name == "tagged_tool"

        assert len(info.resources) == 1
        assert info.resources[0].uri == "resource://tagged"

        assert len(info.templates) == 1
        assert info.templates[0].uri_template == "resource://tagged/{id}"

        assert len(info.prompts) == 1
        assert info.prompts[0].name == "tagged_prompt"

        # Verify this matches what a client would see
        async with Client(mcp) as client:
            tools = await client.list_tools()
            resources = await client.list_resources()
            templates = await client.list_resource_templates()
            prompts = await client.list_prompts()

            assert len(info.tools) == len(tools)
            assert len(info.resources) == len(resources)
            assert len(info.templates) == len(templates)
            assert len(info.prompts) == len(prompts)

    async def test_inspect_respects_tag_filtering_with_mounted_servers(self):
        """Test that inspect applies tag filtering to mounted servers.

        Verifies that when a parent server has tag filters, those filters
        are respected when inspecting components from mounted servers.
        """
        # Create a mounted server with various tagged and untagged components
        mounted = FastMCP("MountedServer")

        @mounted.tool(tags={"allowed"})
        def allowed_tool() -> str:
            return "allowed"

        @mounted.tool(tags={"blocked"})
        def blocked_tool() -> str:
            return "blocked"

        @mounted.tool
        def untagged_tool() -> str:
            return "untagged"

        @mounted.resource("resource://allowed", tags={"allowed"})
        def allowed_resource() -> str:
            return "allowed resource"

        @mounted.resource("resource://blocked", tags={"blocked"})
        def blocked_resource() -> str:
            return "blocked resource"

        @mounted.prompt(tags={"allowed"})
        def allowed_prompt() -> list:
            return [{"role": "user", "content": "allowed"}]

        @mounted.prompt(tags={"blocked"})
        def blocked_prompt() -> list:
            return [{"role": "user", "content": "blocked"}]

        # Create parent server with tag filtering
        parent = FastMCP("ParentServer", include_tags={"allowed"})
        parent.mount(mounted)

        # Get inspect info
        info = await inspect_fastmcp(parent)

        # Only components with "allowed" tag should be visible
        tool_names = [t.name for t in info.tools]
        assert "allowed_tool" in tool_names
        assert "blocked_tool" not in tool_names
        assert "untagged_tool" not in tool_names

        resource_uris = [r.uri for r in info.resources]
        assert "resource://allowed" in resource_uris
        assert "resource://blocked" not in resource_uris

        prompt_names = [p.name for p in info.prompts]
        assert "allowed_prompt" in prompt_names
        assert "blocked_prompt" not in prompt_names

        # Verify this matches what a client would see
        async with Client(parent) as client:
            tools = await client.list_tools()
            resources = await client.list_resources()
            prompts = await client.list_prompts()

            assert len(info.tools) == len(tools)
            assert len(info.resources) == len(resources)
            assert len(info.prompts) == len(prompts)

    async def test_inspect_parent_filters_override_mounted_server_filters(self):
        """Test that parent server tag filters apply to mounted servers.

        Even if a mounted server has no tag filters of its own,
        the parent server's filters should still apply.
        """
        # Create mounted server with NO tag filters (allows everything)
        mounted = FastMCP("MountedServer")

        @mounted.tool(tags={"production"})
        def production_tool() -> str:
            return "production"

        @mounted.tool(tags={"development"})
        def development_tool() -> str:
            return "development"

        @mounted.tool
        def untagged_tool() -> str:
            return "untagged"

        # Create parent with exclude_tags - should filter mounted components
        parent = FastMCP("ParentServer", exclude_tags={"development"})
        parent.mount(mounted)

        # Get inspect info
        info = await inspect_fastmcp(parent)

        # Only production and untagged should be visible
        tool_names = [t.name for t in info.tools]
        assert "production_tool" in tool_names
        assert "untagged_tool" in tool_names
        assert "development_tool" not in tool_names

        # Verify this matches what a client would see
        async with Client(parent) as client:
            tools = await client.list_tools()
            assert len(info.tools) == len(tools)


class TestFastMCP1xCompatibility:
    """Tests for FastMCP 1.x compatibility."""

    async def test_fastmcp1x_empty_server(self):
        """Test get_fastmcp_info_v1 with an empty FastMCP1x server."""
        mcp = FastMCP1x("Test1x")

        info = await inspect_fastmcp_v1(mcp)

        assert info.name == "Test1x"
        assert info.instructions is None
        assert info.fastmcp_version == fastmcp.__version__  # CLI version
        assert info.mcp_version == importlib.metadata.version("mcp")
        assert info.server_generation == 1  # v1 server
        assert info.version is None
        assert info.tools == []
        assert info.prompts == []
        assert info.resources == []
        assert info.templates == []  # No templates added in this test
        assert "tools" in info.capabilities

    async def test_fastmcp1x_with_tools(self):
        """Test get_fastmcp_info_v1 with a FastMCP1x server that has tools."""
        mcp = FastMCP1x("Test1x")

        @mcp.tool()
        def add_numbers(a: int, b: int) -> int:
            return a + b

        @mcp.tool()
        def greet(name: str) -> str:
            return f"Hello, {name}!"

        info = await inspect_fastmcp_v1(mcp)

        assert info.name == "Test1x"
        assert len(info.tools) == 2
        tool_names = [tool.name for tool in info.tools]
        assert "add_numbers" in tool_names
        assert "greet" in tool_names

    async def test_fastmcp1x_with_resources(self):
        """Test get_fastmcp_info_v1 with a FastMCP1x server that has resources."""
        mcp = FastMCP1x("Test1x")

        @mcp.resource("resource://data")
        def get_data() -> str:
            return "Some data"

        info = await inspect_fastmcp_v1(mcp)

        assert info.name == "Test1x"
        assert len(info.resources) == 1
        resource_uris = [res.uri for res in info.resources]
        assert "resource://data" in resource_uris
        assert len(info.templates) == 0  # No templates added in this test
        assert info.server_generation == 1  # v1 server

    async def test_fastmcp1x_with_prompts(self):
        """Test get_fastmcp_info_v1 with a FastMCP1x server that has prompts."""
        mcp = FastMCP1x("Test1x")

        @mcp.prompt("analyze")
        def analyze_data(data: str) -> list:
            return [{"role": "user", "content": f"Analyze: {data}"}]

        info = await inspect_fastmcp_v1(mcp)

        assert info.name == "Test1x"
        assert len(info.prompts) == 1
        prompt_names = [prompt.name for prompt in info.prompts]
        assert "analyze" in prompt_names

    async def test_dispatcher_with_fastmcp1x(self):
        """Test that the main get_fastmcp_info function correctly dispatches to v1."""
        mcp = FastMCP1x("Test1x")

        @mcp.tool()
        def test_tool() -> str:
            return "test"

        info = await inspect_fastmcp(mcp)

        assert info.name == "Test1x"
        assert len(info.tools) == 1
        tool_names = [tool.name for tool in info.tools]
        assert "test_tool" in tool_names
        assert len(info.templates) == 0  # No templates added in this test
        assert info.server_generation == 1  # v1 server

    async def test_dispatcher_with_fastmcp2x(self):
        """Test that the main get_fastmcp_info function correctly dispatches to v2."""
        mcp = FastMCP("Test2x")

        @mcp.tool
        def test_tool() -> str:
            return "test"

        info = await inspect_fastmcp(mcp)

        assert info.name == "Test2x"
        assert len(info.tools) == 1
        tool_names = [tool.name for tool in info.tools]
        assert "test_tool" in tool_names

    async def test_fastmcp1x_vs_fastmcp2x_comparison(self):
        """Test that both versions can be inspected and compared."""
        mcp1x = FastMCP1x("Test1x")
        mcp2x = FastMCP("Test2x")

        @mcp1x.tool()
        def tool1x() -> str:
            return "1x"

        @mcp2x.tool
        def tool2x() -> str:
            return "2x"

        info1x = await inspect_fastmcp(mcp1x)
        info2x = await inspect_fastmcp(mcp2x)

        assert info1x.name == "Test1x"
        assert info2x.name == "Test2x"
        assert len(info1x.tools) == 1
        assert len(info2x.tools) == 1

        tool1x_names = [tool.name for tool in info1x.tools]
        tool2x_names = [tool.name for tool in info2x.tools]
        assert "tool1x" in tool1x_names
        assert "tool2x" in tool2x_names

        # Check server versions
        assert info1x.server_generation == 1  # v1
        assert info2x.server_generation == 2  # v2
        assert info1x.version is None
        assert info2x.version == fastmcp.__version__

        # No templates added in these tests
        assert len(info1x.templates) == 0
        assert len(info2x.templates) == 0


class TestIconExtraction:
    """Tests for icon extraction in inspect."""

    async def test_server_icons_and_website(self):
        """Test that server-level icons and website_url are extracted."""
        from mcp.types import Icon

        mcp = FastMCP(
            "IconServer",
            website_url="https://example.com",
            icons=[
                Icon(
                    src="https://example.com/icon.png",
                    mimeType="image/png",
                    sizes=["48x48"],
                )
            ],
        )

        info = await inspect_fastmcp(mcp)

        assert info.website_url == "https://example.com"
        assert info.icons is not None
        assert len(info.icons) == 1
        assert info.icons[0]["src"] == "https://example.com/icon.png"
        assert info.icons[0]["mimeType"] == "image/png"
        assert info.icons[0]["sizes"] == ["48x48"]

    async def test_server_without_icons(self):
        """Test that servers without icons have None for icons and website_url."""
        mcp = FastMCP("NoIconServer")

        info = await inspect_fastmcp(mcp)

        assert info.website_url is None
        assert info.icons is None

    async def test_tool_icons(self):
        """Test that tool icons are extracted."""
        from mcp.types import Icon

        mcp = FastMCP("ToolIconServer")

        @mcp.tool(
            icons=[
                Icon(
                    src="https://example.com/calculator.png",
                    mimeType="image/png",
                )
            ]
        )
        def calculate(x: int) -> int:
            """Calculate something."""
            return x * 2

        @mcp.tool
        def no_icon_tool() -> str:
            """Tool without icon."""
            return "no icon"

        info = await inspect_fastmcp(mcp)

        assert len(info.tools) == 2

        # Find the calculate tool
        calculate_tool = next(t for t in info.tools if t.name == "calculate")
        assert calculate_tool.icons is not None
        assert len(calculate_tool.icons) == 1
        assert calculate_tool.icons[0]["src"] == "https://example.com/calculator.png"

        # Find the no_icon tool
        no_icon = next(t for t in info.tools if t.name == "no_icon_tool")
        assert no_icon.icons is None

    async def test_resource_icons(self):
        """Test that resource icons are extracted."""
        from mcp.types import Icon

        mcp = FastMCP("ResourceIconServer")

        @mcp.resource(
            "resource://data",
            icons=[Icon(src="https://example.com/data.png", mimeType="image/png")],
        )
        def get_data() -> str:
            """Get data."""
            return "data"

        @mcp.resource("resource://no-icon")
        def get_no_icon() -> str:
            """Get data without icon."""
            return "no icon"

        info = await inspect_fastmcp(mcp)

        assert len(info.resources) == 2

        # Find the data resource
        data_resource = next(r for r in info.resources if r.uri == "resource://data")
        assert data_resource.icons is not None
        assert len(data_resource.icons) == 1
        assert data_resource.icons[0]["src"] == "https://example.com/data.png"

        # Find the no-icon resource
        no_icon = next(r for r in info.resources if r.uri == "resource://no-icon")
        assert no_icon.icons is None

    async def test_template_icons(self):
        """Test that resource template icons are extracted."""
        from mcp.types import Icon

        mcp = FastMCP("TemplateIconServer")

        @mcp.resource(
            "resource://user/{id}",
            icons=[Icon(src="https://example.com/user.png", mimeType="image/png")],
        )
        def get_user(id: str) -> str:
            """Get user by ID."""
            return f"user {id}"

        @mcp.resource("resource://item/{id}")
        def get_item(id: str) -> str:
            """Get item without icon."""
            return f"item {id}"

        info = await inspect_fastmcp(mcp)

        assert len(info.templates) == 2

        # Find the user template
        user_template = next(
            t for t in info.templates if t.uri_template == "resource://user/{id}"
        )
        assert user_template.icons is not None
        assert len(user_template.icons) == 1
        assert user_template.icons[0]["src"] == "https://example.com/user.png"

        # Find the no-icon template
        no_icon = next(
            t for t in info.templates if t.uri_template == "resource://item/{id}"
        )
        assert no_icon.icons is None

    async def test_prompt_icons(self):
        """Test that prompt icons are extracted."""
        from mcp.types import Icon

        mcp = FastMCP("PromptIconServer")

        @mcp.prompt(
            icons=[Icon(src="https://example.com/analyze.png", mimeType="image/png")]
        )
        def analyze(data: str) -> list:
            """Analyze data."""
            return [{"role": "user", "content": f"Analyze: {data}"}]

        @mcp.prompt
        def no_icon_prompt(text: str) -> list:
            """Prompt without icon."""
            return [{"role": "user", "content": text}]

        info = await inspect_fastmcp(mcp)

        assert len(info.prompts) == 2

        # Find the analyze prompt
        analyze_prompt = next(p for p in info.prompts if p.name == "analyze")
        assert analyze_prompt.icons is not None
        assert len(analyze_prompt.icons) == 1
        assert analyze_prompt.icons[0]["src"] == "https://example.com/analyze.png"

        # Find the no-icon prompt
        no_icon = next(p for p in info.prompts if p.name == "no_icon_prompt")
        assert no_icon.icons is None

    async def test_multiple_icons(self):
        """Test that components with multiple icons extract all of them."""
        from mcp.types import Icon

        mcp = FastMCP(
            "MultiIconServer",
            icons=[
                Icon(
                    src="https://example.com/icon-48.png",
                    mimeType="image/png",
                    sizes=["48x48"],
                ),
                Icon(
                    src="https://example.com/icon-96.png",
                    mimeType="image/png",
                    sizes=["96x96"],
                ),
            ],
        )

        @mcp.tool(
            icons=[
                Icon(src="https://example.com/tool-small.png", sizes=["24x24"]),
                Icon(src="https://example.com/tool-large.png", sizes=["48x48"]),
            ]
        )
        def multi_icon_tool() -> str:
            """Tool with multiple icons."""
            return "multi"

        info = await inspect_fastmcp(mcp)

        # Check server icons
        assert info.icons is not None
        assert len(info.icons) == 2
        assert info.icons[0]["sizes"] == ["48x48"]
        assert info.icons[1]["sizes"] == ["96x96"]

        # Check tool icons
        assert len(info.tools) == 1
        assert info.tools[0].icons is not None
        assert len(info.tools[0].icons) == 2
        assert info.tools[0].icons[0]["sizes"] == ["24x24"]
        assert info.tools[0].icons[1]["sizes"] == ["48x48"]

    async def test_data_uri_icons(self):
        """Test that data URI icons are extracted correctly."""
        from mcp.types import Icon

        data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="

        mcp = FastMCP("DataURIServer")

        @mcp.tool(icons=[Icon(src=data_uri, mimeType="image/png")])
        def data_uri_tool() -> str:
            """Tool with data URI icon."""
            return "data"

        info = await inspect_fastmcp(mcp)

        assert len(info.tools) == 1
        assert info.tools[0].icons is not None
        assert info.tools[0].icons[0]["src"] == data_uri
        assert info.tools[0].icons[0]["mimeType"] == "image/png"

    async def test_icons_in_fastmcp_v1(self):
        """Test that icons are extracted from FastMCP 1.x servers."""
        from mcp.types import Icon

        mcp = FastMCP1x("Icon1xServer")

        @mcp.tool(
            icons=[Icon(src="https://example.com/v1-tool.png", mimeType="image/png")]
        )
        def v1_tool() -> str:
            """Tool in v1 server."""
            return "v1"

        info = await inspect_fastmcp_v1(mcp)

        assert len(info.tools) == 1
        # v1 servers should also extract icons if present
        if info.tools[0].icons is not None:
            assert info.tools[0].icons[0]["src"] == "https://example.com/v1-tool.png"

    async def test_icons_in_formatted_output(self):
        """Test that icons appear in formatted JSON output."""
        from mcp.types import Icon

        mcp = FastMCP(
            "FormattedIconServer",
            website_url="https://example.com",
            icons=[Icon(src="https://example.com/server.png", mimeType="image/png")],
        )

        @mcp.tool(
            icons=[Icon(src="https://example.com/tool.png", mimeType="image/png")]
        )
        def icon_tool() -> str:
            """Tool with icon."""
            return "icon"

        info = await inspect_fastmcp(mcp)
        json_bytes = format_fastmcp_info(info)

        import json

        data = json.loads(json_bytes)

        # Check server icons in formatted output
        assert data["server"]["website_url"] == "https://example.com"
        assert data["server"]["icons"] is not None
        assert len(data["server"]["icons"]) == 1
        assert data["server"]["icons"][0]["src"] == "https://example.com/server.png"

        # Check tool icons in formatted output
        assert len(data["tools"]) == 1
        assert data["tools"][0]["icons"] is not None
        assert len(data["tools"][0]["icons"]) == 1
        assert data["tools"][0]["icons"][0]["src"] == "https://example.com/tool.png"

    async def test_icons_always_present_in_json(self):
        """Test that icons and website_url fields are always present in JSON, even when None."""
        mcp = FastMCP("AlwaysPresentServer")

        @mcp.tool
        def no_icon() -> str:
            """Tool without icon."""
            return "none"

        info = await inspect_fastmcp(mcp)
        json_bytes = format_fastmcp_info(info)

        import json

        data = json.loads(json_bytes)

        # Fields should always be present, even when None
        assert "website_url" in data["server"]
        assert "icons" in data["server"]
        assert data["server"]["website_url"] is None
        assert data["server"]["icons"] is None

        assert len(data["tools"]) == 1
        assert "icons" in data["tools"][0]
        assert data["tools"][0]["icons"] is None


class TestFormatFunctions:
    """Tests for the formatting functions."""

    async def test_format_fastmcp_info(self):
        """Test formatting as FastMCP-specific JSON."""
        mcp = FastMCP("TestServer", instructions="Test instructions", version="1.2.3")

        @mcp.tool
        def test_tool(x: int) -> dict:
            """A test tool."""
            return {"result": x * 2}

        info = await inspect_fastmcp(mcp)
        json_bytes = format_fastmcp_info(info)

        # Verify it's valid JSON
        import json

        data = json.loads(json_bytes)

        # Check FastMCP-specific fields are present
        assert "server" in data
        assert data["server"]["name"] == "TestServer"
        assert data["server"]["instructions"] == "Test instructions"
        assert data["server"]["generation"] == 2  # v2 server
        assert data["server"]["version"] == "1.2.3"
        assert "capabilities" in data["server"]

        # Check environment information
        assert "environment" in data
        assert data["environment"]["fastmcp"] == fastmcp.__version__
        assert data["environment"]["mcp"] == importlib.metadata.version("mcp")

        # Check tools
        assert len(data["tools"]) == 1
        assert data["tools"][0]["name"] == "test_tool"
        assert data["tools"][0]["enabled"] is True
        assert "tags" in data["tools"][0]

    async def test_format_mcp_info(self):
        """Test formatting as MCP protocol JSON."""
        mcp = FastMCP("TestServer", instructions="Test instructions", version="2.0.0")

        @mcp.tool
        def add(a: int, b: int) -> int:
            """Add two numbers."""
            return a + b

        @mcp.prompt
        def test_prompt(name: str) -> list:
            """Test prompt."""
            return [{"role": "user", "content": f"Hello {name}"}]

        json_bytes = await format_mcp_info(mcp)

        # Verify it's valid JSON
        import json

        data = json.loads(json_bytes)

        # Check MCP protocol structure with camelCase
        assert "serverInfo" in data
        assert data["serverInfo"]["name"] == "TestServer"

        # Check server version in MCP format
        assert data["serverInfo"]["version"] == "2.0.0"

        # MCP format SHOULD have environment fields
        assert "environment" in data
        assert data["environment"]["fastmcp"] == fastmcp.__version__
        assert data["environment"]["mcp"] == importlib.metadata.version("mcp")
        assert "capabilities" in data

        assert "tools" in data
        assert "prompts" in data
        assert "resources" in data
        assert "resourceTemplates" in data

        # Check tools have MCP format (camelCase fields)
        assert len(data["tools"]) == 1
        assert data["tools"][0]["name"] == "add"
        assert "inputSchema" in data["tools"][0]

        # FastMCP-specific fields should not be present
        assert "tags" not in data["tools"][0]
        assert "enabled" not in data["tools"][0]

    async def test_format_info_with_fastmcp_format(self):
        """Test format_info with fastmcp format."""
        mcp = FastMCP("TestServer")

        @mcp.tool
        def test() -> str:
            return "test"

        # Test with string format
        json_bytes = await format_info(mcp, "fastmcp")
        import json

        data = json.loads(json_bytes)
        assert data["server"]["name"] == "TestServer"
        assert "tags" in data["tools"][0]  # FastMCP-specific field

        # Test with enum format
        json_bytes = await format_info(mcp, InspectFormat.FASTMCP)
        data = json.loads(json_bytes)
        assert data["server"]["name"] == "TestServer"

    async def test_format_info_with_mcp_format(self):
        """Test format_info with mcp format."""
        mcp = FastMCP("TestServer")

        @mcp.tool
        def test() -> str:
            return "test"

        json_bytes = await format_info(mcp, "mcp")

        import json

        data = json.loads(json_bytes)
        assert "serverInfo" in data
        assert "tools" in data
        assert "inputSchema" in data["tools"][0]  # MCP uses camelCase

    async def test_format_info_requires_format(self):
        """Test that format_info requires a format parameter."""
        mcp = FastMCP("TestServer")

        @mcp.tool
        def test() -> str:
            return "test"

        # Should work with valid formats
        json_bytes = await format_info(mcp, "fastmcp")
        assert json_bytes

        json_bytes = await format_info(mcp, "mcp")
        assert json_bytes

        # Should fail with invalid format
        import pytest

        with pytest.raises(ValueError, match="not a valid InspectFormat"):
            await format_info(mcp, "invalid")  # type: ignore

    async def test_tool_with_output_schema(self):
        """Test that output_schema is properly extracted and included."""
        mcp = FastMCP("TestServer")

        @mcp.tool(
            output_schema={
                "type": "object",
                "properties": {
                    "result": {"type": "number"},
                    "message": {"type": "string"},
                },
            }
        )
        def compute(x: int) -> dict:
            """Compute something."""
            return {"result": x * 2, "message": f"Doubled {x}"}

        info = await inspect_fastmcp(mcp)

        # Check output_schema is captured
        assert len(info.tools) == 1
        assert info.tools[0].output_schema is not None
        assert info.tools[0].output_schema["type"] == "object"
        assert "result" in info.tools[0].output_schema["properties"]

        # Verify it's included in FastMCP format
        json_bytes = format_fastmcp_info(info)
        import json

        data = json.loads(json_bytes)
        # Tools are at the top level, not nested
        assert data["tools"][0]["output_schema"]["type"] == "object"
