from datetime import date, timedelta from django.test import TestCase from django.contrib.auth.models import User from rest_framework.test import APIClient from rest_framework.authtoken.models import Token from registered_user.models import RegisteredUser from generator.models import ( UserPreference, WorkoutType, GeneratedWeeklyPlan, GeneratedWorkout, ) from workout.models import Workout from superset.models import Superset, SupersetExercise from exercise.models import Exercise class TestRegenerationContext(TestCase): """Tests for regeneration context (sibling exercise exclusion).""" def setUp(self): self.django_user = User.objects.create_user( username='regenuser', password='testpass123', email='regen@example.com', ) self.registered_user = RegisteredUser.objects.create( user=self.django_user, first_name='Regen', last_name='User', ) self.token = Token.objects.create(user=self.django_user) self.client = APIClient() self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') self.workout_type = WorkoutType.objects.create( name='functional_strength_training', display_name='Functional Strength', typical_rest_between_sets=60, typical_intensity='medium', rep_range_min=8, rep_range_max=12, round_range_min=3, round_range_max=4, duration_bias=0.3, superset_size_min=2, superset_size_max=4, ) self.preference = UserPreference.objects.create( registered_user=self.registered_user, days_per_week=3, ) self.preference.preferred_workout_types.add(self.workout_type) # Create the "First Up" exercise required by superset serializer helper Exercise.objects.get_or_create( name='First Up', defaults={ 'is_reps': False, 'is_duration': True, }, ) # Create enough exercises for testing (needs large pool so hard exclusion isn't relaxed) self.exercises = [] for i in range(60): ex = Exercise.objects.create( name=f'Test Exercise {i}', is_reps=True, is_weight=(i % 2 == 0), ) self.exercises.append(ex) # Create a plan with 2 workouts week_start = date(2026, 3, 2) self.plan = GeneratedWeeklyPlan.objects.create( registered_user=self.registered_user, week_start_date=week_start, week_end_date=week_start + timedelta(days=6), status='completed', ) # Workout 1 (Monday): uses exercises 0-4 self.workout1 = Workout.objects.create( name='Monday Workout', registered_user=self.registered_user, ) ss1 = Superset.objects.create( workout=self.workout1, name='Set 1', rounds=3, order=1, ) for i in range(5): SupersetExercise.objects.create( superset=ss1, exercise=self.exercises[i], reps=10, order=i + 1, ) self.gen_workout1 = GeneratedWorkout.objects.create( plan=self.plan, workout=self.workout1, workout_type=self.workout_type, scheduled_date=week_start, day_of_week=0, is_rest_day=False, status='accepted', focus_area='Full Body', target_muscles=['chest', 'back'], ) # Workout 2 (Wednesday): uses exercises 5-9 self.workout2 = Workout.objects.create( name='Wednesday Workout', registered_user=self.registered_user, ) ss2 = Superset.objects.create( workout=self.workout2, name='Set 1', rounds=3, order=1, ) for i in range(5, 10): SupersetExercise.objects.create( superset=ss2, exercise=self.exercises[i], reps=10, order=i - 4, ) self.gen_workout2 = GeneratedWorkout.objects.create( plan=self.plan, workout=self.workout2, workout_type=self.workout_type, scheduled_date=week_start + timedelta(days=2), day_of_week=2, is_rest_day=False, status='pending', focus_area='Full Body', target_muscles=['legs', 'shoulders'], ) def test_regenerate_excludes_sibling_exercises(self): """ Regenerating workout 2 should exclude exercises 0-4 (used by workout 1). """ # Get the exercise IDs from workout 1 sibling_exercise_ids = set( SupersetExercise.objects.filter( superset__workout=self.workout1 ).values_list('exercise_id', flat=True) ) self.assertEqual(len(sibling_exercise_ids), 5) # Regenerate workout 2 response = self.client.post( f'/generator/workout/{self.gen_workout2.pk}/regenerate/', ) # May fail if not enough exercises in DB for the generator, # but the logic should at least attempt correctly if response.status_code == 200: # Check that the regenerated workout doesn't use sibling exercises self.gen_workout2.refresh_from_db() if self.gen_workout2.workout: new_exercise_ids = set( SupersetExercise.objects.filter( superset__workout=self.gen_workout2.workout ).values_list('exercise_id', flat=True) ) overlap = new_exercise_ids & sibling_exercise_ids self.assertEqual( len(overlap), 0, f'Regenerated workout should not share exercises with siblings. ' f'Overlap: {overlap}' ) def test_preview_day_with_plan_context(self): """Pass plan_id to preview_day, verify it is accepted.""" response = self.client.post( '/generator/preview-day/', { 'target_muscles': ['chest', 'back'], 'focus_area': 'Upper Body', 'workout_type_id': self.workout_type.pk, 'date': '2026-03-04', 'plan_id': self.plan.pk, }, format='json', ) # Should succeed or fail gracefully, not crash self.assertIn(response.status_code, [200, 500]) if response.status_code == 200: data = response.json() self.assertFalse(data.get('is_rest_day', True)) def test_preview_day_without_plan_id(self): """No plan_id, backward compat - should work as before.""" response = self.client.post( '/generator/preview-day/', { 'target_muscles': ['chest'], 'focus_area': 'Chest', 'date': '2026-03-04', }, format='json', ) # Should succeed or fail gracefully (no crash from missing plan_id) self.assertIn(response.status_code, [200, 500]) if response.status_code == 200: data = response.json() self.assertIn('focus_area', data) def test_regenerate_rest_day_fails(self): """Regenerating a rest day should return 400.""" rest_day = GeneratedWorkout.objects.create( plan=self.plan, workout=None, workout_type=None, scheduled_date=date(2026, 3, 7), day_of_week=5, is_rest_day=True, status='accepted', focus_area='Rest Day', target_muscles=[], ) response = self.client.post( f'/generator/workout/{rest_day.pk}/regenerate/', ) self.assertEqual(response.status_code, 400)