# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """Warsaw days, and the periods built out of them.""" from __future__ import annotations import datetime as dt import pytest from signal_bot.stats import periods # 2026-09-01 21:37 Warsaw, as Signal would timestamp it. TUESDAY_MS = int(dt.datetime(2026, 9, 1, 21, 37, tzinfo=periods.WARSAW).timestamp() * 1000) def test_now_is_warsaw() -> None: assert periods.now().tzinfo is periods.WARSAW def test_day_and_hour_are_warsaw_wall_clock() -> None: assert periods.day_of(TUESDAY_MS) == "2026-09-01" assert periods.hour_of(TUESDAY_MS) == 21 def test_a_late_evening_message_stays_on_its_own_day() -> None: """23:50 Warsaw is still today, even though it is already tomorrow in UTC+3.""" late = int(dt.datetime(2026, 9, 1, 23, 50, tzinfo=periods.WARSAW).timestamp() * 1000) assert periods.day_of(late) == "2026-09-01" @pytest.mark.parametrize( ("day", "hours"), [("2025-10-26", 25), ("2026-03-29", 23), ("2026-10-25", 25), ("2026-09-01", 24)], ) def test_warsaw_days_are_not_all_24_hours(day: str, hours: int) -> None: """The reason every period groups on the stored day string. Computed through UTC deliberately: subtracting two aware datetimes that share a tzinfo gives the wall-clock difference, which reports all four of these as 24 and hides the whole problem. """ date = dt.date.fromisoformat(day) start = dt.datetime.combine(date, dt.time(), periods.WARSAW).astimezone(dt.UTC) end = dt.datetime.combine(date + dt.timedelta(days=1), dt.time(), periods.WARSAW) assert (end.astimezone(dt.UTC) - start).total_seconds() / 3600 == hours def test_shift_crosses_a_short_day_by_the_date() -> None: """Minus 86400 seconds would land at 23:00 the evening before.""" assert periods.shift("2026-03-30", -1) == "2026-03-29" assert periods.shift("2026-03-29", -1) == "2026-03-28" def test_today_and_previous_day_take_an_injected_clock() -> None: at = dt.datetime(2026, 9, 2, 0, 30, tzinfo=periods.WARSAW) assert periods.today(at) == "2026-09-02" assert periods.previous_day(at) == "2026-09-01" def test_a_utc_clock_is_converted_rather_than_truncated() -> None: """22:30 UTC is already the next day in Warsaw.""" at = dt.datetime(2026, 9, 1, 22, 30, tzinfo=dt.UTC) assert periods.today(at) == "2026-09-02" def test_weekday_of() -> None: assert periods.weekday_of("2026-09-01") == 1 assert periods.WEEKDAY_NAMES[1] == "Wtorek" assert periods.WEEKDAY_PLURAL[1] == "wtorki" def test_pl_date_uses_the_genitive_month() -> None: assert periods.pl_date("2026-09-01") == "Wtorek, 1 września" assert periods.pl_date("2026-01-31") == "Sobota, 31 stycznia" def test_week_is_monday_to_sunday() -> None: week = periods.week_of("2026-09-01") assert (week.start, week.end, week.days) == ("2026-08-31", "2026-09-06", 7) assert week.label == "tydzień 36" def test_month_bounds_and_length() -> None: for day, end, length in [ ("2026-09-15", "2026-09-30", 30), ("2026-02-03", "2026-02-28", 28), ("2028-02-03", "2028-02-29", 29), ("2026-12-31", "2026-12-31", 31), ]: month = periods.month_of(day) assert (month.end, month.days) == (end, length) assert periods.month_of("2026-09-15").label == "wrzesień 2026" def test_year_bounds() -> None: year = periods.year_of("2026-09-01") assert (year.start, year.end, year.label, year.days) == ( "2026-01-01", "2026-12-31", "2026", 365, ) @pytest.mark.parametrize( ("day", "kind", "expected_start"), [ ("2026-09-01", "week", "2026-08-24"), ("2026-09-15", "month", "2026-08-01"), ("2026-09-15", "year", "2025-01-01"), ], ) def test_previous_period_is_exact(day: str, kind: str, expected_start: str) -> None: """Not "minus seven days": the month before a 31-day month is not 31 days.""" current = {"week": periods.week_of, "month": periods.month_of, "year": periods.year_of}[kind]( day ) assert periods.previous(current).start == expected_start def test_previous_month_of_january_is_december() -> None: assert periods.previous(periods.month_of("2026-01-10")).label == "grudzień 2025"