"""Tests for prompt behavior in LocalProvider.

Tests cover:
- Prompt context injection
- Prompt decorator patterns
"""

import pytest
from mcp.types import TextContent

from fastmcp import Client, Context, FastMCP
from fastmcp.prompts.base import Prompt, PromptResult


class TestPromptContext:
    async def test_prompt_context(self):
        mcp = FastMCP()

        @mcp.prompt
        def prompt_fn(name: str, ctx: Context) -> str:
            assert isinstance(ctx, Context)
            return f"Hello, {name}! {ctx.request_id}"

        async with Client(mcp) as client:
            result = await client.get_prompt("prompt_fn", {"name": "World"})
            assert len(result.messages) == 1
            message = result.messages[0]
            assert message.role == "user"

    async def test_prompt_context_with_callable_object(self):
        mcp = FastMCP()

        class MyPrompt:
            def __call__(self, name: str, ctx: Context) -> str:
                return f"Hello, {name}! {ctx.request_id}"

        mcp.add_prompt(Prompt.from_function(MyPrompt(), name="my_prompt"))

        async with Client(mcp) as client:
            result = await client.get_prompt("my_prompt", {"name": "World"})
            assert len(result.messages) == 1
            message = result.messages[0]
            assert message.role == "user"
            assert isinstance(message.content, TextContent)
            assert message.content.text == "Hello, World! 1"


class TestPromptDecorator:
    async def test_prompt_decorator(self):
        mcp = FastMCP()

        @mcp.prompt
        def fn() -> str:
            return "Hello, world!"

        prompts = await mcp.list_prompts()
        assert len(prompts) == 1
        prompt = next(p for p in prompts if p.name == "fn")
        assert prompt.name == "fn"
        content = await prompt.render()
        assert isinstance(content, PromptResult)
        assert isinstance(content.messages[0].content, TextContent)
        assert content.messages[0].content.text == "Hello, world!"

    async def test_prompt_decorator_without_parentheses(self):
        mcp = FastMCP()

        @mcp.prompt
        def fn() -> str:
            return "Hello, world!"

        prompts = await mcp.list_prompts()
        assert any(p.name == "fn" for p in prompts)

        result = await mcp.render_prompt("fn")
        assert len(result.messages) == 1
        assert isinstance(result.messages[0].content, TextContent)
        assert result.messages[0].content.text == "Hello, world!"

    async def test_prompt_decorator_with_name(self):
        mcp = FastMCP()

        @mcp.prompt(name="custom_name")
        def fn() -> str:
            return "Hello, world!"

        prompts_list = await mcp.list_prompts()
        assert len(prompts_list) == 1
        prompt = next(p for p in prompts_list if p.name == "custom_name")
        assert prompt.name == "custom_name"
        content = await prompt.render()
        assert isinstance(content, PromptResult)
        assert isinstance(content.messages[0].content, TextContent)
        assert content.messages[0].content.text == "Hello, world!"

    async def test_prompt_decorator_with_description(self):
        mcp = FastMCP()

        @mcp.prompt(description="A custom description")
        def fn() -> str:
            return "Hello, world!"

        prompts_list = await mcp.list_prompts()
        assert len(prompts_list) == 1
        prompt = next(p for p in prompts_list if p.name == "fn")
        assert prompt.description == "A custom description"
        content = await prompt.render()
        assert isinstance(content, PromptResult)
        assert isinstance(content.messages[0].content, TextContent)
        assert content.messages[0].content.text == "Hello, world!"

    async def test_prompt_decorator_with_parameters(self):
        mcp = FastMCP()

        @mcp.prompt
        def test_prompt(name: str, greeting: str = "Hello") -> str:
            return f"{greeting}, {name}!"

        prompts = await mcp.list_prompts()
        assert len(prompts) == 1
        prompt = next(p for p in prompts if p.name == "test_prompt")
        assert prompt.arguments is not None
        assert len(prompt.arguments) == 2
        assert prompt.arguments[0].name == "name"
        assert prompt.arguments[0].required is True
        assert prompt.arguments[1].name == "greeting"
        assert prompt.arguments[1].required is False

        result = await mcp.render_prompt("test_prompt", {"name": "World"})
        assert len(result.messages) == 1
        message = result.messages[0]
        assert isinstance(message.content, TextContent)
        assert message.content.text == "Hello, World!"

        result = await mcp.render_prompt(
            "test_prompt", {"name": "World", "greeting": "Hi"}
        )
        assert len(result.messages) == 1
        message = result.messages[0]
        assert isinstance(message.content, TextContent)
        assert message.content.text == "Hi, World!"

    async def test_prompt_decorator_instance_method(self):
        mcp = FastMCP()

        class MyClass:
            def __init__(self, prefix: str):
                self.prefix = prefix

            def test_prompt(self) -> str:
                return f"{self.prefix} Hello, world!"

        obj = MyClass("My prefix:")
        mcp.add_prompt(Prompt.from_function(obj.test_prompt, name="test_prompt"))

        result = await mcp.render_prompt("test_prompt")
        assert len(result.messages) == 1
        message = result.messages[0]
        assert isinstance(message.content, TextContent)
        assert message.content.text == "My prefix: Hello, world!"

    async def test_prompt_decorator_classmethod(self):
        mcp = FastMCP()

        class MyClass:
            prefix = "Class prefix:"

            @classmethod
            def test_prompt(cls) -> str:
                return f"{cls.prefix} Hello, world!"

        mcp.add_prompt(Prompt.from_function(MyClass.test_prompt, name="test_prompt"))

        result = await mcp.render_prompt("test_prompt")
        assert len(result.messages) == 1
        message = result.messages[0]
        assert isinstance(message.content, TextContent)
        assert message.content.text == "Class prefix: Hello, world!"

    async def test_prompt_decorator_classmethod_error(self):
        mcp = FastMCP()

        with pytest.raises(TypeError, match="classmethod"):

            class MyClass:
                @mcp.prompt
                @classmethod
                def test_prompt(cls) -> None:
                    pass

    async def test_prompt_decorator_staticmethod(self):
        mcp = FastMCP()

        class MyClass:
            @mcp.prompt
            @staticmethod
            def test_prompt() -> str:
                return "Static Hello, world!"

        result = await mcp.render_prompt("test_prompt")
        assert len(result.messages) == 1
        message = result.messages[0]
        assert isinstance(message.content, TextContent)
        assert message.content.text == "Static Hello, world!"

    async def test_prompt_decorator_async_function(self):
        mcp = FastMCP()

        @mcp.prompt
        async def test_prompt() -> str:
            return "Async Hello, world!"

        result = await mcp.render_prompt("test_prompt")
        assert len(result.messages) == 1
        message = result.messages[0]
        assert isinstance(message.content, TextContent)
        assert message.content.text == "Async Hello, world!"

    async def test_prompt_decorator_with_tags(self):
        """Test that the prompt decorator properly sets tags."""
        mcp = FastMCP()

        @mcp.prompt(tags={"example", "test-tag"})
        def sample_prompt() -> str:
            return "Hello, world!"

        prompts = await mcp.list_prompts()
        assert len(prompts) == 1
        prompt = next(p for p in prompts if p.name == "sample_prompt")
        assert prompt.tags == {"example", "test-tag"}

    async def test_prompt_decorator_with_string_name(self):
        """Test that @prompt(\"custom_name\") syntax works correctly."""
        mcp = FastMCP()

        @mcp.prompt("string_named_prompt")
        def my_function() -> str:
            """A function with a string name."""
            return "Hello from string named prompt!"

        prompts = await mcp.list_prompts()
        assert any(p.name == "string_named_prompt" for p in prompts)
        assert not any(p.name == "my_function" for p in prompts)

        result = await mcp.render_prompt("string_named_prompt")
        assert len(result.messages) == 1
        assert isinstance(result.messages[0].content, TextContent)
        assert result.messages[0].content.text == "Hello from string named prompt!"

    async def test_prompt_direct_function_call(self):
        """Test that prompts can be registered via direct function call."""
        from typing import cast

        from fastmcp.prompts.function_prompt import DecoratedPrompt

        mcp = FastMCP()

        def standalone_function() -> str:
            """A standalone function to be registered."""
            return "Hello from direct call!"

        result_fn = mcp.prompt(standalone_function, name="direct_call_prompt")

        # In new decorator mode, returns the function with metadata
        decorated = cast(DecoratedPrompt, result_fn)
        assert hasattr(result_fn, "__fastmcp__")
        assert decorated.__fastmcp__.name == "direct_call_prompt"
        assert result_fn is standalone_function

        prompts = await mcp.list_prompts()
        prompt = next(p for p in prompts if p.name == "direct_call_prompt")
        # Prompt is registered separately, not same object as decorated function
        assert prompt.name == "direct_call_prompt"

        result = await mcp.render_prompt("direct_call_prompt")
        assert len(result.messages) == 1
        assert isinstance(result.messages[0].content, TextContent)
        assert result.messages[0].content.text == "Hello from direct call!"

    async def test_prompt_decorator_conflicting_names_error(self):
        """Test that providing both positional and keyword names raises an error."""
        mcp = FastMCP()

        with pytest.raises(
            TypeError,
            match="Cannot specify both a name as first argument and as keyword argument",
        ):

            @mcp.prompt("positional_name", name="keyword_name")
            def my_function() -> str:
                return "Hello, world!"

    async def test_prompt_decorator_staticmethod_order(self):
        """Test that both decorator orders work for static methods"""
        mcp = FastMCP()

        class MyClass:
            @mcp.prompt
            @staticmethod
            def test_prompt() -> str:
                return "Static Hello, world!"

        result = await mcp.render_prompt("test_prompt")
        assert len(result.messages) == 1
        message = result.messages[0]
        assert isinstance(message.content, TextContent)
        assert message.content.text == "Static Hello, world!"

    async def test_prompt_decorator_with_meta(self):
        """Test that meta parameter is passed through the prompt decorator."""
        mcp = FastMCP()

        meta_data = {"version": "3.0", "type": "prompt"}

        @mcp.prompt(meta=meta_data)
        def test_prompt(message: str) -> str:
            return f"Response: {message}"

        prompts = await mcp.list_prompts()
        prompt = next(p for p in prompts if p.name == "test_prompt")

        assert prompt.meta == meta_data


class TestPromptEnabled:
    async def test_toggle_enabled(self):
        mcp = FastMCP()

        @mcp.prompt
        def sample_prompt() -> str:
            return "Hello, world!"

        prompts = await mcp.list_prompts()
        assert any(p.name == "sample_prompt" for p in prompts)

        mcp.disable(names={"sample_prompt"}, components={"prompt"})

        prompts = await mcp.list_prompts()
        assert not any(p.name == "sample_prompt" for p in prompts)

        mcp.enable(names={"sample_prompt"}, components={"prompt"})

        prompts = await mcp.list_prompts()
        assert any(p.name == "sample_prompt" for p in prompts)

    async def test_prompt_disabled(self):
        mcp = FastMCP()

        @mcp.prompt
        def sample_prompt() -> str:
            return "Hello, world!"

        mcp.disable(names={"sample_prompt"}, components={"prompt"})
        prompts = await mcp.list_prompts()
        assert len(prompts) == 0

    async def test_prompt_toggle_enabled(self):
        mcp = FastMCP()

        @mcp.prompt
        def sample_prompt() -> str:
            return "Hello, world!"

        mcp.disable(names={"sample_prompt"}, components={"prompt"})
        prompts = await mcp.list_prompts()
        assert not any(p.name == "sample_prompt" for p in prompts)

        mcp.enable(names={"sample_prompt"}, components={"prompt"})
        prompts = await mcp.list_prompts()
        assert len(prompts) == 1

    async def test_prompt_toggle_disabled(self):
        mcp = FastMCP()

        @mcp.prompt
        def sample_prompt() -> str:
            return "Hello, world!"

        mcp.disable(names={"sample_prompt"}, components={"prompt"})
        prompts = await mcp.list_prompts()
        assert len(prompts) == 0

        # get_prompt() applies enabled transform, returns None for disabled
        prompt = await mcp.get_prompt("sample_prompt")
        assert prompt is None

    async def test_get_prompt_and_disable(self):
        mcp = FastMCP()

        @mcp.prompt
        def sample_prompt() -> str:
            return "Hello, world!"

        prompt = await mcp.get_prompt("sample_prompt")
        assert prompt is not None

        mcp.disable(names={"sample_prompt"}, components={"prompt"})
        prompts = await mcp.list_prompts()
        assert len(prompts) == 0

        # get_prompt() applies enabled transform, returns None for disabled
        prompt = await mcp.get_prompt("sample_prompt")
        assert prompt is None

    async def test_cant_get_disabled_prompt(self):
        mcp = FastMCP()

        @mcp.prompt
        def sample_prompt() -> str:
            return "Hello, world!"

        mcp.disable(names={"sample_prompt"}, components={"prompt"})

        # get_prompt() applies enabled transform, returns None for disabled
        prompt = await mcp.get_prompt("sample_prompt")
        assert prompt is None


class TestPromptTags:
    def create_server(self, include_tags=None, exclude_tags=None):
        mcp = FastMCP()

        @mcp.prompt(tags={"a", "b"})
        def prompt_1() -> str:
            return "1"

        @mcp.prompt(tags={"b", "c"})
        def prompt_2() -> str:
            return "2"

        if include_tags:
            mcp.enable(tags=include_tags, only=True)
        if exclude_tags:
            mcp.disable(tags=exclude_tags)

        return mcp

    async def test_include_tags_all_prompts(self):
        mcp = self.create_server(include_tags={"a", "b"})
        prompts = await mcp.list_prompts()
        assert {p.name for p in prompts} == {"prompt_1", "prompt_2"}

    async def test_include_tags_some_prompts(self):
        mcp = self.create_server(include_tags={"a"})
        prompts = await mcp.list_prompts()
        assert {p.name for p in prompts} == {"prompt_1"}

    async def test_exclude_tags_all_prompts(self):
        mcp = self.create_server(exclude_tags={"a", "b"})
        prompts = await mcp.list_prompts()
        assert {p.name for p in prompts} == set()

    async def test_exclude_tags_some_prompts(self):
        mcp = self.create_server(exclude_tags={"a"})
        prompts = await mcp.list_prompts()
        assert {p.name for p in prompts} == {"prompt_2"}

    async def test_exclude_takes_precedence_over_include(self):
        mcp = self.create_server(exclude_tags={"a"}, include_tags={"b"})
        prompts = await mcp.list_prompts()
        assert {p.name for p in prompts} == {"prompt_2"}

    async def test_read_prompt_includes_tags(self):
        mcp = self.create_server(include_tags={"a"})
        # _get_prompt applies enabled transform (tag filtering)
        prompt = await mcp._get_prompt("prompt_1")
        result = await prompt.render({})
        assert result.messages[0].content.text == "1"

        prompt = await mcp.get_prompt("prompt_2")
        assert prompt is None

    async def test_read_prompt_excludes_tags(self):
        mcp = self.create_server(exclude_tags={"a"})
        # get_prompt applies enabled transform (tag filtering)
        prompt = await mcp.get_prompt("prompt_1")
        assert prompt is None

        prompt = await mcp.get_prompt("prompt_2")
        result = await prompt.render({})
        assert result.messages[0].content.text == "2"
