from datetime import datetime from zoneinfo import ZoneInfo from beaver_agent.jobs.leisure import ( MINE_MIN, QUIET_TICKS, Plan, brief, day_of, enabled, plan_of, quiet_hour, tick_hours, ) ZONE = ZoneInfo("Europe/Warsaw") def at(text: str) -> datetime: return datetime.fromisoformat(text).replace(tzinfo=ZONE) def test_switch_defaults_to_on_and_only_explicit_off_stops_it(): assert enabled(None) assert enabled("# досуг\n\nничего про выключатель") assert enabled("досуг: вкл") assert not enabled("# досуг\n\nдосуг: выкл\nсутки: 2026-09-08") assert not enabled("досуг: off") def test_day_starts_at_noon(): assert day_of(at("2026-09-08 12:00")) == "2026-09-08" assert day_of(at("2026-09-08 23:30")) == "2026-09-08" assert day_of(at("2026-09-09 03:00")) == "2026-09-08" assert day_of(at("2026-09-09 11:59")) == "2026-09-08" assert day_of(at("2026-09-09 12:00")) == "2026-09-09" def test_active_window_ticks_hourly_and_quiet_window_only_by_plan(): hours = tick_hours(QUIET_TICKS) assert hours[0] == 12 assert len(hours) == 20 assert all(h in hours for h in (12, 18, 23, 0, 3)) assert [h for h in hours if quiet_hour(h)] == [4, 6, 8, 10] assert 5 not in hours and 11 not in hours assert tick_hours((5, 7))[-2:] == (5, 7) def test_plan_is_read_from_the_file_for_todays_day_only(): text = ( "# досуг\n\n" "досуг: вкл\n" "сутки: 2026-09-08\n" "тихие часы: 05, 07, 09, 11\n" "мои часы: 13, 16, 19, 22, 01, 05, 09\n" ) plan = plan_of(text, "2026-09-08") assert plan.written assert plan.quiet == (5, 7, 9, 11) assert plan.mine == (13, 16, 19, 22, 1, 5, 9) stale = plan_of(text, "2026-09-09") assert not stale.written assert stale.quiet == QUIET_TICKS def test_missing_plan_falls_back_to_defaults(): plan = plan_of(None, "2026-09-08") assert not plan.written assert plan.quiet == QUIET_TICKS assert len(plan.mine) == MINE_MIN assert set(plan.mine) <= set(tick_hours(plan.quiet)) def test_short_plan_is_topped_up_to_the_guaranteed_minimum(): text = "сутки: 2026-09-08\nмои часы: 14, 20\n" plan = plan_of(text, "2026-09-08") assert plan.written assert len(plan.mine) == MINE_MIN assert 14 in plan.mine and 20 in plan.mine assert plan.mine == tuple(sorted(plan.mine, key=tick_hours(plan.quiet).index)) def test_unreachable_hours_are_dropped_from_the_plan(): text = "сутки: 2026-09-08\nтихие часы: 04, 06, 08, 10\nмои часы: 05, 11, 14\n" plan = plan_of(text, "2026-09-08") assert 5 not in plan.mine and 11 not in plan.mine assert 14 in plan.mine and len(plan.mine) == MINE_MIN def test_brief_names_the_type_and_counts_spent_slots(): plan = Plan( day="2026-09-08", quiet=QUIET_TICKS, mine=(13, 15, 18, 21, 0, 2, 6), written=True, ) text = brief(at("2026-09-08 18:00"), plan) assert "Тип: досуг" in text assert "израсходовано 3 из 7" in text assert "впереди 21, 00, 02, 06" in text assert "Сутки заканчиваются через 18 ч" in text assert "Задания нет" in text assert "Разложи" not in text mixed = brief(at("2026-09-08 19:00"), plan) assert "Тип: смешанный" in mixed assert "израсходовано 3 из 7" in mixed def test_first_tick_of_the_day_is_the_planning_one(): text = brief(at("2026-09-08 12:00"), plan_of(None, "2026-09-08")) assert "Первый тик суток" in text assert "расписания на эти сутки нет" in text.lower() assert str(MINE_MIN) in text def test_last_night_tick_says_nothing_is_left_ahead(): plan = Plan(day="2026-09-08", quiet=QUIET_TICKS, mine=(13, 6), written=True) text = brief(at("2026-09-09 06:00"), plan) assert "впереди больше нет" in text assert "израсходовано 2 из 2" in text assert "Сутки заканчиваются через 6 ч" in text