from collections.abc import Callable

import pytest

import mcp.types as types
from mcp.server import Server
from mcp.server.fastmcp import FastMCP
from mcp.shared.memory import create_connected_server_and_client_session as create_session
from mcp.types import ListToolsRequest, ListToolsResult

from .conftest import StreamSpyCollection

pytestmark = pytest.mark.anyio


@pytest.fixture
async def full_featured_server():
    """Create a server with tools, resources, prompts, and templates."""
    server = FastMCP("test")

    @server.tool(name="test_tool_1")
    async def test_tool_1() -> str:  # pragma: no cover
        """First test tool"""
        return "Result 1"

    @server.tool(name="test_tool_2")
    async def test_tool_2() -> str:  # pragma: no cover
        """Second test tool"""
        return "Result 2"

    @server.resource("resource://test/data")
    async def test_resource() -> str:  # pragma: no cover
        """Test resource"""
        return "Test data"

    @server.prompt()
    async def test_prompt(name: str) -> str:  # pragma: no cover
        """Test prompt"""
        return f"Hello, {name}!"

    @server.resource("resource://test/{name}")
    async def test_template(name: str) -> str:  # pragma: no cover
        """Test resource template"""
        return f"Data for {name}"

    return server


@pytest.mark.parametrize(
    "method_name,request_method",
    [
        ("list_tools", "tools/list"),
        ("list_resources", "resources/list"),
        ("list_prompts", "prompts/list"),
        ("list_resource_templates", "resources/templates/list"),
    ],
)
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
async def test_list_methods_cursor_parameter(
    stream_spy: Callable[[], StreamSpyCollection],
    full_featured_server: FastMCP,
    method_name: str,
    request_method: str,
):
    """Test that the cursor parameter is accepted and correctly passed to the server.

    Covers: list_tools, list_resources, list_prompts, list_resource_templates

    See: https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/pagination#request-format
    """
    async with create_session(full_featured_server._mcp_server) as client_session:
        spies = stream_spy()

        # Test without cursor parameter (omitted)
        method = getattr(client_session, method_name)
        _ = await method()
        requests = spies.get_client_requests(method=request_method)
        assert len(requests) == 1
        assert requests[0].params is None

        spies.clear()

        # Test with cursor=None
        _ = await method(cursor=None)
        requests = spies.get_client_requests(method=request_method)
        assert len(requests) == 1
        assert requests[0].params is None

        spies.clear()

        # Test with cursor as string
        _ = await method(cursor="some_cursor_value")
        requests = spies.get_client_requests(method=request_method)
        assert len(requests) == 1
        assert requests[0].params is not None
        assert requests[0].params["cursor"] == "some_cursor_value"

        spies.clear()

        # Test with empty string cursor
        _ = await method(cursor="")
        requests = spies.get_client_requests(method=request_method)
        assert len(requests) == 1
        assert requests[0].params is not None
        assert requests[0].params["cursor"] == ""


@pytest.mark.parametrize(
    "method_name,request_method",
    [
        ("list_tools", "tools/list"),
        ("list_resources", "resources/list"),
        ("list_prompts", "prompts/list"),
        ("list_resource_templates", "resources/templates/list"),
    ],
)
async def test_list_methods_params_parameter(
    stream_spy: Callable[[], StreamSpyCollection],
    full_featured_server: FastMCP,
    method_name: str,
    request_method: str,
):
    """Test that the params parameter works correctly for list methods.

    Covers: list_tools, list_resources, list_prompts, list_resource_templates

    This tests the new params parameter API (non-deprecated) to ensure
    it correctly handles all parameter combinations.
    """
    async with create_session(full_featured_server._mcp_server) as client_session:
        spies = stream_spy()
        method = getattr(client_session, method_name)

        # Test without params parameter (omitted)
        _ = await method()
        requests = spies.get_client_requests(method=request_method)
        assert len(requests) == 1
        assert requests[0].params is None

        spies.clear()

        # Test with params=None
        _ = await method(params=None)
        requests = spies.get_client_requests(method=request_method)
        assert len(requests) == 1
        assert requests[0].params is None

        spies.clear()

        # Test with empty params (for strict servers)
        _ = await method(params=types.PaginatedRequestParams())
        requests = spies.get_client_requests(method=request_method)
        assert len(requests) == 1
        assert requests[0].params is not None
        assert requests[0].params.get("cursor") is None

        spies.clear()

        # Test with params containing cursor
        _ = await method(params=types.PaginatedRequestParams(cursor="some_cursor_value"))
        requests = spies.get_client_requests(method=request_method)
        assert len(requests) == 1
        assert requests[0].params is not None
        assert requests[0].params["cursor"] == "some_cursor_value"


@pytest.mark.parametrize(
    "method_name",
    [
        "list_tools",
        "list_resources",
        "list_prompts",
        "list_resource_templates",
    ],
)
async def test_list_methods_raises_error_when_both_cursor_and_params_provided(
    full_featured_server: FastMCP,
    method_name: str,
):
    """Test that providing both cursor and params raises ValueError.

    Covers: list_tools, list_resources, list_prompts, list_resource_templates

    When both cursor and params are provided, a ValueError should be raised
    to prevent ambiguity.
    """
    async with create_session(full_featured_server._mcp_server) as client_session:
        method = getattr(client_session, method_name)

        # Call with both cursor and params - should raise ValueError
        with pytest.raises(ValueError, match="Cannot specify both cursor and params"):
            await method(
                cursor="old_cursor",
                params=types.PaginatedRequestParams(cursor="new_cursor"),
            )


async def test_list_tools_with_strict_server_validation():
    """Test that list_tools works with strict servers require a params field,
    even if it is empty.

    Some MCP servers may implement strict JSON-RPC validation that requires
    the params field to always be present in requests, even if empty {}.

    This test ensures such servers are supported by the client SDK for list_resources
    requests without a cursor.
    """

    server = Server("strict_server")

    @server.list_tools()
    async def handle_list_tools(request: ListToolsRequest) -> ListToolsResult:  # pragma: no cover
        """Strict handler that validates params field exists"""

        # Simulate strict server validation
        if request.params is None:
            raise ValueError(
                "Strict server validation failed: params field must be present. "
                "Expected params: {} for requests without cursor."
            )

        # Return empty tools list
        return ListToolsResult(tools=[])

    async with create_session(server) as client_session:
        # Use params to explicitly send params: {} for strict server compatibility
        result = await client_session.list_tools(params=types.PaginatedRequestParams())
        assert result is not None
