"""Test URL mode elicitation feature (SEP 1036)."""

import anyio
import mcp_types as types
import pytest
from mcp_types import ElicitRequestParams, ElicitResult, TextContent
from pydantic import BaseModel, Field

from mcp import Client
from mcp.client import ClientRequestContext
from mcp.server.elicitation import CancelledElicitation, DeclinedElicitation, elicit_url
from mcp.server.mcpserver import Context, MCPServer


@pytest.mark.anyio
async def test_url_elicitation_accept():
    """Test URL mode elicitation with user acceptance."""
    mcp = MCPServer(name="URLElicitationServer")

    @mcp.tool(description="A tool that uses URL elicitation")
    async def request_api_key(ctx: Context) -> str:
        result = await ctx.session.elicit_url(
            message="Please provide your API key to continue.",
            url="https://example.com/api_key_setup",
            elicitation_id="test-elicitation-001",
        )
        # Test only checks accept path
        return f"User {result.action}"

    # Create elicitation callback that accepts URL mode
    async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
        assert params.mode == "url"
        assert params.url == "https://example.com/api_key_setup"
        assert params.elicitation_id == "test-elicitation-001"
        assert params.message == "Please provide your API key to continue."
        return ElicitResult(action="accept")

    async with Client(mcp, mode="legacy", elicitation_callback=elicitation_callback) as client:
        result = await client.call_tool("request_api_key", {})
        assert len(result.content) == 1
        assert isinstance(result.content[0], TextContent)
        assert result.content[0].text == "User accept"


@pytest.mark.anyio
async def test_url_elicitation_decline():
    """Test URL mode elicitation with user declining."""
    mcp = MCPServer(name="URLElicitationDeclineServer")

    @mcp.tool(description="A tool that uses URL elicitation")
    async def oauth_flow(ctx: Context) -> str:
        result = await ctx.session.elicit_url(
            message="Authorize access to your files.",
            url="https://example.com/oauth/authorize",
            elicitation_id="oauth-001",
        )
        # Test only checks decline path
        return f"User {result.action} authorization"

    async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
        assert params.mode == "url"
        return ElicitResult(action="decline")

    async with Client(mcp, mode="legacy", elicitation_callback=elicitation_callback) as client:
        result = await client.call_tool("oauth_flow", {})
        assert len(result.content) == 1
        assert isinstance(result.content[0], TextContent)
        assert result.content[0].text == "User decline authorization"


@pytest.mark.anyio
async def test_url_elicitation_cancel():
    """Test URL mode elicitation with user cancelling."""
    mcp = MCPServer(name="URLElicitationCancelServer")

    @mcp.tool(description="A tool that uses URL elicitation")
    async def payment_flow(ctx: Context) -> str:
        result = await ctx.session.elicit_url(
            message="Complete payment to proceed.",
            url="https://example.com/payment",
            elicitation_id="payment-001",
        )
        # Test only checks cancel path
        return f"User {result.action} payment"

    async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
        assert params.mode == "url"
        return ElicitResult(action="cancel")

    async with Client(mcp, mode="legacy", elicitation_callback=elicitation_callback) as client:
        result = await client.call_tool("payment_flow", {})
        assert len(result.content) == 1
        assert isinstance(result.content[0], TextContent)
        assert result.content[0].text == "User cancel payment"


@pytest.mark.anyio
async def test_url_elicitation_helper_function():
    """Test the elicit_url helper function."""
    mcp = MCPServer(name="URLElicitationHelperServer")

    @mcp.tool(description="Tool using elicit_url helper")
    async def setup_credentials(ctx: Context) -> str:
        result = await elicit_url(
            session=ctx.session,
            message="Set up your credentials",
            url="https://example.com/setup",
            elicitation_id="setup-001",
        )
        # Test only checks accept path - return the type name
        return type(result).__name__

    async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
        return ElicitResult(action="accept")

    async with Client(mcp, mode="legacy", elicitation_callback=elicitation_callback) as client:
        result = await client.call_tool("setup_credentials", {})
        assert len(result.content) == 1
        assert isinstance(result.content[0], TextContent)
        assert result.content[0].text == "AcceptedUrlElicitation"


@pytest.mark.anyio
async def test_url_no_content_in_response():
    """Test that URL mode elicitation responses don't include content field."""
    mcp = MCPServer(name="URLContentCheckServer")

    @mcp.tool(description="Check URL response format")
    async def check_url_response(ctx: Context) -> str:
        result = await ctx.session.elicit_url(
            message="Test message",
            url="https://example.com/test",
            elicitation_id="test-001",
        )

        # URL mode responses should not have content
        assert result.content is None
        return f"Action: {result.action}, Content: {result.content}"

    async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
        # Verify that this is URL mode
        assert params.mode == "url"
        assert isinstance(params, types.ElicitRequestURLParams)
        # URL params have url and elicitation_id, not requested_schema
        assert params.url == "https://example.com/test"
        assert params.elicitation_id == "test-001"
        # Return without content - this is correct for URL mode
        return ElicitResult(action="accept")

    async with Client(mcp, mode="legacy", elicitation_callback=elicitation_callback) as client:
        result = await client.call_tool("check_url_response", {})
        assert len(result.content) == 1
        assert isinstance(result.content[0], TextContent)
        assert "Content: None" in result.content[0].text


@pytest.mark.anyio
async def test_form_mode_still_works():
    """Ensure form mode elicitation still works after SEP 1036."""
    mcp = MCPServer(name="FormModeBackwardCompatServer")

    class NameSchema(BaseModel):
        name: str = Field(description="Your name")

    @mcp.tool(description="Test form mode")
    async def ask_name(ctx: Context) -> str:
        result = await ctx.elicit(message="What is your name?", schema=NameSchema)
        # Test only checks accept path with data
        assert result.action == "accept"
        assert result.data is not None
        return f"Hello, {result.data.name}!"

    async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
        # Verify form mode parameters
        assert params.mode == "form"
        assert isinstance(params, types.ElicitRequestFormParams)
        # Form params have requested_schema, not url/elicitation_id
        assert params.requested_schema is not None
        return ElicitResult(action="accept", content={"name": "Alice"})

    async with Client(mcp, mode="legacy", elicitation_callback=elicitation_callback) as client:
        result = await client.call_tool("ask_name", {})
        assert len(result.content) == 1
        assert isinstance(result.content[0], TextContent)
        assert result.content[0].text == "Hello, Alice!"


@pytest.mark.anyio
async def test_elicit_complete_notification():
    """Test that elicitation completion notifications can be sent and received."""
    mcp = MCPServer(name="ElicitCompleteServer")

    # Track if the notification was sent
    notification_sent = False

    @mcp.tool(description="Tool that sends completion notification")
    async def trigger_elicitation(ctx: Context) -> str:
        nonlocal notification_sent

        # Simulate an async operation (e.g., user completing auth in browser)
        elicitation_id = "complete-test-001"

        # Send completion notification
        await ctx.session.send_elicit_complete(elicitation_id)
        notification_sent = True

        return "Elicitation completed"

    async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
        return ElicitResult(action="accept")  # pragma: no cover

    async with Client(mcp, mode="legacy", elicitation_callback=elicitation_callback) as client:
        result = await client.call_tool("trigger_elicitation", {})
        assert len(result.content) == 1
        assert isinstance(result.content[0], TextContent)
        assert result.content[0].text == "Elicitation completed"

        # Give time for notification to be processed
        await anyio.sleep(0.1)

        # Verify the notification was sent
        assert notification_sent


@pytest.mark.anyio
async def test_url_elicitation_required_error_code():
    """Test that the URL_ELICITATION_REQUIRED error code is correct."""
    # Verify the error code matches the specification (SEP 1036)
    assert types.URL_ELICITATION_REQUIRED == -32042, (
        "URL_ELICITATION_REQUIRED error code must be -32042 per SEP 1036 specification"
    )


@pytest.mark.anyio
async def test_elicit_url_typed_results():
    """Test that elicit_url returns properly typed result objects."""
    mcp = MCPServer(name="TypedResultsServer")

    @mcp.tool(description="Test declined result")
    async def test_decline(ctx: Context) -> str:
        result = await elicit_url(
            session=ctx.session,
            message="Test decline",
            url="https://example.com/decline",
            elicitation_id="decline-001",
        )

        if isinstance(result, DeclinedElicitation):
            return "Declined"
        return "Not declined"  # pragma: no cover

    @mcp.tool(description="Test cancelled result")
    async def test_cancel(ctx: Context) -> str:
        result = await elicit_url(
            session=ctx.session,
            message="Test cancel",
            url="https://example.com/cancel",
            elicitation_id="cancel-001",
        )

        if isinstance(result, CancelledElicitation):
            return "Cancelled"
        return "Not cancelled"  # pragma: no cover

    # Test declined result
    async def decline_callback(context: ClientRequestContext, params: ElicitRequestParams):
        return ElicitResult(action="decline")

    async with Client(mcp, mode="legacy", elicitation_callback=decline_callback) as client:
        result = await client.call_tool("test_decline", {})
        assert len(result.content) == 1
        assert isinstance(result.content[0], TextContent)
        assert result.content[0].text == "Declined"

    # Test cancelled result
    async def cancel_callback(context: ClientRequestContext, params: ElicitRequestParams):
        return ElicitResult(action="cancel")

    async with Client(mcp, mode="legacy", elicitation_callback=cancel_callback) as client:
        result = await client.call_tool("test_cancel", {})
        assert len(result.content) == 1
        assert isinstance(result.content[0], TextContent)
        assert result.content[0].text == "Cancelled"


@pytest.mark.anyio
async def test_deprecated_elicit_method():
    """Test the deprecated elicit() method for backward compatibility."""
    mcp = MCPServer(name="DeprecatedElicitServer")

    class EmailSchema(BaseModel):
        email: str = Field(description="Email address")

    @mcp.tool(description="Test deprecated elicit method")
    async def use_deprecated_elicit(ctx: Context) -> str:
        # Use the deprecated elicit() method which should call elicit_form()
        result = await ctx.session.elicit(
            message="Enter your email",
            requested_schema=EmailSchema.model_json_schema(),
        )

        if result.action == "accept" and result.content:
            return f"Email: {result.content.get('email', 'none')}"
        return "No email provided"  # pragma: no cover

    async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
        # Verify this is form mode
        assert params.mode == "form"
        assert params.requested_schema is not None
        return ElicitResult(action="accept", content={"email": "test@example.com"})

    async with Client(mcp, mode="legacy", elicitation_callback=elicitation_callback) as client:
        result = await client.call_tool("use_deprecated_elicit", {})
        assert len(result.content) == 1
        assert isinstance(result.content[0], TextContent)
        assert result.content[0].text == "Email: test@example.com"


@pytest.mark.anyio
async def test_ctx_elicit_url_convenience_method():
    """Test the ctx.elicit_url() convenience method (vs ctx.session.elicit_url())."""
    mcp = MCPServer(name="CtxElicitUrlServer")

    @mcp.tool(description="A tool that uses ctx.elicit_url() directly")
    async def direct_elicit_url(ctx: Context) -> str:
        # Use ctx.elicit_url() directly instead of ctx.session.elicit_url()
        result = await ctx.elicit_url(
            message="Test the convenience method",
            url="https://example.com/test",
            elicitation_id="ctx-test-001",
        )
        return f"Result: {result.action}"

    async def elicitation_callback(context: ClientRequestContext, params: ElicitRequestParams):
        assert params.mode == "url"
        assert params.elicitation_id == "ctx-test-001"
        return ElicitResult(action="accept")

    async with Client(mcp, mode="legacy", elicitation_callback=elicitation_callback) as client:
        result = await client.call_tool("direct_elicit_url", {})
        assert isinstance(result.content[0], TextContent)
        assert result.content[0].text == "Result: accept"
