import threading
from typing import Any

import pytest
from mcp_types import (
    ElicitRequest,
    ElicitRequestFormParams,
    EmbeddedResource,
    InputRequiredResult,
    TextContent,
    TextResourceContents,
)

from mcp.server.mcpserver import Context
from mcp.server.mcpserver.prompts.base import AssistantMessage, Message, Prompt, UserMessage


class TestRenderPrompt:
    @pytest.mark.anyio
    async def test_basic_fn(self):
        def fn() -> str:
            return "Hello, world!"

        prompt = Prompt.from_function(fn)
        assert await prompt.render(None, Context()) == [
            UserMessage(content=TextContent(type="text", text="Hello, world!"))
        ]

    @pytest.mark.anyio
    async def test_async_fn(self):
        async def fn() -> str:
            return "Hello, world!"

        prompt = Prompt.from_function(fn)
        assert await prompt.render(None, Context()) == [
            UserMessage(content=TextContent(type="text", text="Hello, world!"))
        ]

    @pytest.mark.anyio
    async def test_fn_with_args(self):
        async def fn(name: str, age: int = 30) -> str:
            return f"Hello, {name}! You're {age} years old."

        prompt = Prompt.from_function(fn)
        assert await prompt.render({"name": "World"}, Context()) == [
            UserMessage(content=TextContent(type="text", text="Hello, World! You're 30 years old."))
        ]

    @pytest.mark.anyio
    async def test_fn_with_invalid_kwargs(self):
        async def fn(name: str, age: int = 30) -> str:  # pragma: no cover
            return f"Hello, {name}! You're {age} years old."

        prompt = Prompt.from_function(fn)
        with pytest.raises(ValueError):
            await prompt.render({"age": 40}, Context())

    @pytest.mark.anyio
    async def test_fn_returns_message(self):
        async def fn() -> UserMessage:
            return UserMessage(content="Hello, world!")

        prompt = Prompt.from_function(fn)
        assert await prompt.render(None, Context()) == [
            UserMessage(content=TextContent(type="text", text="Hello, world!"))
        ]

    @pytest.mark.anyio
    async def test_fn_returns_assistant_message(self):
        async def fn() -> AssistantMessage:
            return AssistantMessage(content=TextContent(type="text", text="Hello, world!"))

        prompt = Prompt.from_function(fn)
        assert await prompt.render(None, Context()) == [
            AssistantMessage(content=TextContent(type="text", text="Hello, world!"))
        ]

    @pytest.mark.anyio
    async def test_fn_returns_multiple_messages(self):
        expected: list[Message] = [
            UserMessage("Hello, world!"),
            AssistantMessage("How can I help you today?"),
            UserMessage("I'm looking for a restaurant in the center of town."),
        ]

        async def fn() -> list[Message]:
            return expected

        prompt = Prompt.from_function(fn)
        assert await prompt.render(None, Context()) == expected

    @pytest.mark.anyio
    async def test_fn_returns_list_of_strings(self):
        expected = [
            "Hello, world!",
            "I'm looking for a restaurant in the center of town.",
        ]

        async def fn() -> list[str]:
            return expected

        prompt = Prompt.from_function(fn)
        assert await prompt.render(None, Context()) == [UserMessage(t) for t in expected]

    @pytest.mark.anyio
    async def test_fn_returns_resource_content(self):
        """Test returning a message with resource content."""

        async def fn() -> UserMessage:
            return UserMessage(
                content=EmbeddedResource(
                    type="resource",
                    resource=TextResourceContents(
                        uri="file://file.txt",
                        text="File contents",
                        mime_type="text/plain",
                    ),
                )
            )

        prompt = Prompt.from_function(fn)
        assert await prompt.render(None, Context()) == [
            UserMessage(
                content=EmbeddedResource(
                    type="resource",
                    resource=TextResourceContents(
                        uri="file://file.txt",
                        text="File contents",
                        mime_type="text/plain",
                    ),
                )
            )
        ]

    @pytest.mark.anyio
    async def test_fn_returns_mixed_content(self):
        """Test returning messages with mixed content types."""

        async def fn() -> list[Message]:
            return [
                UserMessage(content="Please analyze this file:"),
                UserMessage(
                    content=EmbeddedResource(
                        type="resource",
                        resource=TextResourceContents(
                            uri="file://file.txt",
                            text="File contents",
                            mime_type="text/plain",
                        ),
                    )
                ),
                AssistantMessage(content="I'll help analyze that file."),
            ]

        prompt = Prompt.from_function(fn)
        assert await prompt.render(None, Context()) == [
            UserMessage(content=TextContent(type="text", text="Please analyze this file:")),
            UserMessage(
                content=EmbeddedResource(
                    type="resource",
                    resource=TextResourceContents(
                        uri="file://file.txt",
                        text="File contents",
                        mime_type="text/plain",
                    ),
                )
            ),
            AssistantMessage(content=TextContent(type="text", text="I'll help analyze that file.")),
        ]

    @pytest.mark.anyio
    async def test_fn_returns_dict_with_resource(self):
        """Test returning a dict with resource content."""

        async def fn() -> dict[str, Any]:
            return {
                "role": "user",
                "content": {
                    "type": "resource",
                    "resource": {
                        "uri": "file://file.txt",
                        "text": "File contents",
                        "mimeType": "text/plain",
                    },
                },
            }

        prompt = Prompt.from_function(fn)
        assert await prompt.render(None, Context()) == [
            UserMessage(
                content=EmbeddedResource(
                    type="resource",
                    resource=TextResourceContents(
                        uri="file://file.txt",
                        text="File contents",
                        mime_type="text/plain",
                    ),
                )
            )
        ]


@pytest.mark.anyio
async def test_sync_fn_runs_in_worker_thread():
    """Sync prompt functions must run in a worker thread, not the event loop."""

    main_thread = threading.get_ident()
    fn_thread: list[int] = []

    def blocking_fn() -> str:
        fn_thread.append(threading.get_ident())
        return "hello"

    prompt = Prompt.from_function(blocking_fn)
    messages = await prompt.render(None, Context())

    assert messages == [UserMessage(content=TextContent(type="text", text="hello"))]
    assert fn_thread[0] != main_thread


@pytest.mark.anyio
async def test_render_passes_input_required_result_through_unchanged():
    """Prompt.render returns the InputRequiredResult the function returned, bypassing
    message conversion entirely (SEP-2322 multi-round-trip pass-through)."""
    sentinel = InputRequiredResult(
        input_requests={
            "who": ElicitRequest(
                params=ElicitRequestFormParams(
                    message="Who is this for?",
                    requested_schema={
                        "type": "object",
                        "properties": {"name": {"type": "string"}},
                        "required": ["name"],
                    },
                )
            )
        }
    )

    def asking_prompt() -> InputRequiredResult:
        return sentinel

    prompt = Prompt.from_function(asking_prompt)
    result = await prompt.render(None, Context())
    assert result is sentinel
