"""Tests for the FormInput provider."""

import json

import pydantic
import pytest

from fastmcp import FastMCP
from fastmcp.apps.form import (
    _FORM_SUPPORTS_DEFAULTS,
    FormInput,
    _backfill_boolean_defaults,
)
from fastmcp.server.providers.addressing import hashed_backend_name


class Contact(pydantic.BaseModel):
    name: str
    email: str
    phone: str | None = None


class NoteForm(pydantic.BaseModel):
    title: str
    content: str
    archived: bool = False


class TestFormInputProvider:
    async def test_collect_returns_structured_content(self):
        server = FastMCP("test", providers=[FormInput(model=Contact)])

        result = await server.call_tool(
            "collect_contact",
            {"prompt": "Enter your details"},
        )
        assert result.structured_content is not None

    async def test_tool_name_derived_from_model(self):
        server = FastMCP("test", providers=[FormInput(model=Contact)])

        tools = await server.list_tools()
        tool_names = [t.name for t in tools]
        assert "collect_contact" in tool_names

    async def test_custom_tool_name(self):
        server = FastMCP(
            "test",
            providers=[FormInput(model=Contact, tool_name="new_contact")],
        )

        tools = await server.list_tools()
        tool_names = [t.name for t in tools]
        assert "new_contact" in tool_names

    async def test_submit_validates_and_returns_json(self):
        server = FastMCP("test", providers=[FormInput(model=Contact)])

        result = await server.call_tool(
            hashed_backend_name("Contact", "submit_form"),
            {"data": {"name": "Alice", "email": "alice@example.com"}},
        )
        text = result.content[0].text  # type: ignore[union-attr]  # ty:ignore[unresolved-attribute]
        parsed = json.loads(text)
        assert parsed["name"] == "Alice"
        assert parsed["email"] == "alice@example.com"
        assert parsed["phone"] is None

    async def test_submit_with_callback(self):
        saved: list[Contact] = []

        def on_submit(contact: Contact) -> str:
            saved.append(contact)
            return f"Saved {contact.name}"

        server = FastMCP(
            "test",
            providers=[FormInput(model=Contact, on_submit=on_submit)],
        )

        result = await server.call_tool(
            hashed_backend_name("Contact", "submit_form"),
            {"data": {"name": "Bob", "email": "bob@example.com"}},
        )
        text = result.content[0].text  # type: ignore[union-attr]  # ty:ignore[unresolved-attribute]
        assert "Saved Bob" in text
        assert len(saved) == 1
        assert saved[0].name == "Bob"

    async def test_backend_tool_hidden(self):
        server = FastMCP("test", providers=[FormInput(model=Contact)])

        tools = await server.list_tools()
        tool_names = [t.name for t in tools]
        assert "_submit_form" not in tool_names

    async def test_submit_boolean_false_omitted(self):
        """Unchecked checkboxes omit the field; submit_form should still succeed."""
        server = FastMCP("test", providers=[FormInput(model=NoteForm)])

        result = await server.call_tool(
            hashed_backend_name("NoteForm", "submit_form"),
            {"data": {"title": "My Note", "content": "Hello"}},
        )
        text = result.content[0].text  # type: ignore[union-attr]  # ty:ignore[unresolved-attribute]
        parsed = json.loads(text)
        assert parsed["title"] == "My Note"
        assert parsed["archived"] is False

    async def test_submit_boolean_true_preserved(self):
        """When a boolean field is explicitly True, it should be preserved."""
        server = FastMCP("test", providers=[FormInput(model=NoteForm)])

        result = await server.call_tool(
            hashed_backend_name("NoteForm", "submit_form"),
            {"data": {"title": "My Note", "content": "Hello", "archived": True}},
        )
        text = result.content[0].text  # type: ignore[union-attr]  # ty:ignore[unresolved-attribute]
        parsed = json.loads(text)
        assert parsed["archived"] is True

    async def test_submit_no_data_does_not_crash(self):
        """When data is omitted entirely, the tool should not raise a missing argument error."""
        server = FastMCP("test", providers=[FormInput(model=NoteForm)])

        # Should reach model validation (not crash with "missing required argument"
        # for the data parameter itself). Pydantic will still reject missing
        # required fields like title/content, but that's expected.
        with pytest.raises(pydantic.ValidationError, match="title"):
            await server.call_tool(hashed_backend_name("NoteForm", "submit_form"), {})

    async def test_multiple_models(self):
        class Address(pydantic.BaseModel):
            street: str
            city: str

        server = FastMCP(
            "test",
            providers=[
                FormInput(model=Contact),
                FormInput(model=Address),
            ],
        )

        tools = await server.list_tools()
        tool_names = [t.name for t in tools]
        assert "collect_contact" in tool_names
        assert "collect_address" in tool_names


class TestCollectInputDefault:
    """The `default` arg pre-fills the form for the user to confirm/edit."""

    @pytest.mark.skipif(
        not _FORM_SUPPORTS_DEFAULTS,
        reason="prefab-ui<0.19.1 does not support Form.from_model(defaults=...)",
    )
    async def test_default_prefills_form_fields(self):
        server = FastMCP("test", providers=[FormInput(model=Contact)])

        result = await server.call_tool(
            "collect_contact",
            {
                "prompt": "Confirm your details",
                "default": {"name": "Alice", "email": "alice@example.com"},
            },
        )
        # The form view should serialize with the prefilled values.
        payload = json.dumps(result.structured_content)
        assert "Alice" in payload
        assert "alice@example.com" in payload

    async def test_default_absent_still_renders_blank_form(self):
        """Sanity: omitting `default` keeps the existing blank-form behavior."""
        server = FastMCP("test", providers=[FormInput(model=Contact)])

        result = await server.call_tool(
            "collect_contact",
            {"prompt": "Enter your details"},
        )
        assert result.structured_content is not None

    async def test_default_silent_on_older_prefab(self):
        """When the sniff says False, `default` is accepted but no-ops.

        This test's real value is that `default` is part of the tool
        signature regardless of prefab version, so agents can always pass
        it without runtime errors.
        """
        server = FastMCP("test", providers=[FormInput(model=Contact)])

        result = await server.call_tool(
            "collect_contact",
            {
                "prompt": "Confirm your details",
                "default": {"name": "Bob"},
            },
        )
        assert result.structured_content is not None


class TestBackfillBooleanDefaults:
    def test_missing_bool_with_default_gets_backfilled(self):
        data = {"title": "Note", "content": "Body"}
        result = _backfill_boolean_defaults(NoteForm, data)
        assert result["archived"] is False

    def test_present_bool_not_overwritten(self):
        data = {"title": "Note", "content": "Body", "archived": True}
        result = _backfill_boolean_defaults(NoteForm, data)
        assert result["archived"] is True

    def test_required_bool_without_default_gets_false(self):
        class FormWithRequiredBool(pydantic.BaseModel):
            name: str
            active: bool

        data = {"name": "Test"}
        result = _backfill_boolean_defaults(FormWithRequiredBool, data)
        assert result["active"] is False

    def test_non_bool_fields_untouched(self):
        data = {"title": "Note"}
        result = _backfill_boolean_defaults(NoteForm, data)
        assert "content" not in result
