"""Reminder wave planning and quiet hours (tasks 5.2-5.3, spec reminders).

Timezone arithmetic and the skip-late rule are the two places this system can fail
silently: nobody notices a reminder that never fired until the wedding, and a reminder that
fires at 3am wakes 800 people.
"""

from datetime import UTC, datetime, time, timedelta

import pytest

from app.models.enums import Channel
from app.services.messaging import (
    MAX_ATTEMPTS,
    RETRY_DELAYS,
    defer_past_quiet_hours,
    idempotency_key,
    in_quiet_hours,
)
from app.services.planner import OFFSET_PURPOSE, compute_send_at

DHAKA = "Asia/Dhaka"


def test_wave_lands_at_the_configured_local_time() -> None:
    """A Walima at 19:00 Dhaka on 12 Sept: T-2 must be 10:00 Dhaka on 10 Sept, which is
    04:00 UTC — not 10:00 UTC."""
    starts = datetime(2026, 9, 12, 13, 0, tzinfo=UTC)  # 19:00 Dhaka
    send_at = compute_send_at(starts, 2, time(10, 0), DHAKA)

    assert send_at.astimezone(UTC) == datetime(2026, 9, 10, 4, 0, tzinfo=UTC)


@pytest.mark.parametrize(("offset", "expected_day"), [(15, 28), (7, 5), (2, 10)])
def test_all_three_waves_land_on_the_right_dates(offset: int, expected_day: int) -> None:
    starts = datetime(2026, 9, 12, 13, 0, tzinfo=UTC)
    send_at = compute_send_at(starts, offset, time(10, 0), DHAKA)
    local = send_at.astimezone(__import__("zoneinfo").ZoneInfo(DHAKA))
    assert local.day == expected_day
    assert (local.hour, local.minute) == (10, 0)


def test_an_event_just_after_midnight_local_does_not_slip_a_day() -> None:
    """00:30 Dhaka is still the previous day in UTC — computing in UTC would be off by one."""
    starts = datetime(2026, 9, 11, 18, 30, tzinfo=UTC)  # 00:30 Dhaka on the 12th
    send_at = compute_send_at(starts, 2, time(10, 0), DHAKA)
    local = send_at.astimezone(__import__("zoneinfo").ZoneInfo(DHAKA))
    assert (local.month, local.day) == (9, 10)


def test_each_offset_maps_to_its_own_template() -> None:
    """A T-15 email must not say 'just 2 days to go'."""
    assert len({OFFSET_PURPOSE[15], OFFSET_PURPOSE[7], OFFSET_PURPOSE[2]}) == 3


# ------------------------------------------------------------------ idempotency


def test_idempotency_key_is_stable_for_the_same_triple() -> None:
    """The unique constraint on this value is the entire duplicate-send defence."""
    import uuid

    inv, sched = uuid.uuid4(), uuid.uuid4()
    assert idempotency_key(inv, sched, Channel.EMAIL) == idempotency_key(inv, sched, Channel.EMAIL)


def test_idempotency_key_separates_waves_and_channels() -> None:
    import uuid

    inv = uuid.uuid4()
    wave_a, wave_b = uuid.uuid4(), uuid.uuid4()
    assert idempotency_key(inv, wave_a, Channel.EMAIL) != idempotency_key(
        inv, wave_b, Channel.EMAIL
    )
    # v2 adds channels; the same wave on a different channel is a different job.
    assert idempotency_key(inv, wave_a, Channel.EMAIL) != idempotency_key(inv, wave_a, Channel.SMS)


def test_direct_sends_are_distinguishable_from_scheduled_ones() -> None:
    import uuid

    inv = uuid.uuid4()
    assert "direct" in idempotency_key(inv, None, Channel.EMAIL)


# ------------------------------------------------------------------ quiet hours


@pytest.mark.parametrize("hour", [22, 23, 0, 3, 7])
def test_night_hours_are_quiet(hour: int) -> None:
    moment = datetime(2026, 9, 10, hour, 0, tzinfo=__import__("zoneinfo").ZoneInfo(DHAKA))
    assert in_quiet_hours(moment, DHAKA, "22:00", "08:00")


@pytest.mark.parametrize("hour", [8, 10, 14, 21])
def test_daytime_is_not_quiet(hour: int) -> None:
    moment = datetime(2026, 9, 10, hour, 0, tzinfo=__import__("zoneinfo").ZoneInfo(DHAKA))
    assert not in_quiet_hours(moment, DHAKA, "22:00", "08:00")


def test_a_late_night_send_defers_to_the_next_morning() -> None:
    """FR-6.9: a retry that becomes due at 23:30 waits until 08:00, it does not fire."""
    tz = __import__("zoneinfo").ZoneInfo(DHAKA)
    moment = datetime(2026, 9, 10, 23, 30, tzinfo=tz)
    deferred = defer_past_quiet_hours(moment, DHAKA, "22:00", "08:00").astimezone(tz)
    assert (deferred.day, deferred.hour) == (11, 8)


def test_an_early_morning_send_defers_to_the_same_morning() -> None:
    tz = __import__("zoneinfo").ZoneInfo(DHAKA)
    moment = datetime(2026, 9, 10, 3, 0, tzinfo=tz)
    deferred = defer_past_quiet_hours(moment, DHAKA, "22:00", "08:00").astimezone(tz)
    assert (deferred.day, deferred.hour) == (10, 8)


def test_a_daytime_send_is_left_alone() -> None:
    tz = __import__("zoneinfo").ZoneInfo(DHAKA)
    moment = datetime(2026, 9, 10, 14, 0, tzinfo=tz)
    assert defer_past_quiet_hours(moment, DHAKA, "22:00", "08:00") == moment


# ------------------------------------------------------------------ retries


def test_backoff_schedule_matches_the_spec() -> None:
    """FR-6.10: three attempts at +1min, +10min, +1hr."""
    assert (timedelta(minutes=1), timedelta(minutes=10), timedelta(hours=1)) == RETRY_DELAYS
    assert MAX_ATTEMPTS == 3


def test_backoff_is_strictly_increasing() -> None:
    assert list(RETRY_DELAYS) == sorted(RETRY_DELAYS)
