from datetime import date, datetime import pytest from random_times import MIN_GAP_MIN, compute_fire_times # Window 09:00-21:00 = minutes 540..1260 -> 720 min wide. WINDOW = "09:00-21:00" WINDOW_START = datetime(2026, 3, 21, 9, 0) WINDOW_END = datetime(2026, 3, 21, 21, 0) def cfg(**overrides) -> dict: base = {"times_per_day": 5, "window": WINDOW} base.update(overrides) return base def test_deterministic(): day = date(2026, 3, 21) assert compute_fire_times(day, "drink water", cfg()) == compute_fire_times(day, "drink water", cfg()) def test_differs_per_text(): day = date(2026, 3, 21) assert compute_fire_times(day, "drink water", cfg()) != compute_fire_times(day, "stretch", cfg()) @pytest.mark.parametrize("count", [1, 2, 5, 10]) def test_count_matches_times_per_day(count): times = compute_fire_times(date(2026, 3, 21), "x", cfg(times_per_day=count)) assert len(times) == count def test_min_gap_respected(): times = compute_fire_times(date(2026, 3, 21), "x", cfg(times_per_day=8)) for earlier, later in zip(times, times[1:]): assert (later - earlier).total_seconds() >= MIN_GAP_MIN * 60 def test_within_window(): for fire in compute_fire_times(date(2026, 3, 21), "x", cfg()): assert WINDOW_START <= fire <= WINDOW_END @pytest.mark.parametrize( "day,expected", [ (date(2026, 3, 23), True), # Monday (date(2026, 3, 27), True), # Friday (date(2026, 3, 28), False), # Saturday (date(2026, 3, 29), False), # Sunday ], ) def test_days_weekday_filter(day, expected): times = compute_fire_times(day, "x", cfg(days="1-5")) assert bool(times) == expected @pytest.mark.parametrize( "spec,day,expected", [ ("*", date(2026, 3, 28), True), # Saturday allowed by wildcard ("0", date(2026, 3, 29), True), # Sunday as 0 ("7", date(2026, 3, 29), True), # Sunday as 7 ("1,3,5", date(2026, 3, 25), True), # Wednesday ("1,3,5", date(2026, 3, 24), False), # Tuesday ], ) def test_days_parser(spec, day, expected): times = compute_fire_times(day, "x", cfg(days=spec)) assert bool(times) == expected def test_from_until_filter(): bounded = cfg(**{"from": "2026-06-01", "until": "2026-06-30"}) assert compute_fire_times(date(2026, 5, 31), "x", bounded) == [] assert compute_fire_times(date(2026, 7, 1), "x", bounded) == [] assert len(compute_fire_times(date(2026, 6, 15), "x", bounded)) == 5 def test_infeasible_count_raises(): # 50 times * 15-min gap = 735 min required > 720 min window. with pytest.raises(ValueError): compute_fire_times(date(2026, 3, 21), "x", cfg(times_per_day=50)) def test_bad_window_raises(): with pytest.raises(ValueError): compute_fire_times(date(2026, 3, 21), "x", cfg(window="21:00-09:00")) def test_config_validated_before_date_filter(): # Out-of-range date still surfaces a structural error rather than returning []. with pytest.raises(ValueError): compute_fire_times(date(2000, 1, 1), "x", cfg(times_per_day=50, until="1999-01-01")) # --- Weekly period ----------------------------------------------------------- # Week of Mon 2026-03-23 .. Sun 2026-03-29. WEEK = [date(2026, 3, d) for d in range(23, 30)] def weekly_cfg(**overrides) -> dict: base = {"times_per_day": 2, "window": WINDOW, "period": "week"} base.update(overrides) return base def _week_fires(text: str, cfg_dict: dict) -> list[datetime]: return [fire for day in WEEK for fire in compute_fire_times(day, text, cfg_dict)] def test_weekly_count_across_week(): assert len(_week_fires("x", weekly_cfg(times_per_day=2))) == 2 def test_weekly_distinct_days(): fires = _week_fires("x", weekly_cfg(times_per_day=3)) assert len({f.date() for f in fires}) == 3 def test_weekly_deterministic_across_days(): # Every day of the week must agree on the same plan, so summing per-day calls # over the week yields a stable set regardless of call order. assert _week_fires("walk", weekly_cfg()) == _week_fires("walk", weekly_cfg()) def test_weekly_within_window(): for fire in _week_fires("x", weekly_cfg(times_per_day=4)): assert WINDOW_START.time() <= fire.time() <= WINDOW_END.time() def test_weekly_days_filter_limits_eligible(): fires = _week_fires("x", weekly_cfg(times_per_day=2, days="1-5")) assert all(f.weekday() < 5 for f in fires) def test_weekly_count_clamped_to_eligible_days(): # Capacity (7 days) admits 3, but until clips this week to Mon+Tue -> 2 fires, no error. bounded = weekly_cfg(times_per_day=3, until="2026-03-24") fires = _week_fires("x", bounded) assert len(fires) == 2 assert {f.date() for f in fires} == {date(2026, 3, 23), date(2026, 3, 24)} def test_weekly_from_until_clips_to_partial_week(): bounded = weekly_cfg(times_per_day=2, **{"from": "2026-03-25", "until": "2026-03-27"}) fires = _week_fires("x", bounded) assert all(date(2026, 3, 25) <= f.date() <= date(2026, 3, 27) for f in fires) assert len(fires) == 2 def test_weekly_count_exceeds_capacity_raises(): with pytest.raises(ValueError): compute_fire_times(WEEK[0], "x", weekly_cfg(times_per_day=8)) def test_weekly_count_exceeds_filtered_capacity_raises(): with pytest.raises(ValueError): compute_fire_times(WEEK[0], "x", weekly_cfg(times_per_day=3, days="1,2"))