"""Tests for the standalone @prompt decorator.

The @prompt decorator attaches metadata to functions without registering them
to a server. Functions can be added explicitly via server.add_prompt() or
discovered by FileSystemProvider.
"""

from typing import cast

import pytest

from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.prompts import prompt
from fastmcp.prompts.function_prompt import DecoratedPrompt, PromptMeta


class TestPromptDecorator:
    """Tests for the @prompt decorator."""

    def test_prompt_without_parens(self):
        """@prompt without parentheses should attach metadata."""

        @prompt
        def analyze(topic: str) -> str:
            return f"Analyze: {topic}"

        decorated = cast(DecoratedPrompt, analyze)
        assert callable(analyze)
        assert hasattr(analyze, "__fastmcp__")
        assert isinstance(decorated.__fastmcp__, PromptMeta)
        assert decorated.__fastmcp__.name is None  # Uses function name by default

    def test_prompt_with_empty_parens(self):
        """@prompt() with empty parentheses should attach metadata."""

        @prompt()
        def analyze(topic: str) -> str:
            return f"Analyze: {topic}"

        decorated = cast(DecoratedPrompt, analyze)
        assert callable(analyze)
        assert hasattr(analyze, "__fastmcp__")
        assert isinstance(decorated.__fastmcp__, PromptMeta)

    def test_prompt_with_name_arg(self):
        """@prompt("name") with name as first arg should work."""

        @prompt("custom-analyze")
        def analyze(topic: str) -> str:
            return f"Analyze: {topic}"

        decorated = cast(DecoratedPrompt, analyze)
        assert callable(analyze)
        assert hasattr(analyze, "__fastmcp__")
        assert decorated.__fastmcp__.name == "custom-analyze"

    def test_prompt_with_name_kwarg(self):
        """@prompt(name="name") with keyword arg should work."""

        @prompt(name="custom-analyze")
        def analyze(topic: str) -> str:
            return f"Analyze: {topic}"

        decorated = cast(DecoratedPrompt, analyze)
        assert callable(analyze)
        assert hasattr(analyze, "__fastmcp__")
        assert decorated.__fastmcp__.name == "custom-analyze"

    def test_prompt_with_all_metadata(self):
        """@prompt with all metadata should store it all."""

        @prompt(
            name="custom-analyze",
            title="Analysis Prompt",
            description="Analyzes topics",
            tags={"analysis", "demo"},
            meta={"custom": "value"},
        )
        def analyze(topic: str) -> str:
            return f"Analyze: {topic}"

        decorated = cast(DecoratedPrompt, analyze)
        assert callable(analyze)
        assert hasattr(analyze, "__fastmcp__")
        assert decorated.__fastmcp__.name == "custom-analyze"
        assert decorated.__fastmcp__.title == "Analysis Prompt"
        assert decorated.__fastmcp__.description == "Analyzes topics"
        assert decorated.__fastmcp__.tags == {"analysis", "demo"}
        assert decorated.__fastmcp__.meta == {"custom": "value"}

    async def test_prompt_function_still_callable(self):
        """Decorated function should still be directly callable."""

        @prompt
        def analyze(topic: str) -> str:
            """Analyze a topic."""
            return f"Please analyze: {topic}"

        # The function is still callable even though it has metadata
        result = cast(DecoratedPrompt, analyze)("Python")
        assert result == "Please analyze: Python"

    async def test_staticmethod_metadata_is_available_on_unwrapped_function(self):
        """@prompt should attach metadata where staticmethod access can find it."""

        class MyClass:
            @prompt(name="custom-static-prompt")
            @staticmethod
            def my_prompt() -> str:
                return "hello"

        mcp = FastMCP("Test")
        mcp.add_prompt(MyClass.my_prompt)

        async with Client(mcp) as client:
            prompts = await client.list_prompts()

        assert any(p.name == "custom-static-prompt" for p in prompts)

    def test_prompt_rejects_classmethod_decorator(self):
        """@prompt should reject classmethod-decorated functions."""
        with pytest.raises(TypeError, match="classmethod"):

            class MyClass:
                @prompt
                @classmethod
                def my_prompt(cls, topic: str) -> str:
                    return f"Analyze: {topic}"

    def test_prompt_with_both_name_args_raises(self):
        """@prompt should raise if both positional and keyword name are given."""
        with pytest.raises(TypeError, match="Cannot specify.*both.*argument.*keyword"):

            @prompt("name1", name="name2")  # type: ignore[call-overload]  # ty:ignore[invalid-argument-type]
            def my_prompt() -> str:
                return "hello"

    async def test_prompt_added_to_server(self):
        """Prompt created by @prompt should work when added to a server."""

        @prompt
        def analyze(topic: str) -> str:
            """Analyze a topic."""
            return f"Please analyze: {topic}"

        mcp = FastMCP("Test")
        mcp.add_prompt(analyze)

        async with Client(mcp) as client:
            prompts = await client.list_prompts()
            assert any(p.name == "analyze" for p in prompts)

            result = await client.get_prompt("analyze", {"topic": "FastMCP"})
            assert "FastMCP" in str(result)
