"""Tests for the unified restaurant agent: language detection, tool filtering."""

from __future__ import annotations

from unittest.mock import MagicMock

import pytest
from pydantic_ai import ToolDefinition

from app.agents.deps import AgentDeps, CallerIdentity
from app.agents.restaurant import (
    _TOOL_CAPABILITIES,
    _filter_tools,
    detect_language,
    restaurant_agent,
)

# ---------------------------------------------------------------------------
# Language detection
# ---------------------------------------------------------------------------


class TestDetectLanguage:
    def test_dutch_message(self):
        assert detect_language("Ik wil een tafel reserveren") == "nl"

    def test_english_message(self):
        assert detect_language("What are your opening hours?") == "en"

    def test_dutch_greeting(self):
        assert detect_language("Hallo, goedemiddag") == "nl"

    def test_english_greeting(self):
        assert detect_language("Hello, how are you?") == "en"

    def test_empty_message_defaults_nl(self):
        assert detect_language("") == "nl"

    def test_punctuation_stripped(self):
        assert detect_language("Morgen, 4 personen.") == "nl"

    def test_mixed_defaults_nl(self):
        # Equal hits → nl wins
        assert detect_language("is") == "nl"


# ---------------------------------------------------------------------------
# Tool capability mapping
# ---------------------------------------------------------------------------


class TestToolCapabilities:
    def test_all_tools_mapped(self):
        """Every tool registered on the agent has a capability mapping."""
        for tool in restaurant_agent._function_toolset.tools.values():
            assert tool.name in _TOOL_CAPABILITIES, (
                f"Tool {tool.name!r} missing from _TOOL_CAPABILITIES"
            )

    def test_faq_tools(self):
        faq_tools = {k for k, v in _TOOL_CAPABILITIES.items() if v == "faq"}
        assert faq_tools == {
            "search_knowledge_base",
            "is_open_now",
            "get_opening_hours",
            "get_restaurant_policies",
        }

    def test_reservation_tools(self):
        res_tools = {k for k, v in _TOOL_CAPABILITIES.items() if v == "reservation"}
        assert res_tools == {
            "check_availability",
            "create_reservation",
            "modify_reservation",
            "cancel_reservation",
            "find_available_slots",
            "find_reservation",
            "get_restaurant_info",
        }

    def test_takeaway_tools(self):
        tk_tools = {k for k, v in _TOOL_CAPABILITIES.items() if v == "takeaway"}
        assert tk_tools == {
            "search_menu",
            "create_order",
            "get_payment_link",
        }


# ---------------------------------------------------------------------------
# Dynamic tool filtering via prepare_tools
# ---------------------------------------------------------------------------


def _make_tool_def(name: str) -> ToolDefinition:
    return ToolDefinition(name=name, description=f"Tool {name}", parameters_json_schema={})


def _make_deps(capabilities: set[str]) -> AgentDeps:
    return AgentDeps(
        session=MagicMock(),
        restaurant_id="rest-1",
        http_client=MagicMock(),
        caller=CallerIdentity(
            channel="dashboard",
            verified=True,
            identity_key="test-user",
        ),
        enabled_capabilities=capabilities,
    )


class TestFilterTools:
    @pytest.mark.anyio
    async def test_faq_only(self):
        ctx = MagicMock()
        ctx.deps = _make_deps({"faq"})
        defs = [
            _make_tool_def("search_knowledge_base"),
            _make_tool_def("check_availability"),
            _make_tool_def("search_menu"),
        ]
        result = await _filter_tools(ctx, defs)
        assert result is not None
        names = {td.name for td in result}
        assert names == {"search_knowledge_base"}

    @pytest.mark.anyio
    async def test_faq_and_reservation(self):
        ctx = MagicMock()
        ctx.deps = _make_deps({"faq", "reservation"})
        defs = [
            _make_tool_def("search_knowledge_base"),
            _make_tool_def("check_availability"),
            _make_tool_def("create_reservation"),
            _make_tool_def("search_menu"),
        ]
        result = await _filter_tools(ctx, defs)
        assert result is not None
        names = {td.name for td in result}
        assert names == {"search_knowledge_base", "check_availability", "create_reservation"}

    @pytest.mark.anyio
    async def test_all_capabilities(self):
        ctx = MagicMock()
        ctx.deps = _make_deps({"faq", "reservation", "takeaway"})
        defs = [_make_tool_def(name) for name in _TOOL_CAPABILITIES]
        result = await _filter_tools(ctx, defs)
        assert result is not None
        # Dashboard/verified caller: verification tools are excluded
        non_verification = {n for n, c in _TOOL_CAPABILITIES.items() if c != "verification"}
        assert len(result) == len(non_verification)

    @pytest.mark.anyio
    async def test_empty_capabilities(self):
        ctx = MagicMock()
        ctx.deps = _make_deps(set())
        defs = [_make_tool_def("search_knowledge_base")]
        result = await _filter_tools(ctx, defs)
        assert result is not None
        assert len(result) == 0


# ---------------------------------------------------------------------------
# Reservation system prompt: verification block is channel/verification-aware
# ---------------------------------------------------------------------------


class TestReservationPromptVerificationBlock:
    """Regression: the prompt used to instruct the model to call
    ``send_verification_code`` unconditionally. On the dashboard the tool
    is filtered out by ``_filter_tools``, so the model would hallucinate
    "I don't have the tools to verify you" mid-reply. The prompt must
    mirror the same gate as ``_filter_tools``."""

    def _build_ctx(self, *, channel: str, verified: bool) -> MagicMock:
        deps = AgentDeps(
            session=MagicMock(),
            restaurant_id="rest-1",
            http_client=MagicMock(),
            caller=CallerIdentity(
                channel=channel,
                verified=verified,
                identity_key="anchor",
                conversation_id="conv-1",
            ),
            enabled_capabilities={"reservation", "faq"},
        )
        ctx = MagicMock()
        ctx.deps = deps
        return ctx

    @pytest.mark.anyio
    async def test_dashboard_prompt_omits_verification_instructions(self):
        from app.agents.restaurant import _reservation_prompt

        prompt = await _reservation_prompt(self._build_ctx(channel="dashboard", verified=True))
        assert prompt is not None

        assert "send_verification_code" not in prompt
        assert "verify_code" not in prompt
        assert "verification_required" not in prompt
        assert "direct access" in prompt

    @pytest.mark.anyio
    async def test_unverified_website_prompt_includes_verification_flow(self):
        from app.agents.restaurant import _reservation_prompt

        prompt = await _reservation_prompt(self._build_ctx(channel="website", verified=False))
        assert prompt is not None

        assert "send_verification_code" in prompt
        assert "verify_code" in prompt
        assert "verification_required" in prompt
        # Operator-style "direct access" wording must not leak into guest prompts.
        assert "direct access" not in prompt

    @pytest.mark.anyio
    async def test_verified_whatsapp_prompt_omits_verification_instructions(self):
        from app.agents.restaurant import _reservation_prompt

        prompt = await _reservation_prompt(self._build_ctx(channel="whatsapp", verified=True))
        assert prompt is not None

        assert "send_verification_code" not in prompt

    @pytest.mark.anyio
    async def test_unverified_website_prompt_has_explicit_post_verify_steer(self):
        """Regression: with only a vague "retry the reservation operation"
        instruction, the model would emit a generic greeting after verify_code
        and forget the original cancel/modify intent."""
        from app.agents.restaurant import _reservation_prompt

        prompt = await _reservation_prompt(self._build_ctx(channel="website", verified=False))
        assert prompt is not None

        # Explicit post-verify steer that bans a fresh greeting.
        assert "POST-VERIFY" in prompt
        assert "DO NOT greet" in prompt
        assert "ORIGINAL" in prompt or "original" in prompt


# ---------------------------------------------------------------------------
# History window must span a verification round-trip
# ---------------------------------------------------------------------------


class TestHistoryWindow:
    """Regression: the previous limit of 10 messages caused the agent to
    evict the original user intent ("I want to cancel my reservation") by
    the time it generated the post-verify reply. A verification round-trip
    routinely produces 10+ messages (user msg → tool call → tool return,
    repeated for find_reservation, send_verification_code, verify_code)."""

    def test_history_processor_limit_spans_verification_flow(self):
        import inspect

        from app.agents import restaurant as agent_module

        source = inspect.getsource(agent_module)
        # The agent constructor uses partial(keep_recent_messages, limit=N).
        # N must be large enough to keep the first user message through a
        # full verification round-trip.
        assert "keep_recent_messages, limit=30" in source, (
            "history limit must be ≥30 to survive a verification round-trip; "
            "lowering it re-introduces the post-verify context-loss bug"
        )
