from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from app.agents.tools.reservation import create_reservation_impl
from app.models.conversation import Conversation
from app.models.customer import Customer as CustomerModel
from app.routers.chat import _get_or_create_conversation

pytestmark = pytest.mark.anyio

TOOLS_MOD = "app.agents.tools.reservation"


def _scalar_result(value: object) -> MagicMock:
    result = MagicMock()
    result.scalar_one_or_none.return_value = value
    return result


def _restaurant() -> MagicMock:
    restaurant = MagicMock()
    restaurant.timezone = "Europe/Amsterdam"
    restaurant.settings = {}
    return restaurant


class TestGuestNameNormalization:
    @patch(f"{TOOLS_MOD}.restate_proxy", new_callable=AsyncMock)
    async def test_guest_name_normalized_to_title_case(self, mock_restate, fake_ctx):
        with patch(
            f"{TOOLS_MOD}._load_restaurant",
            new_callable=AsyncMock,
            return_value=_restaurant(),
        ):
            fake_ctx.deps.session.execute.side_effect = [
                _scalar_result(None),
                _scalar_result(MagicMock(id="cust-1")),
            ]

            await create_reservation_impl(
                fake_ctx,
                "john doe",
                "john@example.com",
                None,
                "2026-06-15",
                "19:00",
                2,
            )

        payload = mock_restate.await_args.args[4]
        assert payload["guest_name"] == "John Doe"

    @patch(f"{TOOLS_MOD}.restate_proxy", new_callable=AsyncMock)
    async def test_guest_name_uppercase_normalized(self, mock_restate, fake_ctx):
        with patch(
            f"{TOOLS_MOD}._load_restaurant",
            new_callable=AsyncMock,
            return_value=_restaurant(),
        ):
            fake_ctx.deps.session.execute.side_effect = [
                _scalar_result(None),
                _scalar_result(MagicMock(id="cust-1")),
            ]

            await create_reservation_impl(
                fake_ctx,
                "JOHN DOE",
                "john@example.com",
                None,
                "2026-06-15",
                "19:00",
                2,
            )

        payload = mock_restate.await_args.args[4]
        assert payload["guest_name"] == "John Doe"

    @patch(f"{TOOLS_MOD}.restate_proxy", new_callable=AsyncMock)
    async def test_guest_name_whitespace_stripped(self, mock_restate, fake_ctx):
        with patch(
            f"{TOOLS_MOD}._load_restaurant",
            new_callable=AsyncMock,
            return_value=_restaurant(),
        ):
            fake_ctx.deps.session.execute.side_effect = [
                _scalar_result(None),
                _scalar_result(MagicMock(id="cust-1")),
            ]

            await create_reservation_impl(
                fake_ctx,
                "  john  ",
                "john@example.com",
                None,
                "2026-06-15",
                "19:00",
                2,
            )

        payload = mock_restate.await_args.args[4]
        assert payload["guest_name"] == "John"

    async def test_empty_guest_name_returns_error(self, fake_ctx):
        result = await create_reservation_impl(
            fake_ctx,
            "   ",
            "john@example.com",
            None,
            "2026-06-15",
            "19:00",
            2,
        )

        assert result == {"error": "Guest name is required"}


class TestCustomerCreationUsesRequestScopedSession:
    @patch(f"{TOOLS_MOD}.restate_proxy", new_callable=AsyncMock)
    async def test_creates_customer_with_request_scoped_session(self, mock_restate, fake_ctx):
        session = fake_ctx.deps.session

        with patch(
            f"{TOOLS_MOD}._load_restaurant",
            new_callable=AsyncMock,
            return_value=_restaurant(),
        ):
            session.execute.side_effect = [_scalar_result(None), _scalar_result(None)]

            await create_reservation_impl(
                fake_ctx,
                "new guest",
                "new@example.com",
                "+31600000000",
                "2026-06-15",
                "19:00",
                2,
            )

        session.add.assert_called_once()
        added_customer = session.add.call_args.args[0]
        assert isinstance(added_customer, CustomerModel)
        assert added_customer.name == "New Guest"
        assert added_customer.email == "new@example.com"
        assert added_customer.phone == "+31600000000"
        session.commit.assert_awaited_once()
        mock_restate.assert_awaited_once()

    @patch(f"{TOOLS_MOD}.restate_proxy", new_callable=AsyncMock)
    async def test_customer_creation_stays_in_same_session_when_restate_fails(
        self, mock_restate, fake_ctx
    ):
        session = fake_ctx.deps.session
        mock_restate.side_effect = RuntimeError("restate unavailable")

        with patch(
            f"{TOOLS_MOD}._load_restaurant",
            new_callable=AsyncMock,
            return_value=_restaurant(),
        ):
            session.execute.side_effect = [_scalar_result(None), _scalar_result(None)]

            with pytest.raises(RuntimeError, match="restate unavailable"):
                await create_reservation_impl(
                    fake_ctx,
                    "new guest",
                    "new@example.com",
                    "+31600000000",
                    "2026-06-15",
                    "19:00",
                    2,
                )

        session.add.assert_called_once()
        added_customer = session.add.call_args.args[0]
        assert isinstance(added_customer, CustomerModel)
        session.commit.assert_awaited_once()
        assert fake_ctx.deps.session is session


class TestConversationLanguagePersistence:
    async def test_new_conversation_persists_language_on_create(self, mock_session):
        conversation = await _get_or_create_conversation(
            mock_session,
            conversation_id=None,
            restaurant_id="rest-1",
            agent_type="reservation",
            channel="dashboard",
            language="nl",
        )

        mock_session.add.assert_called_once()
        added_conversation = mock_session.add.call_args.args[0]
        assert isinstance(added_conversation, Conversation)
        assert added_conversation.language == "nl"
        mock_session.commit.assert_awaited_once()
        assert conversation is added_conversation
