"""Tenant-local time conversion — pure-unit tests (no DB / no Redis).

Covers src/core/timezone.py, the single sanctioned place for zone handling.

Background: storage was already correct (every datetime column is TIMESTAMPTZ),
but wall-clock values had no zone. A scheduled service is stored as
service_date DATE + service_time TIME, and the code that combined them assumed
UTC — so a 2:00 PM Toronto service resolved to 14:00 UTC (10:00 AM local),
pushing the derived "24 hour" family reminder 4-5 hours off and shifting again
across DST. These tests pin the corrected behaviour.
"""
from datetime import date, datetime, time, timezone

import pytest

from src.core.timezone import (
    DEFAULT_TIMEZONE,
    local_datetime,
    resolve_tz,
    tenant_today,
    to_tenant_local,
    tz_abbreviation,
)

TORONTO = resolve_tz("America/Toronto")
KOLKATA = resolve_tz("Asia/Kolkata")
UTC = resolve_tz("UTC")


class TestResolveTz:
    def test_valid_name(self):
        assert str(resolve_tz("Asia/Kolkata")) == "Asia/Kolkata"

    @pytest.mark.parametrize("bad", [None, "", "Not/AZone", "EST5EDT-nonsense"])
    def test_falls_back_rather_than_raising(self, bad):
        """Read path must never 500 a listing page over one bad tenant value."""
        assert str(resolve_tz(bad)) == DEFAULT_TIMEZONE


class TestLocalDatetime:
    def test_summer_toronto_is_utc_minus_4(self):
        # EDT — the canonical case from the bug report.
        assert local_datetime(date(2026, 7, 15), time(14, 0), TORONTO) == datetime(
            2026, 7, 15, 18, 0, tzinfo=timezone.utc
        )

    def test_winter_toronto_is_utc_minus_5(self):
        # EST — same wall-clock, different instant. The old UTC-assuming code
        # produced 14:00Z for both, so the error silently changed size at DST.
        assert local_datetime(date(2026, 1, 15), time(14, 0), TORONTO) == datetime(
            2026, 1, 15, 19, 0, tzinfo=timezone.utc
        )

    def test_half_hour_offset_zone(self):
        # IST is UTC+5:30 — catches any assumption that offsets are whole hours.
        assert local_datetime(date(2026, 7, 15), time(14, 0), KOLKATA) == datetime(
            2026, 7, 15, 8, 30, tzinfo=timezone.utc
        )

    def test_same_wall_clock_differs_by_tenant(self):
        """Two cemeteries booking "2 PM" are not booking the same instant."""
        tor = local_datetime(date(2026, 7, 15), time(14, 0), TORONTO)
        kol = local_datetime(date(2026, 7, 15), time(14, 0), KOLKATA)
        assert tor != kol

    def test_missing_time_is_local_midnight(self):
        assert local_datetime(date(2026, 7, 15), None, TORONTO) == datetime(
            2026, 7, 15, 4, 0, tzinfo=timezone.utc
        )

    def test_missing_date_yields_none(self):
        """No date means no instant — never fabricate one."""
        assert local_datetime(None, time(14, 0), TORONTO) is None

    def test_result_is_always_aware(self):
        assert local_datetime(date(2026, 7, 15), time(14, 0), TORONTO).tzinfo is not None

    def test_utc_tenant_is_identity(self):
        assert local_datetime(date(2026, 7, 15), time(14, 0), UTC) == datetime(
            2026, 7, 15, 14, 0, tzinfo=timezone.utc
        )


class TestDstBoundaries:
    """DST transitions are where wall-clock reasoning breaks down."""

    def test_spring_forward_day_before_and_after(self):
        # 2026-03-08 is the US/Canada spring-forward date.
        before = local_datetime(date(2026, 3, 7), time(14, 0), TORONTO)
        after = local_datetime(date(2026, 3, 9), time(14, 0), TORONTO)
        assert before.hour == 19  # EST, UTC-5
        assert after.hour == 18  # EDT, UTC-4

    def test_ambiguous_hour_resolves_deterministically(self):
        """Fall-back repeats 1:30 AM; we take the first (pre-transition) one."""
        result = local_datetime(date(2026, 11, 1), time(1, 30), TORONTO)
        assert result is not None
        assert result.tzinfo is not None

    def test_consecutive_days_across_transition_are_23h_apart(self):
        """Spring-forward is 2026-03-08 at 2 AM, so Mar 7 is EST and Mar 8 EDT.

        Same wall-clock on consecutive days is only 23 real hours apart here.
        This is why `service_dt - timedelta(hours=24)` cannot be assumed to
        land on the same wall-clock time the previous day — see the residual
        noted on SchedulingService._schedule_reminder.
        """
        mar7 = local_datetime(date(2026, 3, 7), time(14, 0), TORONTO)
        mar8 = local_datetime(date(2026, 3, 8), time(14, 0), TORONTO)
        assert mar7.hour == 19  # EST, UTC-5
        assert mar8.hour == 18  # EDT, UTC-4
        assert (mar8 - mar7).total_seconds() == 23 * 3600


class TestToTenantLocal:
    def test_renders_in_cemetery_zone(self):
        instant = datetime(2026, 7, 15, 18, 0, tzinfo=timezone.utc)
        assert to_tenant_local(instant, TORONTO).hour == 14

    def test_naive_input_treated_as_utc_not_relabelled(self):
        naive = datetime(2026, 7, 15, 18, 0)
        assert to_tenant_local(naive, TORONTO).hour == 14

    def test_none_passes_through(self):
        assert to_tenant_local(None, TORONTO) is None

    def test_round_trip_preserves_wall_clock(self):
        instant = local_datetime(date(2026, 7, 15), time(14, 0), TORONTO)
        rendered = to_tenant_local(instant, TORONTO)
        assert (rendered.date(), rendered.hour, rendered.minute) == (
            date(2026, 7, 15),
            14,
            0,
        )

    def test_india_viewer_sees_same_instant_shifted(self):
        """The originally-reported scenario, from the viewer's side.

        The instant is identical; only its rendering differs. Product rule is
        that scheduled services render in the CEMETERY's zone, so callers pass
        the tenant zone — but the underlying conversion must still be correct.
        """
        instant = local_datetime(date(2026, 7, 15), time(14, 0), TORONTO)
        assert to_tenant_local(instant, KOLKATA).hour == 23
        assert to_tenant_local(instant, KOLKATA).minute == 30


class TestTenantToday:
    def test_returns_a_date(self):
        assert isinstance(tenant_today(TORONTO), date)

    def test_zones_can_disagree_on_today(self):
        """Kolkata is never behind Toronto, and is often a day ahead."""
        assert tenant_today(KOLKATA) >= tenant_today(TORONTO)

    def test_matches_manual_conversion(self):
        expected = datetime.now(timezone.utc).astimezone(TORONTO).date()
        assert tenant_today(TORONTO) == expected


class TestTzAbbreviation:
    def test_depends_on_instant_not_just_zone(self):
        summer = datetime(2026, 7, 15, 18, 0, tzinfo=timezone.utc)
        winter = datetime(2026, 1, 15, 19, 0, tzinfo=timezone.utc)
        assert tz_abbreviation(summer, TORONTO) == "EDT"
        assert tz_abbreviation(winter, TORONTO) == "EST"

    def test_india(self):
        assert tz_abbreviation(datetime(2026, 7, 15, tzinfo=timezone.utc), KOLKATA) == "IST"
