"""Tests for custom component subclasses with task support.

Verifies that custom Tool, Resource, and Prompt subclasses can use
background task execution by setting task_config.
"""

import asyncio
from typing import Any
from unittest.mock import MagicMock

import pytest

from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.server.tasks import TaskConfig
from fastmcp.tools.base import Tool, ToolResult
from fastmcp.utilities.components import FastMCPComponent


class CustomTool(Tool):
    """A custom tool subclass with task support."""

    task_config: TaskConfig = TaskConfig(mode="optional")
    parameters: dict[str, Any] = {"type": "object", "properties": {}}

    async def run(self, arguments: dict[str, Any]) -> ToolResult:
        return ToolResult(content=f"Custom tool executed with {arguments}")


class CustomToolWithLogic(Tool):
    """A custom tool with actual async work."""

    task_config: TaskConfig = TaskConfig(mode="optional")
    parameters: dict[str, Any] = {
        "type": "object",
        "properties": {"duration": {"type": "integer"}},
    }

    async def run(self, arguments: dict[str, Any]) -> ToolResult:
        duration = arguments.get("duration", 0)
        await asyncio.sleep(duration * 0.01)  # Short sleep for testing
        return ToolResult(content=f"Completed after {duration} units")


class CustomToolForbidden(Tool):
    """A custom tool with task_config forbidden (default)."""

    parameters: dict[str, Any] = {"type": "object", "properties": {}}

    async def run(self, arguments: dict[str, Any]) -> ToolResult:
        return ToolResult(content="Sync only")


@pytest.fixture
def custom_tool_server():
    """Create a server with custom tool subclasses."""
    mcp = FastMCP("custom-tool-server")
    mcp.add_tool(CustomTool(name="custom_tool", description="A custom tool"))
    mcp.add_tool(
        CustomToolWithLogic(name="custom_logic", description="Custom tool with logic")
    )
    mcp.add_tool(
        CustomToolForbidden(name="custom_forbidden", description="No task support")
    )
    return mcp


async def test_custom_tool_sync_execution(custom_tool_server):
    """Custom tool executes synchronously when no task metadata."""
    async with Client(custom_tool_server) as client:
        result = await client.call_tool("custom_tool", {})
        assert "Custom tool executed" in str(result)


async def test_custom_tool_background_execution(custom_tool_server):
    """Custom tool executes as background task when task=True."""
    async with Client(custom_tool_server) as client:
        task = await client.call_tool("custom_tool", {}, task=True)

        assert task is not None
        assert not task.returned_immediately
        assert task.task_id is not None

        # Wait for result
        result = await task.result()
        assert "Custom tool executed" in str(result)


async def test_custom_tool_with_arguments(custom_tool_server):
    """Custom tool receives arguments correctly in background execution."""
    async with Client(custom_tool_server) as client:
        task = await client.call_tool("custom_logic", {"duration": 1}, task=True)

        assert task is not None
        result = await task.result()
        assert "Completed after 1 units" in str(result)


async def test_custom_tool_forbidden_sync_only(custom_tool_server):
    """Custom tool with forbidden mode executes sync only."""
    async with Client(custom_tool_server) as client:
        # Sync execution works
        result = await client.call_tool("custom_forbidden", {})
        assert "Sync only" in str(result)


async def test_custom_tool_forbidden_rejects_task(custom_tool_server):
    """Custom tool with forbidden mode returns error for task request."""
    async with Client(custom_tool_server) as client:
        task = await client.call_tool("custom_forbidden", {}, task=True)

        # Should return immediately with error
        assert task.returned_immediately


async def test_custom_tool_registers_with_docket():
    """Verify custom tool's register_with_docket is called during server startup."""
    from unittest.mock import MagicMock

    tool = CustomTool(name="test", description="test")
    mock_docket = MagicMock()

    tool.register_with_docket(mock_docket)

    # Should register self.run with docket using prefixed key
    mock_docket.register.assert_called_once()
    call_args = mock_docket.register.call_args
    assert call_args[1]["names"] == ["tool:test@"]


async def test_custom_tool_forbidden_does_not_register():
    """Verify custom tool with forbidden mode doesn't register with docket."""
    tool = CustomToolForbidden(name="test", description="test")
    mock_docket = MagicMock()

    tool.register_with_docket(mock_docket)

    # Should NOT register
    mock_docket.register.assert_not_called()


# ==============================================================================
# Base FastMCPComponent Tests
# ==============================================================================


class TestFastMCPComponentDocketMethods:
    """Tests for base FastMCPComponent docket integration."""

    def test_default_task_config_is_forbidden(self):
        """Base component defaults to task_config mode='forbidden'."""
        component = FastMCPComponent(name="test")
        assert component.task_config.mode == "forbidden"

    def test_register_with_docket_is_noop(self):
        """Base register_with_docket does nothing (subclasses override)."""
        component = FastMCPComponent(name="test")
        mock_docket = MagicMock()

        # Should not raise, just no-op
        component.register_with_docket(mock_docket)

        # Should not have called any docket methods
        mock_docket.register.assert_not_called()

    async def test_add_to_docket_raises_when_forbidden(self):
        """Base add_to_docket raises RuntimeError when mode is 'forbidden'."""
        component = FastMCPComponent(name="test")
        mock_docket = MagicMock()

        with pytest.raises(RuntimeError, match="task execution not supported"):
            await component.add_to_docket(mock_docket)

    async def test_add_to_docket_raises_not_implemented_when_allowed(self):
        """Base add_to_docket raises NotImplementedError when not forbidden."""
        component = FastMCPComponent(
            name="test", task_config=TaskConfig(mode="optional")
        )
        mock_docket = MagicMock()

        with pytest.raises(
            NotImplementedError, match="does not implement add_to_docket"
        ):
            await component.add_to_docket(mock_docket)
