import json
import threading
from typing import Any

import pytest
from mcp_types import Annotations, ElicitRequest, ElicitRequestFormParams, InputRequiredResult
from pydantic import BaseModel

from mcp.server.mcpserver import Context, MCPServer
from mcp.server.mcpserver.exceptions import ResourceError
from mcp.server.mcpserver.resources import FunctionResource, ResourceTemplate
from mcp.server.mcpserver.resources.templates import (
    DEFAULT_RESOURCE_SECURITY,
    ResourceSecurity,
    ResourceSecurityError,
)


def _make(uri_template: str, security: ResourceSecurity = DEFAULT_RESOURCE_SECURITY) -> ResourceTemplate:
    def handler(**kwargs: Any) -> str:
        raise NotImplementedError  # these tests only exercise matches()

    return ResourceTemplate.from_function(fn=handler, uri_template=uri_template, security=security)


def test_matches_rfc6570_reserved_expansion():
    # {+path} allows / — the feature the old regex implementation couldn't support
    t = _make("file://docs/{+path}")
    assert t.matches("file://docs/src/main.py") == {"path": "src/main.py"}


def test_matches_rejects_encoded_slash_traversal():
    # %2F decodes to / in UriTemplate.match(), giving "../../etc/passwd".
    # ResourceSecurity's traversal check then rejects the '..' components.
    t = _make("file://docs/{name}")
    with pytest.raises(ResourceSecurityError, match="'name'"):
        t.matches("file://docs/..%2F..%2Fetc%2Fpasswd")


def test_matches_rejects_path_traversal_by_default():
    t = _make("file://docs/{name}")
    with pytest.raises(ResourceSecurityError):
        t.matches("file://docs/..")


def test_matches_rejects_path_traversal_in_reserved_var():
    # Even {+path} gets the traversal check — it's semantic, not structural
    t = _make("file://docs/{+path}")
    with pytest.raises(ResourceSecurityError):
        t.matches("file://docs/../../etc/passwd")


def test_matches_rejects_absolute_path():
    t = _make("file://docs/{+path}")
    with pytest.raises(ResourceSecurityError):
        t.matches("file://docs//etc/passwd")


def test_matches_allows_dotdot_as_substring():
    # .. is only dangerous as a path component
    t = _make("git://refs/{range}")
    assert t.matches("git://refs/v1.0..v2.0") == {"range": "v1.0..v2.0"}


def test_matches_exempt_params_skip_security():
    policy = ResourceSecurity(exempt_params={"range"})
    t = _make("git://diff/{+range}", security=policy)
    assert t.matches("git://diff/../foo") == {"range": "../foo"}


def test_matches_disabled_policy_allows_traversal():
    policy = ResourceSecurity(reject_path_traversal=False, reject_absolute_paths=False)
    t = _make("file://docs/{name}", security=policy)
    assert t.matches("file://docs/..") == {"name": ".."}


def test_matches_rejects_null_byte_by_default():
    # %00 decodes to \x00 which defeats string comparisons
    # ("..\x00" != "..") and can truncate in C extensions.
    t = _make("file://docs/{name}")
    with pytest.raises(ResourceSecurityError):
        t.matches("file://docs/key%00.txt")
    # Null byte also defeats the traversal check's component comparison
    with pytest.raises(ResourceSecurityError):
        t.matches("file://docs/..%00%2Fsecret")


def test_matches_null_byte_check_can_be_disabled():
    policy = ResourceSecurity(reject_null_bytes=False)
    t = _make("file://docs/{name}", security=policy)
    assert t.matches("file://docs/key%00.txt") == {"name": "key\x00.txt"}


def test_security_rejection_does_not_fall_through_to_next_template():
    # A strict template's security rejection must halt iteration, not
    # fall through to a later permissive template. Previously matches()
    # returned None for both "no match" and "security failed", making
    # registration order security-critical.
    strict = _make("file://docs/{name}")
    lax = _make(
        "file://docs/{+path}",
        security=ResourceSecurity(exempt_params={"path"}),
    )
    uri = "file://docs/..%2Fsecrets"
    # Strict matches structurally then fails security -> raises.
    with pytest.raises(ResourceSecurityError) as exc:
        strict.matches(uri)
    assert exc.value.param == "name"
    # If this raised, the resource manager never reaches the lax
    # template. Verify the lax template WOULD have accepted it.
    assert lax.matches(uri) == {"path": "../secrets"}


def test_matches_explode_checks_each_segment():
    t = _make("api{/parts*}")
    assert t.matches("api/a/b/c") == {"parts": ["a", "b", "c"]}
    # Any segment with traversal rejects the whole match
    with pytest.raises(ResourceSecurityError):
        t.matches("api/a/../c")


def test_matches_encoded_backslash_caught_by_traversal_check():
    # %5C decodes to '\\'. The traversal check normalizes '\\' to '/'
    # and catches the '..' components.
    t = _make("file://docs/{name}")
    with pytest.raises(ResourceSecurityError):
        t.matches("file://docs/..%5C..%5Csecret")


def test_matches_encoded_dots_caught_by_traversal_check():
    # %2E%2E decodes to '..' which the traversal check rejects.
    t = _make("file://docs/{name}")
    with pytest.raises(ResourceSecurityError):
        t.matches("file://docs/%2E%2E")


def test_matches_mixed_encoded_and_literal_slash():
    # The literal '/' stops the simple-var regex, so the URI doesn't
    # match the template at all.
    t = _make("file://docs/{name}")
    assert t.matches("file://docs/..%2F../etc") is None


def test_matches_encoded_slash_without_traversal_allowed():
    # %2F decoding to '/' is fine when there's no traversal involved.
    # UriTemplate accepts it; ResourceSecurity only blocks '..' and
    # absolute paths. Handlers that need single-segment should use
    # safe_join or validate explicitly.
    t = _make("file://docs/{name}")
    assert t.matches("file://docs/sub%2Ffile.txt") == {"name": "sub/file.txt"}


def test_matches_escapes_template_literals():
    # Regression: old impl treated . as regex wildcard
    t = _make("data://v1.0/{id}")
    assert t.matches("data://v1.0/42") == {"id": "42"}
    assert t.matches("data://v1X0/42") is None


class TestResourceTemplate:
    """Test ResourceTemplate functionality."""

    def test_template_creation(self):
        """Test creating a template from a function."""

        def my_func(key: str, value: int) -> dict[str, Any]:
            return {"key": key, "value": value}

        template = ResourceTemplate.from_function(
            fn=my_func,
            uri_template="test://{key}/{value}",
            name="test",
        )
        assert template.uri_template == "test://{key}/{value}"
        assert template.name == "test"
        assert template.mime_type == "text/plain"  # default
        assert template.fn(key="test", value=42) == my_func(key="test", value=42)

    def test_template_matches(self):
        """Test matching URIs against a template."""

        def my_func(key: str, value: int) -> dict[str, Any]:  # pragma: no cover
            return {"key": key, "value": value}

        template = ResourceTemplate.from_function(
            fn=my_func,
            uri_template="test://{key}/{value}",
            name="test",
        )

        # Valid match
        params = template.matches("test://foo/123")
        assert params == {"key": "foo", "value": "123"}

        # No match
        assert template.matches("test://foo") is None
        assert template.matches("other://foo/123") is None

    @pytest.mark.anyio
    async def test_create_resource(self):
        """Test creating a resource from a template."""

        def my_func(key: str, value: int) -> dict[str, Any]:
            return {"key": key, "value": value}

        template = ResourceTemplate.from_function(
            fn=my_func,
            uri_template="test://{key}/{value}",
            name="test",
        )

        resource = await template.create_resource(
            "test://foo/123",
            {"key": "foo", "value": 123},
            Context(),
        )

        assert isinstance(resource, FunctionResource)
        content = await resource.read()
        assert isinstance(content, str)
        data = json.loads(content)
        assert data == {"key": "foo", "value": 123}

    @pytest.mark.anyio
    async def test_template_error(self):
        """Test error handling in template resource creation."""

        def failing_func(x: str) -> str:
            raise ValueError("Test error")

        template = ResourceTemplate.from_function(
            fn=failing_func,
            uri_template="fail://{x}",
            name="fail",
        )

        with pytest.raises(ResourceError, match="Error creating resource from template"):
            await template.create_resource("fail://test", {"x": "test"}, Context())

    @pytest.mark.anyio
    async def test_async_text_resource(self):
        """Test creating a text resource from async function."""

        async def greet(name: str) -> str:
            return f"Hello, {name}!"

        template = ResourceTemplate.from_function(
            fn=greet,
            uri_template="greet://{name}",
            name="greeter",
        )

        resource = await template.create_resource(
            "greet://world",
            {"name": "world"},
            Context(),
        )

        assert isinstance(resource, FunctionResource)
        content = await resource.read()
        assert content == "Hello, world!"

    @pytest.mark.anyio
    async def test_async_binary_resource(self):
        """Test creating a binary resource from async function."""

        async def get_bytes(value: str) -> bytes:
            return value.encode()

        template = ResourceTemplate.from_function(
            fn=get_bytes,
            uri_template="bytes://{value}",
            name="bytes",
        )

        resource = await template.create_resource(
            "bytes://test",
            {"value": "test"},
            Context(),
        )

        assert isinstance(resource, FunctionResource)
        content = await resource.read()
        assert content == b"test"

    @pytest.mark.anyio
    async def test_basemodel_conversion(self):
        """Test handling of BaseModel types."""

        class MyModel(BaseModel):
            key: str
            value: int

        def get_data(key: str, value: int) -> MyModel:
            return MyModel(key=key, value=value)

        template = ResourceTemplate.from_function(
            fn=get_data,
            uri_template="test://{key}/{value}",
            name="test",
        )

        resource = await template.create_resource(
            "test://foo/123",
            {"key": "foo", "value": 123},
            Context(),
        )

        assert isinstance(resource, FunctionResource)
        content = await resource.read()
        assert isinstance(content, str)
        data = json.loads(content)
        assert data == {"key": "foo", "value": 123}

    @pytest.mark.anyio
    async def test_custom_type_conversion(self):
        """Test handling of custom types."""

        class CustomData:
            def __init__(self, value: str):
                self.value = value

            def __str__(self) -> str:
                return self.value

        def get_data(value: str) -> CustomData:
            return CustomData(value)

        template = ResourceTemplate.from_function(
            fn=get_data,
            uri_template="test://{value}",
            name="test",
        )

        resource = await template.create_resource(
            "test://hello",
            {"value": "hello"},
            Context(),
        )

        assert isinstance(resource, FunctionResource)
        content = await resource.read()
        assert content == '"hello"'


class TestResourceTemplateAnnotations:
    """Test annotations on resource templates."""

    def test_template_with_annotations(self):
        """Test creating a template with annotations."""

        def get_user_data(user_id: str) -> str:  # pragma: no cover
            return f"User {user_id}"

        annotations = Annotations(priority=0.9)

        template = ResourceTemplate.from_function(
            fn=get_user_data, uri_template="resource://users/{user_id}", annotations=annotations
        )

        assert template.annotations is not None
        assert template.annotations.priority == 0.9

    def test_template_without_annotations(self):
        """Test that annotations are optional for templates."""

        def get_user_data(user_id: str) -> str:  # pragma: no cover
            return f"User {user_id}"

        template = ResourceTemplate.from_function(fn=get_user_data, uri_template="resource://users/{user_id}")

        assert template.annotations is None

    @pytest.mark.anyio
    async def test_template_annotations_in_mcpserver(self):
        """Test template annotations via an MCPServer decorator."""

        mcp = MCPServer()

        @mcp.resource("resource://dynamic/{id}", annotations=Annotations(audience=["user"], priority=0.7))
        def get_dynamic(id: str) -> str:  # pragma: no cover
            """A dynamic annotated resource."""
            return f"Data for {id}"

        templates = await mcp.list_resource_templates()
        assert len(templates) == 1
        assert templates[0].annotations is not None
        assert templates[0].annotations.audience == ["user"]
        assert templates[0].annotations.priority == 0.7

    @pytest.mark.anyio
    async def test_template_created_resources_inherit_annotations(self):
        """Test that resources created from templates inherit annotations."""

        def get_item(item_id: str) -> str:
            return f"Item {item_id}"

        annotations = Annotations(priority=0.6)

        template = ResourceTemplate.from_function(
            fn=get_item, uri_template="resource://items/{item_id}", annotations=annotations
        )

        # Create a resource from the template
        resource = await template.create_resource("resource://items/123", {"item_id": "123"}, Context())
        assert not isinstance(resource, InputRequiredResult)

        # The resource should inherit the template's annotations
        assert resource.annotations is not None
        assert resource.annotations.priority == 0.6

        # Verify the resource works correctly
        content = await resource.read()
        assert content == "Item 123"


class TestResourceTemplateMetadata:
    """Test ResourceTemplate meta handling."""

    def test_template_from_function_with_metadata(self):
        """Test that ResourceTemplate.from_function() accepts and stores meta parameter."""

        def get_user(user_id: str) -> str:  # pragma: no cover
            return f"User {user_id}"

        metadata = {"requires_auth": True, "rate_limit": 100}

        template = ResourceTemplate.from_function(
            fn=get_user,
            uri_template="resource://users/{user_id}",
            meta=metadata,
        )

        assert template.meta is not None
        assert template.meta == metadata
        assert template.meta["requires_auth"] is True
        assert template.meta["rate_limit"] == 100

    @pytest.mark.anyio
    async def test_template_created_resources_inherit_metadata(self):
        """Test that resources created from templates inherit meta from template."""

        def get_item(item_id: str) -> str:
            return f"Item {item_id}"

        metadata = {"category": "inventory", "cacheable": True}

        template = ResourceTemplate.from_function(
            fn=get_item,
            uri_template="resource://items/{item_id}",
            meta=metadata,
        )

        # Create a resource from the template
        resource = await template.create_resource("resource://items/123", {"item_id": "123"}, Context())

        # The resource should inherit the template's metadata
        assert resource.meta is not None
        assert resource.meta == metadata
        assert resource.meta["category"] == "inventory"
        assert resource.meta["cacheable"] is True


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

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

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

    template = ResourceTemplate.from_function(fn=blocking_fn, uri_template="test://{name}")
    resource = await template.create_resource("test://world", {"name": "world"}, Context())

    assert isinstance(resource, FunctionResource)
    assert await resource.read() == "hello world"
    assert fn_thread[0] != main_thread


@pytest.mark.anyio
async def test_create_resource_passes_input_required_result_through_unchanged():
    """create_resource returns the InputRequiredResult the template function returned
    instead of wrapping it in a FunctionResource (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 ask(topic: str) -> InputRequiredResult:
        return sentinel

    template = ResourceTemplate.from_function(fn=ask, uri_template="ask://{topic}")
    result = await template.create_resource("ask://databases", {"topic": "databases"}, Context())
    assert result is sentinel
