"""Unit tests for ``app.utils.tz``.

These pin the conversion contract every reservation write path relies
on: naive ISO is restaurant-local, aware ISO is UTC, stored values are
naive-UTC, and JSON output carries an explicit ``Z`` suffix so the
frontend cannot mis-parse it as browser-local time.
"""

from __future__ import annotations

import json
from datetime import UTC, date, datetime
from typing import Any
from unittest import TestCase

from app.models.reservation import ReservationRead
from app.utils.tz import (
    DEFAULT_TZ,
    local_day_window,
    naive_utc_to_local,
    parse_local_iso,
    resolve_tz,
    to_naive_utc,
)


class TestResolveTz(TestCase):
    def test_known_zone_returned_intact(self) -> None:
        tz = resolve_tz("Europe/Amsterdam")
        self.assertEqual(str(tz), "Europe/Amsterdam")

    def test_blank_falls_back_to_default(self) -> None:
        for value in (None, "", "   "):
            with self.subTest(value=value):
                self.assertEqual(str(resolve_tz(value)), DEFAULT_TZ)

    def test_unknown_zone_falls_back_to_default(self) -> None:
        self.assertEqual(str(resolve_tz("Not/A/Zone")), DEFAULT_TZ)


class TestToNaiveUtc(TestCase):
    def test_naive_treated_as_restaurant_local(self) -> None:
        # 09:30 Brussels in May (DST → UTC+2) should land at 07:30 UTC.
        tz = resolve_tz("Europe/Brussels")
        got = to_naive_utc(datetime(2026, 5, 27, 9, 30), tz)
        self.assertEqual(got, datetime(2026, 5, 27, 7, 30))
        self.assertIsNone(got.tzinfo)

    def test_naive_in_winter_uses_winter_offset(self) -> None:
        # 09:30 Brussels in January (CET → UTC+1) should land at 08:30 UTC.
        tz = resolve_tz("Europe/Brussels")
        got = to_naive_utc(datetime(2026, 1, 15, 9, 30), tz)
        self.assertEqual(got, datetime(2026, 1, 15, 8, 30))

    def test_aware_value_converted_to_utc_then_stripped(self) -> None:
        tz = resolve_tz("Europe/Brussels")
        aware = datetime(2026, 5, 27, 7, 30, tzinfo=UTC)
        got = to_naive_utc(aware, tz)
        self.assertEqual(got, datetime(2026, 5, 27, 7, 30))
        self.assertIsNone(got.tzinfo)


class TestParseLocalIso(TestCase):
    def test_naive_iso_is_restaurant_local(self) -> None:
        # Mirrors the public widget payload: ``${date}T${time}:00``.
        tz = resolve_tz("Europe/Brussels")
        got = parse_local_iso("2026-05-27T19:00:00", tz)
        self.assertEqual(got, datetime(2026, 5, 27, 17, 0))

    def test_aware_iso_is_utc(self) -> None:
        # Mirrors the agent path payload after ``parse_restaurant_local_datetime``.
        tz = resolve_tz("Europe/Brussels")
        got = parse_local_iso("2026-05-27T17:00:00+00:00", tz)
        self.assertEqual(got, datetime(2026, 5, 27, 17, 0))


class TestNaiveUtcToLocal(TestCase):
    def test_round_trip_via_local(self) -> None:
        tz = resolve_tz("Europe/Brussels")
        stored = datetime(2026, 5, 27, 7, 30)
        self.assertEqual(naive_utc_to_local(stored, tz), datetime(2026, 5, 27, 9, 30))


class TestLocalDayWindow(TestCase):
    def test_brussels_dst_day_is_naive_utc_22_to_22(self) -> None:
        # A summer-DST day in Brussels: local midnight is 22:00 UTC the
        # previous day; next local midnight is 22:00 UTC same day.
        tz = resolve_tz("Europe/Brussels")
        start, end = local_day_window(date(2026, 5, 27), tz)
        self.assertEqual(start, datetime(2026, 5, 26, 22, 0))
        self.assertEqual(end, datetime(2026, 5, 27, 22, 0))
        self.assertIsNone(start.tzinfo)
        self.assertIsNone(end.tzinfo)

    def test_utc_day_window_is_exactly_one_calendar_day(self) -> None:
        tz = resolve_tz("UTC")
        start, end = local_day_window(date(2026, 5, 27), tz)
        self.assertEqual(start, datetime(2026, 5, 27, 0, 0))
        self.assertEqual(end, datetime(2026, 5, 28, 0, 0))


class TestReservationReadEmitsZ(TestCase):
    """The frontend parses these strings with ``new Date(iso)``; missing
    Z means the browser interprets the value as browser-local and the
    displayed time drifts by the tz offset."""

    def _payload(self) -> dict[str, Any]:
        return {
            "id": "r1",
            "restaurant_id": "rest1",
            "guest_name": "Tester",
            "party_size": 2,
            "reserved_at": datetime(2026, 5, 27, 7, 30),
            "end_time": datetime(2026, 5, 27, 9, 0),
            "status": "confirmed",
            "source": "widget",
        }

    def test_reserved_at_serialises_with_z(self) -> None:
        body = json.loads(ReservationRead(**self._payload()).model_dump_json())
        self.assertEqual(body["reserved_at"], "2026-05-27T07:30:00Z")

    def test_end_time_serialises_with_z(self) -> None:
        body = json.loads(ReservationRead(**self._payload()).model_dump_json())
        self.assertEqual(body["end_time"], "2026-05-27T09:00:00Z")

    def test_completed_at_serialises_with_z_when_set(self) -> None:
        payload = self._payload()
        payload["completed_at"] = datetime(2026, 5, 27, 11, 0)
        body = json.loads(ReservationRead(**payload).model_dump_json())
        self.assertEqual(body["completed_at"], "2026-05-27T11:00:00Z")
