"""Tests for the ProgressionService class.""" from datetime import date, timedelta from sqlmodel import SQLModel, Session, create_engine from app.models.user import User from app.models.exercise import Exercise from app.models.workout_day import WorkoutDay from app.models.workout_session import WorkoutSession from app.models.workout_log import WorkoutLog from app.models.user_exercise_program import UserExerciseProgram from app.models.progress_log import ProgressLog from app.services.progression_service import ProgressionService class TestProgressionService: """Tests for the auto-progression engine.""" def _setup(self): """Create an in-memory DB with a user, exercise, and program.""" engine = create_engine("sqlite:///:memory:") SQLModel.metadata.create_all(engine) session = Session(engine) user = User(username="phil", password_hash="h", display_name="Phillip") day = WorkoutDay(name="Push", day_number=1, description="Push day") exercise = Exercise( name="DB Chest Press", muscle_group="Chest", workout_day="Push", sets=3, tempo="3-1-2", form_cues="...", ) session.add_all([user, day, exercise]) session.commit() session.refresh(user) session.refresh(day) session.refresh(exercise) program = UserExerciseProgram( user_id=user.id, exercise_id=exercise.id, wk1_reps="8", wk4_reps="12", wk1_weight="30 lbs", wk4_weight="40 lbs", ) session.add(program) session.commit() session.refresh(program) service = ProgressionService(session) return session, service, user, day, exercise, program def test_suggest_reps_increase(self) -> None: """Should suggest +1-2 reps when below wk4 target.""" session, service, user, day, exercise, program = self._setup() # Log a session where user did 8 reps (wk1 target) ws = WorkoutSession( user_id=user.id, workout_day_id=day.id, date=date.today() - timedelta(days=7), ) session.add(ws) session.commit() session.refresh(ws) for set_num in range(1, 4): session.add(WorkoutLog( session_id=ws.id, exercise_id=exercise.id, set_number=set_num, reps_completed=8, weight_used="30 lbs", felt_easy=False, )) session.commit() suggestion = service.get_suggestion(user.id, exercise.id) assert suggestion is not None assert suggestion["suggested_reps"] >= 9 # +1-2 reps assert suggestion["suggested_weight"] == "30 lbs" # same weight assert suggestion["progression_type"] == "reps_increase" session.close() def test_suggest_weight_increase(self) -> None: """Should suggest +5 lbs when at wk4 rep target and felt easy.""" session, service, user, day, exercise, program = self._setup() # Log two sessions at max reps, all felt easy for week_offset in [14, 7]: ws = WorkoutSession( user_id=user.id, workout_day_id=day.id, date=date.today() - timedelta(days=week_offset), ) session.add(ws) session.commit() session.refresh(ws) for set_num in range(1, 4): session.add(WorkoutLog( session_id=ws.id, exercise_id=exercise.id, set_number=set_num, reps_completed=12, weight_used="30 lbs", felt_easy=True, )) session.commit() suggestion = service.get_suggestion(user.id, exercise.id) assert suggestion is not None assert suggestion["suggested_weight"] == "35 lbs" # +5 lbs assert suggestion["progression_type"] == "weight_increase" session.close() def test_suggest_deload(self) -> None: """Should suggest deload after 4 weeks of progression.""" session, service, user, day, exercise, program = self._setup() # Log 4 weeks of sessions (simulate week 5 trigger) for week in range(4): ws = WorkoutSession( user_id=user.id, workout_day_id=day.id, date=date.today() - timedelta(days=(4 - week) * 7), ) session.add(ws) session.commit() session.refresh(ws) for set_num in range(1, 4): session.add(WorkoutLog( session_id=ws.id, exercise_id=exercise.id, set_number=set_num, reps_completed=12, weight_used="40 lbs", felt_easy=False, )) session.commit() suggestion = service.get_suggestion(user.id, exercise.id) assert suggestion is not None # After 4 consecutive weeks, week 5 should be deload if suggestion["progression_type"] == "deload": assert "32" in suggestion["suggested_weight"] # -20% of 40 session.close() def test_no_suggestion_without_logs(self) -> None: """Should return program defaults when no logs exist.""" session, service, user, day, exercise, program = self._setup() suggestion = service.get_suggestion(user.id, exercise.id) assert suggestion is not None assert suggestion["suggested_reps"] == 8 # wk1 default assert suggestion["suggested_weight"] == "30 lbs" # wk1 default assert suggestion["progression_type"] == "baseline" session.close() def test_record_progression(self) -> None: """record_progression should write to progress_log table.""" session, service, user, day, exercise, program = self._setup() service.record_progression( user_id=user.id, exercise_id=exercise.id, suggested_reps=10, suggested_weight="30 lbs", actual_reps=10, actual_weight="30 lbs", progression_type="reps_increase", ) from sqlmodel import select logs = session.exec(select(ProgressLog)).all() assert len(logs) == 1 assert logs[0].progression_applied == "reps_increase" session.close()