"""Tests for screen_locker._status.run_status().""" from __future__ import annotations from datetime import UTC import json from pathlib import Path from typing import TYPE_CHECKING from unittest.mock import MagicMock, patch from screen_locker._status import _load_extra_benefits, run_status from screen_locker.tests.conftest import _make_locker if TYPE_CHECKING: import pytest # --------------------------------------------------------------------------- # _load_extra_benefits helpers # --------------------------------------------------------------------------- class TestLoadExtraBenefits: """Tests for _load_extra_benefits.""" def test_missing_file_returns_empty(self, tmp_path: Path) -> None: """Non-existent EXTRA_BENEFITS_FILE → {}.""" with patch("screen_locker._status.EXTRA_BENEFITS_FILE", tmp_path / "nope.json"): assert _load_extra_benefits() == {} def test_valid_json_returned(self, tmp_path: Path) -> None: """Valid JSON → dict.""" f = tmp_path / "eb.json" f.write_text(json.dumps({"skip_credits": 2})) with patch("screen_locker._status.EXTRA_BENEFITS_FILE", f): assert _load_extra_benefits() == {"skip_credits": 2} def test_invalid_json_returns_empty(self, tmp_path: Path) -> None: """ValueError (invalid JSON) → {}.""" f = tmp_path / "eb.json" f.write_text("{bad}") with patch("screen_locker._status.EXTRA_BENEFITS_FILE", f): assert _load_extra_benefits() == {} def test_oserror_returns_empty(self, tmp_path: Path) -> None: """OSError on read_text → {}.""" f = tmp_path / "eb.json" f.write_text("{}") with ( patch("screen_locker._status.EXTRA_BENEFITS_FILE", f), patch.object(Path, "read_text", side_effect=OSError("perm")), ): assert _load_extra_benefits() == {} # --------------------------------------------------------------------------- # run_status integration tests # --------------------------------------------------------------------------- class TestRunStatusNormal: """Tests for run_status display paths (no workouts in log).""" def test_empty_log_no_fill( self, tmp_path: Path, capsys: pytest.CaptureFixture ) -> None: """Empty log, no RunnerUp fill → 'No new workouts found', need-more message.""" eb_file = tmp_path / "eb.json" log_file = tmp_path / "log.json" locker = _make_locker(log_file, n_filled=0) with ( patch("screen_locker._status.EXTRA_BENEFITS_FILE", eb_file), patch("screen_locker._status.current_streak", return_value=0), patch("screen_locker._status.has_extended_early_bird", return_value=False), patch("screen_locker._status.count_weekly_workouts", return_value=0), patch("sys.exit"), ): run_status(locker) out = capsys.readouterr().out assert "No new workouts found" in out assert "Need" in out def test_full_week_no_break_when_today_is_sunday( self, tmp_path: Path, capsys: pytest.CaptureFixture ) -> None: """Today == the week's Sunday: the per-day loop runs all 7 days, never breaking early.""" from datetime import datetime fixed_now = datetime(2026, 7, 5, 12, 0, tzinfo=UTC) # a real Sunday eb_file = tmp_path / "eb.json" log_file = tmp_path / "log.json" locker = _make_locker(log_file, n_filled=0) mock_datetime_cls = MagicMock() mock_datetime_cls.now.return_value = fixed_now with ( patch("screen_locker._status.datetime", mock_datetime_cls), patch("screen_locker._status.EXTRA_BENEFITS_FILE", eb_file), patch("screen_locker._status.current_streak", return_value=0), patch("screen_locker._status.has_extended_early_bird", return_value=False), patch("screen_locker._status.count_weekly_workouts", return_value=0), patch("sys.exit"), ): run_status(locker) out = capsys.readouterr().out assert out.count("no entry") == 7 def test_sick_day_shown_when_no_log_entry( self, tmp_path: Path, capsys: pytest.CaptureFixture ) -> None: """A date with no workout_log entry but in the sickdays pool → sick_day.""" from screen_locker.tests._sick_helpers import mark_sick mark_sick() eb_file = tmp_path / "eb.json" log_file = tmp_path / "log.json" locker = _make_locker(log_file, n_filled=0) with ( patch("screen_locker._status.EXTRA_BENEFITS_FILE", eb_file), patch("screen_locker._status.current_streak", return_value=0), patch("screen_locker._status.has_extended_early_bird", return_value=False), patch("screen_locker._status.count_weekly_workouts", return_value=0), patch("sys.exit"), ): run_status(locker) out = capsys.readouterr().out assert "sick_day" in out def test_shutdown_config_printed( self, tmp_path: Path, capsys: pytest.CaptureFixture ) -> None: """Shutdown config present → shutdown time line shown.""" eb_file = tmp_path / "eb.json" locker = _make_locker(tmp_path / "log.json", cfg=(1320, 1320, 300)) with ( patch("screen_locker._status.EXTRA_BENEFITS_FILE", eb_file), patch("screen_locker._status.current_streak", return_value=0), patch("screen_locker._status.has_extended_early_bird", return_value=False), patch("screen_locker._status.count_weekly_workouts", return_value=0), patch("sys.exit"), ): run_status(locker) out = capsys.readouterr().out assert "Shutdown tonight" in out assert "22:00" in out def test_no_shutdown_config( self, tmp_path: Path, capsys: pytest.CaptureFixture ) -> None: """Shutdown config None → no shutdown line.""" eb_file = tmp_path / "eb.json" locker = _make_locker(tmp_path / "log.json", cfg=None) with ( patch("screen_locker._status.EXTRA_BENEFITS_FILE", eb_file), patch("screen_locker._status.current_streak", return_value=0), patch("screen_locker._status.has_extended_early_bird", return_value=False), patch("screen_locker._status.count_weekly_workouts", return_value=0), patch("sys.exit"), ): run_status(locker) out = capsys.readouterr().out assert "Shutdown tonight" not in out def test_streak_shown_and_no_weekly_bonus_line( self, tmp_path: Path, capsys: pytest.CaptureFixture ) -> None: """streak=2, eb_ext=True → shown; the dropped weekly bonus is not.""" eb_file = tmp_path / "eb.json" locker = _make_locker(tmp_path / "log.json", n_filled=0) with ( patch("screen_locker._status.EXTRA_BENEFITS_FILE", eb_file), patch("screen_locker._status.current_streak", return_value=2), patch("screen_locker._status.has_extended_early_bird", return_value=True), patch("screen_locker._status.count_weekly_workouts", return_value=0), patch("sys.exit"), ): run_status(locker) out = capsys.readouterr().out assert "Shutdown bonus" not in out assert "Streak (5+ wks) : 2" in out assert "Yes — until 09:00" in out class TestRunStatusWorkoutLog: """Tests for per-day log display and counted/uncounted workout marking.""" def test_counted_entry_shown_with_checkmark( self, tmp_path: Path, capsys: pytest.CaptureFixture ) -> None: """Log entry with counted type → ✓ mark printed.""" from datetime import datetime today = datetime.now(tz=UTC).astimezone().date().isoformat() log_file = tmp_path / "log.json" log_file.write_text( json.dumps( { today: { "workout_data": { "type": "runnerup_verified", "source": "run.tcx", } } } ) ) eb_file = tmp_path / "eb.json" locker = _make_locker(log_file, n_filled=0) with ( patch("screen_locker._status.EXTRA_BENEFITS_FILE", eb_file), patch("screen_locker._status.current_streak", return_value=0), patch("screen_locker._status.has_extended_early_bird", return_value=False), patch("screen_locker._status.count_weekly_workouts", return_value=1), patch("sys.exit"), ): run_status(locker) out = capsys.readouterr().out assert "✓" in out assert "runnerup_verified" in out assert "run.tcx" in out def test_uncounted_entry_shown_with_x( self, tmp_path: Path, capsys: pytest.CaptureFixture ) -> None: """Log entry with uncounted type → ✗ mark printed.""" from datetime import datetime today = datetime.now(tz=UTC).astimezone().date().isoformat() log_file = tmp_path / "log.json" log_file.write_text( json.dumps({today: {"workout_data": {"type": "heat_skip", "source": ""}}}) ) eb_file = tmp_path / "eb.json" locker = _make_locker(log_file, n_filled=0) with ( patch("screen_locker._status.EXTRA_BENEFITS_FILE", eb_file), patch("screen_locker._status.current_streak", return_value=0), patch("screen_locker._status.has_extended_early_bird", return_value=False), patch("screen_locker._status.count_weekly_workouts", return_value=0), patch("sys.exit"), ): run_status(locker) out = capsys.readouterr().out assert "heat_skip" in out