from __future__ import annotations

import importlib
import os
import sys
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest import IsolatedAsyncioTestCase

_ = os.environ.setdefault("NEON_DATABASE_URL", "postgresql://user:pass@localhost:5432/testdb")

_REPO_ROOT = Path(__file__).resolve().parents[2]
_RESTATE_SERVICES_PATH = _REPO_ROOT / "restate_services"
if str(_RESTATE_SERVICES_PATH) not in sys.path:
    sys.path.insert(0, str(_RESTATE_SERVICES_PATH))

reservation_saga = importlib.import_module("reservation_saga")


class _FinalizationCtx:
    def __init__(self) -> None:
        self.calls: list[tuple[str, object, str | None, dict[str, str]]] = []

    async def object_call(
        self, handler: object, *, key: str, arg: dict[str, str]
    ) -> dict[str, str]:
        self.calls.append(("object_call", handler, key, arg))
        return {"id": key, "status": arg["status"]}

    def service_send(self, handler: object, *, arg: Any) -> None:
        self.calls.append(("service_send", handler, None, arg))


class TestReservationSagaBehaviors(IsolatedAsyncioTestCase):
    def test_booked_covers_sums_party_sizes(self) -> None:
        bookings = [
            SimpleNamespace(party_size=2),
            SimpleNamespace(party_size=4),
            SimpleNamespace(party_size=3),
        ]
        self.assertEqual(reservation_saga._booked_covers(bookings), 9)

    def test_has_capacity_accepts_when_within_remaining_covers(self) -> None:
        bookings = [
            SimpleNamespace(party_size=2),
            SimpleNamespace(party_size=3),
        ]
        self.assertTrue(
            reservation_saga._has_capacity(bookings, max_covers=6, incoming_party_size=1)
        )

    def test_has_capacity_rejects_when_new_party_exceeds_covers(self) -> None:
        bookings = [
            SimpleNamespace(party_size=2),
            SimpleNamespace(party_size=3),
        ]
        self.assertFalse(
            reservation_saga._has_capacity(bookings, max_covers=6, incoming_party_size=2)
        )

    def test_has_capacity_rejects_multi_party_even_with_few_bookings(self) -> None:
        bookings = [
            SimpleNamespace(party_size=4),
            SimpleNamespace(party_size=4),
        ]
        self.assertFalse(
            reservation_saga._has_capacity(bookings, max_covers=8, incoming_party_size=1)
        )

    def test_has_capacity_unlimited_when_max_covers_is_none(self) -> None:
        """When max_covers is None (not set), capacity is unlimited."""
        bookings = [
            SimpleNamespace(party_size=100),
            SimpleNamespace(party_size=200),
        ]
        self.assertTrue(
            reservation_saga._has_capacity(bookings, max_covers=None, incoming_party_size=50)
        )

    def test_forward_steps_have_explicit_compensation_mapping(self) -> None:
        self.assertEqual(
            reservation_saga.FORWARD_STEP_TO_COMPENSATION,
            {
                "insert_reservation": "cancel_reservation",
                "charge_deposit": "refund_deposit",
                "send_confirmation_email": "send_cancellation_email",
            },
        )

    async def test_finalize_confirms_before_scheduling_reminder(self) -> None:
        ctx = _FinalizationCtx()

        async def _update_status_handler(_ctx: object, _data: dict[str, str]) -> dict[str, str]:
            return {"status": "confirmed"}

        async def _schedule_single_reminder(_ctx: object, _reservation_id: str) -> dict[str, str]:
            return {"status": "scheduled"}

        await reservation_saga._confirm_and_schedule_reminder(
            ctx,
            "res-1",
            restaurant_id="rest-1",
            update_status_handler=_update_status_handler,
            schedule_single_reminder=_schedule_single_reminder,
        )

        self.assertEqual(ctx.calls[0][0], "object_call")
        self.assertEqual(ctx.calls[0][2], "res-1")
        self.assertEqual(ctx.calls[0][3], {"status": "confirmed", "restaurant_id": "rest-1"})
        self.assertEqual(ctx.calls[1][0], "service_send")
        self.assertEqual(ctx.calls[1][3], {"reservation_id": "res-1", "restaurant_id": "rest-1"})


class _RecordingSession:
    """Minimal AsyncSession stand-in for the customer upsert helper.

    Wires `execute(...).scalar_one_or_none()` to a configurable return value
    and records every `add(...)` + `commit()` so tests can assert what was
    persisted without standing up a real Postgres.
    """

    def __init__(self, existing: Any | None = None) -> None:
        self._existing = existing
        self.added: list[Any] = []
        self.commits = 0

    async def execute(self, _stmt: Any) -> Any:
        existing = self._existing

        class _Result:
            def scalar_one_or_none(self_inner) -> Any:  # noqa: N805
                return existing

        return _Result()

    def add(self, obj: Any) -> None:
        self.added.append(obj)

    async def commit(self) -> None:
        self.commits += 1


class TestUpsertReservationCustomer(IsolatedAsyncioTestCase):
    async def test_creates_new_customer_when_email_has_no_match(self) -> None:
        session = _RecordingSession(existing=None)
        cid = await reservation_saga._upsert_reservation_customer(
            session,
            restaurant_id="rest-1",
            guest_name="Alice",
            guest_email="alice@example.com",
            guest_phone="+3231230000",
        )
        self.assertTrue(cid)
        self.assertEqual(len(session.added), 1)
        created = session.added[0]
        self.assertEqual(created.id, cid)
        self.assertEqual(created.restaurant_id, "rest-1")
        self.assertEqual(created.name, "Alice")
        self.assertEqual(created.email, "alice@example.com")
        self.assertEqual(created.phone, "+3231230000")
        self.assertEqual(session.commits, 1)

    async def test_creates_new_customer_when_email_missing(self) -> None:
        """No email == no dedupe key; helper must skip the lookup and insert."""
        session = _RecordingSession(existing=None)
        cid = await reservation_saga._upsert_reservation_customer(
            session,
            restaurant_id="rest-1",
            guest_name="Walk-In",
            guest_email=None,
            guest_phone=None,
        )
        self.assertEqual(len(session.added), 1)
        self.assertEqual(session.added[0].id, cid)
        self.assertIsNone(session.added[0].email)

    async def test_reuses_existing_customer_and_syncs_fields(self) -> None:
        existing = SimpleNamespace(
            id="cust-existing",
            name="Old Name",
            email="alice@example.com",
            phone=None,
        )
        session = _RecordingSession(existing=existing)
        cid = await reservation_saga._upsert_reservation_customer(
            session,
            restaurant_id="rest-1",
            guest_name="Alice Updated",
            guest_email="alice@example.com",
            guest_phone="+3231230000",
        )
        self.assertEqual(cid, "cust-existing")
        self.assertEqual(existing.name, "Alice Updated")
        self.assertEqual(existing.phone, "+3231230000")
        self.assertEqual(session.commits, 1)
        # The synced object is re-added so SQLAlchemy tracks the change.
        self.assertIs(session.added[0], existing)

    async def test_reuses_existing_customer_without_writing_when_unchanged(self) -> None:
        existing = SimpleNamespace(
            id="cust-existing",
            name="Alice",
            email="alice@example.com",
            phone="+3231230000",
        )
        session = _RecordingSession(existing=existing)
        cid = await reservation_saga._upsert_reservation_customer(
            session,
            restaurant_id="rest-1",
            guest_name="Alice",
            guest_email="alice@example.com",
            guest_phone="+3231230000",
        )
        self.assertEqual(cid, "cust-existing")
        self.assertEqual(session.commits, 0)
        self.assertEqual(session.added, [])

    async def test_falls_back_to_guest_when_name_is_empty(self) -> None:
        session = _RecordingSession(existing=None)
        await reservation_saga._upsert_reservation_customer(
            session,
            restaurant_id="rest-1",
            guest_name="",
            guest_email="anon@example.com",
            guest_phone=None,
        )
        self.assertEqual(session.added[0].name, "Guest")
