# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """The daily report, driven by an injected clock.""" from __future__ import annotations import datetime as dt from signal_bot.scheduler import SECONDS_PER_DAY, Action, DailyScheduler def _scheduler(action: Action, now: dt.datetime, slept: list[float]) -> DailyScheduler: async def sleeper(seconds: float) -> None: slept.append(seconds) return DailyScheduler(21, 37, action, lambda: now, sleeper) async def _noop() -> None: return None def test_waits_until_later_today() -> None: now = dt.datetime(2026, 8, 29, 20, 37, tzinfo=dt.UTC) assert _scheduler(_noop, now, []).seconds_until_next(now) == 3600 def test_rolls_over_to_tomorrow_once_passed() -> None: now = dt.datetime(2026, 8, 29, 22, 37, tzinfo=dt.UTC) assert _scheduler(_noop, now, []).seconds_until_next(now) == SECONDS_PER_DAY - 3600 def test_exact_target_time_waits_a_full_day() -> None: """Never returns zero, which would busy-loop the report.""" now = dt.datetime(2026, 8, 29, 21, 37, tzinfo=dt.UTC) assert _scheduler(_noop, now, []).seconds_until_next(now) == SECONDS_PER_DAY async def test_runs_the_action_after_sleeping() -> None: calls: list[int] = [] slept: list[float] = [] async def action() -> None: calls.append(1) now = dt.datetime(2026, 8, 29, 20, 37, tzinfo=dt.UTC) await _scheduler(action, now, slept).run(iterations=2) assert calls == [1, 1] assert slept == [3600, 3600] async def test_a_failing_report_does_not_stop_the_loop() -> None: calls: list[int] = [] async def action() -> None: calls.append(1) msg = "signal is down" raise RuntimeError(msg) now = dt.datetime(2026, 8, 29, 20, 37, tzinfo=dt.UTC) await _scheduler(action, now, []).run(iterations=2) assert calls == [1, 1] async def test_zero_iterations_does_nothing() -> None: slept: list[float] = [] now = dt.datetime(2026, 8, 29, 20, 37, tzinfo=dt.UTC) await _scheduler(_noop, now, slept).run(iterations=0) assert slept == []