"""Tests for tool modules.

Tests cover:
- Tool module imports
- get_tools function
- Handler function existence
- Tool schema generation
"""
from __future__ import annotations

import asyncio
from unittest.mock import AsyncMock, patch
import pytest
from mcp import types as mcp_types


# =============================================================================
# Fixtures
# =============================================================================

@pytest.fixture
def mock_client():
    """Create a mock MetaClient for tool testing."""
    client = AsyncMock()
    client.get = AsyncMock(return_value={"data": []})
    client.post = AsyncMock(return_value={"id": "123"})
    client.delete = AsyncMock(return_value={"success": True})
    client.access_token = "test_token"
    client.version = "v24.0"
    return client


class _FakeMCPServer:
    def __init__(self):
        self.list_tools_handler = None
        self.call_tool_handler = None

    def list_tools(self):
        def decorator(func):
            self.list_tools_handler = func
            return func
        return decorator

    def call_tool(self):
        def decorator(func):
            self.call_tool_handler = func
            return func
        return decorator


class _FakeMCP:
    def __init__(self):
        self._mcp_server = _FakeMCPServer()


# =============================================================================
# Tool Module Import Tests
# =============================================================================

class TestToolModuleImports:
    """Tests for tool module imports."""

    def test_ad_management_imports(self):
        """Test ad_management module imports."""
        from src.tools.ad_management import get_tools
        assert callable(get_tools)

    def test_advantage_imports(self):
        """Test advantage module imports."""
        from src.tools.advantage import get_tools
        assert callable(get_tools)

    def test_audiences_imports(self):
        """Test audiences module imports."""
        from src.tools.audiences import get_tools
        assert callable(get_tools)

    def test_insights_imports(self):
        """Test insights module imports."""
        from src.tools.insights import get_tools
        assert callable(get_tools)

    def test_creative_imports(self):
        """Test creative module imports."""
        from src.tools.creative import get_tools
        assert callable(get_tools)

    def test_conversion_imports(self):
        """Test conversion module imports."""
        from src.tools.conversion import get_tools
        assert callable(get_tools)

    def test_fraud_imports(self):
        """Test fraud module imports."""
        from src.tools.fraud import get_tools
        assert callable(get_tools)

    def test_monitoring_imports(self):
        """Test monitoring module imports."""
        from src.tools.monitoring import get_tools
        assert callable(get_tools)

    def test_tactical_imports(self):
        """Test tactical module imports."""
        from src.tools.tactical import get_tools
        assert callable(get_tools)

    def test_reporting_imports(self):
        """Test reporting module imports."""
        from src.tools.reporting import get_tools
        assert callable(get_tools)

    def test_roas_imports(self):
        """Test roas module imports."""
        from src.tools.roas import get_tools
        assert callable(get_tools)


# =============================================================================
# Tool Registration Tests
# =============================================================================

class TestToolRegistration:
    """Tests for tool registration."""

    def test_ad_management_tools_registered(self, mock_client):
        """Test ad_management tools are registered."""
        from src.tools.ad_management import get_tools
        
        tools = get_tools(mock_client)
        
        assert len(tools) > 0
        for name, (tool, handler) in tools.items():
            assert name.startswith("meta.")
            assert callable(handler)

    def test_audiences_tools_registered(self, mock_client):
        """Test audiences tools are registered."""
        from src.tools.audiences import get_tools
        
        tools = get_tools(mock_client)
        
        assert len(tools) > 0
        # Should include v24 compliance tools
        tool_names = list(tools.keys())
        assert any("audience" in name for name in tool_names)

    def test_conversion_tools_registered(self, mock_client):
        """Test conversion tools are registered."""
        from src.tools.conversion import get_tools
        
        tools = get_tools(mock_client)
        
        assert len(tools) > 0
        # Should include v24 flagged check
        tool_names = list(tools.keys())
        assert any("conversion" in name for name in tool_names)

    def test_fraud_tools_registered(self, mock_client):
        """Test fraud tools are registered."""
        from src.tools.fraud import get_tools
        
        tools = get_tools(mock_client)
        
        assert len(tools) > 0
        tool_names = list(tools.keys())
        assert any("fraud" in name for name in tool_names)

    @pytest.mark.asyncio
    async def test_register_all_tools_call_handler_executes_tool(self, mock_client):
        """Test end-to-end call handler execution for a registered tool."""
        from src.tools import register_all_tools

        mock_client.request = AsyncMock(return_value={"data": []})
        fake_mcp = _FakeMCP()

        with patch("src.meta_client.get_client", return_value=mock_client):
            register_all_tools(fake_mcp)
            assert fake_mcp._mcp_server.call_tool_handler is not None

            result = await fake_mcp._mcp_server.call_tool_handler(
                "meta.graph.request",
                {"method": "GET", "path": "me"},
            )

        assert isinstance(result, dict)
        assert result == {"data": []}
        mock_client.request.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_register_all_tools_unknown_tool_returns_structured_error(self, mock_client):
        """Unknown tools should return MCP error payloads, not raw exceptions."""
        from src.tools import register_all_tools

        fake_mcp = _FakeMCP()
        with patch("src.meta_client.get_client", return_value=mock_client):
            register_all_tools(fake_mcp)
            result = await fake_mcp._mcp_server.call_tool_handler(
                "meta.this_does_not_exist",
                {},
            )

        assert result.isError is True
        assert result.structuredContent["error"]["code"] == "VALIDATION_ERROR"

    @pytest.mark.asyncio
    async def test_register_all_tools_cache_error_returns_degraded_success(self, mock_client):
        """Cache failures should degrade gracefully without returning an MCP error."""
        from src import tools as tools_module
        from src.errors import CacheError

        fake_mcp = _FakeMCP()

        async def _handler(_: dict):
            raise CacheError("redis unavailable", operation="get")

        class _FakeModule:
            @staticmethod
            def get_tools(client):
                return {
                    "meta.test.cache_error": (
                        mcp_types.Tool(
                            name="meta.test.cache_error",
                            description="Cache error fallback test tool",
                            inputSchema={"type": "object", "properties": {}},
                        ),
                        _handler,
                    )
                }

        with (
            patch.object(tools_module, "TOOL_MODULES", [_FakeModule]),
            patch("src.meta_client.get_client", return_value=mock_client),
            patch("src.licensing.is_tool_allowed", return_value=True),
        ):
            tools_module.register_all_tools(fake_mcp)
            result = await fake_mcp._mcp_server.call_tool_handler(
                "meta.test.cache_error",
                {},
            )

        assert isinstance(result, dict)
        assert result["cache"] == "degraded"

    @pytest.mark.asyncio
    async def test_register_all_tools_loads_with_client_once_when_listed_concurrently(self, mock_client):
        """Concurrent list requests should not race during lazy load."""
        from src.schemas import ToolResult
        from src import tools as tools_module

        fake_mcp = _FakeMCP()
        load_calls = []

        async def _handler(_: dict) -> ToolResult:
            return ToolResult(ok=True, data={"ok": True})

        class _FakeModule:
            @staticmethod
            def get_tools(client):
                load_calls.append(client)
                return {
                    "meta.test.ping": (
                        mcp_types.Tool(
                            name="meta.test.ping",
                            description="Ping tool for concurrency testing",
                            inputSchema={"type": "object", "properties": {}},
                        ),
                        _handler,
                    )
                }

        with (
            patch.object(tools_module, "TOOL_MODULES", [_FakeModule]),
            patch("src.meta_client.get_client", return_value=mock_client),
        ):
            tools_module.register_all_tools(fake_mcp)
            await asyncio.gather(
                fake_mcp._mcp_server.list_tools_handler(),
                fake_mcp._mcp_server.list_tools_handler(),
            )

        # One deferred load with None + one client-aware load under lock.
        assert len(load_calls) == 2
        assert load_calls[0] is None
        assert load_calls[1] is mock_client


# =============================================================================
# Tool Schema Tests
# =============================================================================

class TestToolSchemas:
    """Tests for tool input schemas."""

    def test_tools_have_input_schemas(self, mock_client):
        """Test that all tools have input schemas."""
        from src.tools.audiences import get_tools
        
        tools = get_tools(mock_client)
        
        for name, (tool, handler) in tools.items():
            assert tool.inputSchema is not None, f"{name} missing inputSchema"
            assert "type" in tool.inputSchema, f"{name} has invalid inputSchema"

    def test_tool_schema_has_properties(self, mock_client):
        """Test that tool schemas have properties."""
        from src.tools.conversion import get_tools
        
        tools = get_tools(mock_client)
        
        for name, (tool, handler) in tools.items():
            schema = tool.inputSchema
            # Schema should have properties or be empty for no-arg tools
            assert "type" in schema


# =============================================================================
# Platform-Specific Tool Tests
# =============================================================================

class TestPlatformTools:
    """Tests for platform-specific tools."""

    def test_threads_module_imports(self):
        """Test threads module imports."""
        from src.tools.threads import get_tools
        assert callable(get_tools)

    def test_whatsapp_flows_module_imports(self):
        """Test whatsapp_flows module imports."""
        from src.tools.whatsapp_flows import get_tools
        assert callable(get_tools)

    def test_reels_module_imports(self):
        """Test reels module imports."""
        from src.tools.reels import get_tools
        assert callable(get_tools)

    def test_ccco_module_imports(self):
        """Test ccco (Cross-Channel Conversion Optimization) module imports."""
        from src.tools.ccco import get_tools
        assert callable(get_tools)

    def test_signals_module_imports(self):
        """Test signals module imports."""
        from src.tools.signals import get_tools
        assert callable(get_tools)

    def test_advantage_tools_registered(self, mock_client):
        """Test advantage tools are registered and include migration support."""
        from src.tools.advantage import get_tools

        tools = get_tools(mock_client)
        assert "meta.advantage.migrate_to_advantage_plus" in tools
        assert "meta.advantage.check_v24_readiness" in tools


# =============================================================================
# Advanced Tool Tests
# =============================================================================

class TestAdvancedTools:
    """Tests for advanced tool modules."""

    def test_portfolio_module_imports(self):
        """Test portfolio module imports."""
        from src.tools.portfolio import get_tools
        assert callable(get_tools)

    def test_predictive_module_imports(self):
        """Test predictive module imports."""
        from src.tools.predictive import get_tools
        assert callable(get_tools)

    def test_analysis_module_imports(self):
        """Test analysis module imports."""
        from src.tools.analysis import get_tools
        assert callable(get_tools)

    def test_comparison_module_imports(self):
        """Test comparison module imports."""
        from src.tools.comparison import get_tools
        assert callable(get_tools)


class TestTypedToolHandlers:
    """Tests for handlers that adapt dict inputs into typed request models."""

    @pytest.mark.asyncio
    async def test_targeting_demographic_handler_uses_request_model(self, mock_client):
        """Targeting handler should pass a typed request object to the engine."""
        from src.schemas import DemographicPerformanceProfilerRequest
        from src.tools.targeting import demographic_performance_profiler_handler

        class _FakeEngine:
            def __init__(self, client):
                self.client = client

            async def demographic_performance_profiler(self, request):
                assert isinstance(request, DemographicPerformanceProfilerRequest)
                assert request.ad_account_id == "act_123"
                return {"ok": True, "campaigns_analyzed": 0}

        with patch("src.targeting_intelligence.TargetingIntelligenceEngine", _FakeEngine):
            result = await demographic_performance_profiler_handler(
                mock_client,
                {"ad_account_id": "act_123"},
            )

        assert result.ok is True
        assert isinstance(result.data, dict)
        assert result.data["ok"] is True

    @pytest.mark.asyncio
    async def test_creative_handler_requires_ad_identifier(self, mock_client):
        """Creative copy analysis needs ad_ids (or ad_id) to build request model."""
        from src.tools.creative import analyze_ad_copy_performance_handler

        result = await analyze_ad_copy_performance_handler(
            mock_client,
            {"ad_account_id": "act_123"},
        )

        assert result.ok is False
        assert "ad_ids" in result.error["message"]

    @pytest.mark.asyncio
    async def test_creative_handler_uses_request_model(self, mock_client):
        """Creative handler should build and pass typed request model."""
        from src.schemas import AdCopyAnalysisRequest
        from src.tools.creative import analyze_ad_copy_performance_handler

        class _FakeEngine:
            def __init__(self, client):
                self.client = client

            async def analyze_ad_copy_performance(self, request):
                assert isinstance(request, AdCopyAnalysisRequest)
                assert request.ad_ids == ["ad_1"]
                return {"analysis": "ok"}

        with patch("src.creative_intelligence.CreativeIntelligenceEngine", _FakeEngine):
            result = await analyze_ad_copy_performance_handler(
                mock_client,
                {"ad_id": "ad_1"},
            )

        assert result.ok is True
        assert result.data["analysis"] == "ok"


# =============================================================================
# Tool Count Tests
# =============================================================================

class TestToolCount:
    """Tests for tool count verification."""

    def test_minimum_tool_count(self, mock_client):
        """Test that minimum expected tools are registered."""
        from src.tools import TOOL_MODULES
        
        all_tools = {}
        for module in TOOL_MODULES:
            try:
                tools = module.get_tools(mock_client)
                all_tools.update(tools)
            except Exception as exc:
                pytest.fail(f"Failed to register tools from {module.__name__}: {exc}")
        
        # Should have at least 250 tools (some modules may not register with mock client)
        assert len(all_tools) >= 250, f"Expected at least 250 tools, got {len(all_tools)}"

    def test_tools_have_unique_names(self, mock_client):
        """Test that all tool names are unique."""
        from src.tools import TOOL_MODULES
        
        all_tools = {}
        for module in TOOL_MODULES:
            try:
                tools = module.get_tools(mock_client)
                all_tools.update(tools)
            except Exception as exc:
                pytest.fail(f"Failed to register tools from {module.__name__}: {exc}")
        
        names = list(all_tools.keys())
        unique_names = set(names)
        
        assert len(names) == len(unique_names), "Duplicate tool names found"
