"""Tests for AMP MCP config fetching and tool resolution."""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from crewai.agent.core import Agent
from crewai.mcp.config import MCPServerHTTP, MCPServerSSE
from crewai.mcp.tool_resolver import MCPToolResolver
from crewai.tools.base_tool import BaseTool


@pytest.fixture
def agent():
    return Agent(
        role="Test Agent",
        goal="Test goal",
        backstory="Test backstory",
    )


@pytest.fixture
def resolver(agent):
    return MCPToolResolver(agent=agent, logger=agent._logger)


@pytest.fixture
def mock_tool_definitions():
    return [
        {
            "name": "search",
            "description": "Search tool",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Search query"}
                },
                "required": ["query"],
            },
        },
        {
            "name": "create_page",
            "description": "Create a page",
            "inputSchema": {},
        },
    ]


class TestBuildMCPConfigFromDict:
    def test_builds_http_config(self):
        config_dict = {
            "type": "http",
            "url": "https://mcp.example.com/api",
            "headers": {"Authorization": "Bearer token123"},
            "streamable": True,
            "cache_tools_list": False,
        }

        result = MCPToolResolver._build_mcp_config_from_dict(config_dict)

        assert isinstance(result, MCPServerHTTP)
        assert result.url == "https://mcp.example.com/api"
        assert result.headers == {"Authorization": "Bearer token123"}
        assert result.streamable is True
        assert result.cache_tools_list is False

    def test_builds_sse_config(self):
        config_dict = {
            "type": "sse",
            "url": "https://mcp.example.com/sse",
            "headers": {"Authorization": "Bearer token123"},
            "cache_tools_list": True,
        }

        result = MCPToolResolver._build_mcp_config_from_dict(config_dict)

        assert isinstance(result, MCPServerSSE)
        assert result.url == "https://mcp.example.com/sse"
        assert result.headers == {"Authorization": "Bearer token123"}
        assert result.cache_tools_list is True

    def test_defaults_to_http(self):
        config_dict = {
            "url": "https://mcp.example.com/api",
        }

        result = MCPToolResolver._build_mcp_config_from_dict(config_dict)

        assert isinstance(result, MCPServerHTTP)
        assert result.streamable is True

    def test_http_defaults(self):
        config_dict = {
            "type": "http",
            "url": "https://mcp.example.com/api",
        }

        result = MCPToolResolver._build_mcp_config_from_dict(config_dict)

        assert result.headers is None
        assert result.streamable is True
        assert result.cache_tools_list is False


class TestFetchAmpMCPConfigs:
    @patch("crewai.plus_api.PlusAPI")
    @patch("crewai_tools.tools.crewai_platform_tools.misc.get_platform_integration_token", return_value="test-api-key")
    def test_fetches_configs_successfully(self, mock_get_token, mock_plus_api_class, resolver):
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "configs": {
                "notion": {
                    "type": "sse",
                    "url": "https://mcp.notion.so/sse",
                    "headers": {"Authorization": "Bearer notion-token"},
                },
                "github": {
                    "type": "http",
                    "url": "https://mcp.github.com/api",
                    "headers": {"Authorization": "Bearer gh-token"},
                },
            },
        }
        mock_plus_api = MagicMock()
        mock_plus_api.get_mcp_configs.return_value = mock_response
        mock_plus_api_class.return_value = mock_plus_api

        result = resolver._fetch_amp_mcp_configs(["notion", "github"])

        assert "notion" in result
        assert "github" in result
        assert result["notion"]["url"] == "https://mcp.notion.so/sse"
        mock_plus_api_class.assert_called_once_with(api_key="test-api-key")
        mock_plus_api.get_mcp_configs.assert_called_once_with(["notion", "github"])

    @patch("crewai.plus_api.PlusAPI")
    @patch("crewai_tools.tools.crewai_platform_tools.misc.get_platform_integration_token", return_value="test-api-key")
    def test_omits_missing_slugs(self, mock_get_token, mock_plus_api_class, resolver):
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "configs": {"notion": {"type": "sse", "url": "https://mcp.notion.so/sse"}},
        }
        mock_plus_api = MagicMock()
        mock_plus_api.get_mcp_configs.return_value = mock_response
        mock_plus_api_class.return_value = mock_plus_api

        result = resolver._fetch_amp_mcp_configs(["notion", "missing-server"])

        assert "notion" in result
        assert "missing-server" not in result

    @patch("crewai.plus_api.PlusAPI")
    @patch("crewai_tools.tools.crewai_platform_tools.misc.get_platform_integration_token", return_value="test-api-key")
    def test_returns_empty_on_http_error(self, mock_get_token, mock_plus_api_class, resolver):
        mock_response = MagicMock()
        mock_response.status_code = 500
        mock_plus_api = MagicMock()
        mock_plus_api.get_mcp_configs.return_value = mock_response
        mock_plus_api_class.return_value = mock_plus_api

        result = resolver._fetch_amp_mcp_configs(["notion"])

        assert result == {}

    @patch("crewai.plus_api.PlusAPI")
    @patch("crewai_tools.tools.crewai_platform_tools.misc.get_platform_integration_token", return_value="test-api-key")
    def test_returns_empty_on_network_error(self, mock_get_token, mock_plus_api_class, resolver):
        import httpx

        mock_plus_api = MagicMock()
        mock_plus_api.get_mcp_configs.side_effect = httpx.ConnectError("Connection refused")
        mock_plus_api_class.return_value = mock_plus_api

        result = resolver._fetch_amp_mcp_configs(["notion"])

        assert result == {}

    @patch("crewai_tools.tools.crewai_platform_tools.misc.get_platform_integration_token", side_effect=Exception("No token"))
    def test_returns_empty_when_no_token(self, mock_get_token, resolver):
        result = resolver._fetch_amp_mcp_configs(["notion"])

        assert result == {}


class TestParseAmpRef:
    def test_bare_slug(self):
        slug, tool = MCPToolResolver._parse_amp_ref("notion")
        assert slug == "notion"
        assert tool is None

    def test_bare_slug_with_tool(self):
        slug, tool = MCPToolResolver._parse_amp_ref("notion#search")
        assert slug == "notion"
        assert tool == "search"

    def test_bare_slug_with_empty_tool(self):
        slug, tool = MCPToolResolver._parse_amp_ref("notion#")
        assert slug == "notion"
        assert tool is None

    def test_legacy_prefix_slug(self):
        slug, tool = MCPToolResolver._parse_amp_ref("crewai-amp:notion")
        assert slug == "notion"
        assert tool is None

    def test_legacy_prefix_with_tool(self):
        slug, tool = MCPToolResolver._parse_amp_ref("crewai-amp:notion#search")
        assert slug == "notion"
        assert tool == "search"


class TestGetMCPToolsAmpIntegration:
    @patch("crewai.mcp.tool_resolver.MCPClient")
    @patch.object(MCPToolResolver, "_fetch_amp_mcp_configs")
    def test_single_request_for_multiple_amp_refs(
        self, mock_fetch, mock_client_class, agent, mock_tool_definitions
    ):
        mock_fetch.return_value = {
            "notion": {
                "type": "sse",
                "url": "https://mcp.notion.so/sse",
                "headers": {"Authorization": "Bearer token"},
            },
            "github": {
                "type": "http",
                "url": "https://mcp.github.com/api",
                "headers": {"Authorization": "Bearer gh-token"},
                "streamable": True,
            },
        }

        mock_client = AsyncMock()
        mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
        mock_client.connected = False
        mock_client.connect = AsyncMock()
        mock_client.disconnect = AsyncMock()
        mock_client_class.return_value = mock_client

        tools = agent.get_mcp_tools(["notion", "github"])

        mock_fetch.assert_called_once_with(["notion", "github"])
        assert len(tools) == 4  # 2 tools per server

    @patch("crewai.mcp.tool_resolver.MCPClient")
    @patch.object(MCPToolResolver, "_fetch_amp_mcp_configs")
    def test_tool_filter_with_hash_syntax(
        self, mock_fetch, mock_client_class, agent, mock_tool_definitions
    ):
        mock_fetch.return_value = {
            "notion": {
                "type": "sse",
                "url": "https://mcp.notion.so/sse",
                "headers": {"Authorization": "Bearer token"},
            },
        }

        mock_client = AsyncMock()
        mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
        mock_client.connected = False
        mock_client.connect = AsyncMock()
        mock_client.disconnect = AsyncMock()
        mock_client_class.return_value = mock_client

        tools = agent.get_mcp_tools(["notion#search"])

        mock_fetch.assert_called_once_with(["notion"])
        assert len(tools) == 1
        assert tools[0].name == "mcp_notion_so_sse_search"

    @patch("crewai.mcp.tool_resolver.MCPClient")
    @patch.object(MCPToolResolver, "_fetch_amp_mcp_configs")
    def test_tool_filter_with_hyphenated_hash_syntax(
        self, mock_fetch, mock_client_class, agent
    ):
        """notion#get-page must match the tool whose sanitized name is get_page."""
        mock_fetch.return_value = {
            "notion": {
                "type": "sse",
                "url": "https://mcp.notion.so/sse",
                "headers": {"Authorization": "Bearer token"},
            },
        }

        hyphenated_tool_definitions = [
            {
                "name": "get_page",
                "original_name": "get-page",
                "description": "Get a page",
                "inputSchema": {},
            },
            {
                "name": "search",
                "original_name": "search",
                "description": "Search tool",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "query": {"type": "string", "description": "Search query"}
                    },
                    "required": ["query"],
                },
            },
        ]

        mock_client = AsyncMock()
        mock_client.list_tools = AsyncMock(return_value=hyphenated_tool_definitions)
        mock_client.connected = False
        mock_client.connect = AsyncMock()
        mock_client.disconnect = AsyncMock()
        mock_client_class.return_value = mock_client

        tools = agent.get_mcp_tools(["notion#get-page"])

        mock_fetch.assert_called_once_with(["notion"])
        assert len(tools) == 1
        assert tools[0].name.endswith("_get_page")

    @patch("crewai.mcp.tool_resolver.MCPClient")
    @patch.object(MCPToolResolver, "_fetch_amp_mcp_configs")
    def test_deduplicates_slugs(
        self, mock_fetch, mock_client_class, agent, mock_tool_definitions
    ):
        mock_fetch.return_value = {
            "notion": {
                "type": "sse",
                "url": "https://mcp.notion.so/sse",
                "headers": {"Authorization": "Bearer token"},
            },
        }

        mock_client = AsyncMock()
        mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
        mock_client.connected = False
        mock_client.connect = AsyncMock()
        mock_client.disconnect = AsyncMock()
        mock_client_class.return_value = mock_client

        tools = agent.get_mcp_tools(["notion#search", "notion#create_page"])

        mock_fetch.assert_called_once_with(["notion"])
        assert len(tools) == 2

    @patch.object(MCPToolResolver, "_fetch_amp_mcp_configs")
    def test_skips_missing_configs_gracefully(self, mock_fetch, agent):
        mock_fetch.return_value = {}

        tools = agent.get_mcp_tools(["missing-server"])

        assert tools == []

    @patch("crewai.mcp.tool_resolver.MCPClient")
    @patch.object(MCPToolResolver, "_fetch_amp_mcp_configs")
    def test_legacy_crewai_amp_prefix_still_works(
        self, mock_fetch, mock_client_class, agent, mock_tool_definitions
    ):
        mock_fetch.return_value = {
            "notion": {
                "type": "sse",
                "url": "https://mcp.notion.so/sse",
                "headers": {"Authorization": "Bearer token"},
            },
        }

        mock_client = AsyncMock()
        mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
        mock_client.connected = False
        mock_client.connect = AsyncMock()
        mock_client.disconnect = AsyncMock()
        mock_client_class.return_value = mock_client

        tools = agent.get_mcp_tools(["crewai-amp:notion"])

        mock_fetch.assert_called_once_with(["notion"])
        assert len(tools) == 2

    @patch("crewai.mcp.tool_resolver.MCPClient")
    @patch.object(MCPToolResolver, "_fetch_amp_mcp_configs")
    @patch.object(MCPToolResolver, "_resolve_external")
    def test_non_amp_items_unaffected(
        self,
        mock_external,
        mock_fetch,
        mock_client_class,
        agent,
        mock_tool_definitions,
    ):
        mock_fetch.return_value = {
            "notion": {
                "type": "sse",
                "url": "https://mcp.notion.so/sse",
            },
        }

        mock_client = AsyncMock()
        mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
        mock_client.connected = False
        mock_client.connect = AsyncMock()
        mock_client.disconnect = AsyncMock()
        mock_client_class.return_value = mock_client

        mock_external_tool = MagicMock(spec=BaseTool)
        mock_external.return_value = [mock_external_tool]

        http_config = MCPServerHTTP(
            url="https://other.mcp.com/api",
            headers={"Authorization": "Bearer other"},
        )

        tools = agent.get_mcp_tools(
            [
                "notion",
                "https://external.mcp.com/api",
                http_config,
            ]
        )

        mock_fetch.assert_called_once_with(["notion"])
        mock_external.assert_called_once_with("https://external.mcp.com/api")
        # 2 from notion + 1 from external + 2 from http_config
        assert len(tools) == 5


class TestResolveExternalToolFilter:
    """Tests for _resolve_external with #tool-name filtering."""

    @pytest.fixture
    def agent(self):
        return Agent(
            role="Test Agent",
            goal="Test goal",
            backstory="Test backstory",
        )

    @pytest.fixture
    def resolver(self, agent):
        return MCPToolResolver(agent=agent, logger=agent._logger)

    @patch.object(MCPToolResolver, "_get_mcp_tool_schemas")
    def test_filters_hyphenated_tool_name(self, mock_schemas, resolver):
        """https://...#get-page must match the sanitized key get_page in schemas."""
        mock_schemas.return_value = {
            "get_page": {
                "description": "Get a page",
                "args_schema": None,
            },
            "search": {
                "description": "Search tool",
                "args_schema": None,
            },
        }

        tools = resolver._resolve_external("https://mcp.example.com/api#get-page")

        assert len(tools) == 1
        assert "get_page" in tools[0].name

    @patch.object(MCPToolResolver, "_get_mcp_tool_schemas")
    def test_filters_underscored_tool_name(self, mock_schemas, resolver):
        """https://...#get_page must also match the sanitized key get_page."""
        mock_schemas.return_value = {
            "get_page": {
                "description": "Get a page",
                "args_schema": None,
            },
            "search": {
                "description": "Search tool",
                "args_schema": None,
            },
        }

        tools = resolver._resolve_external("https://mcp.example.com/api#get_page")

        assert len(tools) == 1
        assert "get_page" in tools[0].name

    @patch.object(MCPToolResolver, "_get_mcp_tool_schemas")
    def test_returns_all_tools_without_hash(self, mock_schemas, resolver):
        mock_schemas.return_value = {
            "get_page": {
                "description": "Get a page",
                "args_schema": None,
            },
            "search": {
                "description": "Search tool",
                "args_schema": None,
            },
        }

        tools = resolver._resolve_external("https://mcp.example.com/api")

        assert len(tools) == 2

    @patch.object(MCPToolResolver, "_get_mcp_tool_schemas")
    def test_returns_empty_for_nonexistent_tool(self, mock_schemas, resolver):
        mock_schemas.return_value = {
            "search": {
                "description": "Search tool",
                "args_schema": None,
            },
        }

        tools = resolver._resolve_external("https://mcp.example.com/api#nonexistent")

        assert len(tools) == 0
