from datetime import date import pytest from app.core.dto.training import ApproachDTO, ExerciseSessionDTO from app.core.metrics import estimate_1rm, session_metrics, summarize def make_session(day: int, *sets: tuple, month: int = 1) -> ExerciseSessionDTO: return ExerciseSessionDTO( date=date(2025, month, day), approaches=[ApproachDTO(weight=weight, reps=reps) for weight, reps in sets], ) def test_estimate_1rm_uses_epley_formula(): assert estimate_1rm(140, 12) == pytest.approx(196) assert estimate_1rm(100, 10) == pytest.approx(133.333, rel=1e-4) def test_single_rep_is_the_weight_itself(): assert estimate_1rm(100, 1) == 100 def test_session_metrics_of_weighted_sets(): metrics = session_metrics(make_session(1, (100, 12), (110, 10)).approaches) assert metrics.max_weight == 110 assert metrics.e1rm == pytest.approx(max(140.0, 110 * (1 + 10 / 30))) assert metrics.volume == 100 * 12 + 110 * 10 assert metrics.total_reps == 22 assert metrics.best_reps == 12 def test_sets_without_weight_count_only_as_reps(): metrics = session_metrics(make_session(1, (0, 7), (47, 12)).approaches) assert metrics.e1rm == pytest.approx(estimate_1rm(47, 12)) assert metrics.volume == 47 * 12 assert metrics.total_reps == 19 def test_session_without_weight_has_no_e1rm(): metrics = session_metrics(make_session(1, (0, 11), (0, 8)).approaches) assert metrics.e1rm is None assert metrics.volume == 0 assert metrics.total_reps == 19 assert metrics.best_reps == 11 def test_weighted_progress_records_and_change(): # newest first, like the repository returns history = [ make_session(20, (100, 12)), # e1RM 140 make_session(10, (110, 12), (100, 12)), # e1RM 154 (record) make_session(1, (80, 10)), # e1RM 106.67 ] progress = summarize(history) assert progress.weighted is True assert progress.max_weight == 110 assert progress.best_primary.value == pytest.approx(154) assert progress.best_primary.date == date(2025, 1, 10) assert progress.best_secondary.value == 110 * 12 + 100 * 12 assert progress.first_primary == pytest.approx(80 * (1 + 10 / 30)) assert progress.last_primary == pytest.approx(140) assert progress.primary_change == pytest.approx(140 - 80 * (1 + 10 / 30)) assert progress.primary_change_percent == pytest.approx( (140 - 80 * (1 + 10 / 30)) / (80 * (1 + 10 / 30)) * 100 ) def test_record_keeps_the_first_date_it_was_reached(): history = [make_session(20, (100, 12)), make_session(10, (100, 12))] assert summarize(history).best_primary.date == date(2025, 1, 10) def test_exercise_without_weight_is_tracked_by_reps(): history = [ make_session(20, (0, 8), (0, 7)), # best 8, total 15 make_session(10, (0, 10), (0, 5)), # best 10, total 15 make_session(1, (0, 6)), # best 6, total 6 ] progress = summarize(history) assert progress.weighted is False assert progress.max_weight == 0 assert progress.best_primary.value == 10 assert progress.best_primary.date == date(2025, 1, 10) assert progress.best_secondary.value == 15 assert progress.best_secondary.date == date(2025, 1, 10) assert (progress.first_primary, progress.last_primary) == (6, 8) assert progress.primary_change == 2 def test_weighted_exercise_ignores_sessions_without_weight_for_primary_metric(): history = [ make_session(20, (0, 12)), # no e1RM make_session(10, (100, 10)), ] progress = summarize(history) assert progress.weighted is True assert progress.last_primary == pytest.approx(estimate_1rm(100, 10)) assert progress.primary_change == 0 def test_percent_change_is_undefined_when_the_first_value_is_zero(): progress = summarize([make_session(1, (0, 0))]) assert progress.primary_change_percent is None