import json import re from pathlib import Path from fastapi.testclient import TestClient from app.main import create_app def make_client(tmp_path: Path) -> TestClient: app = create_app(state_dir=tmp_path) return TestClient(app) def test_health_endpoint_reports_ok(tmp_path: Path) -> None: client = make_client(tmp_path) response = client.get('/api/health') assert response.status_code == 200 assert response.json() == {'status': 'ok'} def test_dashboard_summary_seeds_john_and_manndra(tmp_path: Path) -> None: client = make_client(tmp_path) response = client.get('/api/dashboard/summary') assert response.status_code == 200 payload = response.json() user_ids = [user['id'] for user in payload['users']] assert user_ids == ['john', 'manndra'] assert payload['counts'] == { 'exercise_templates': 31, 'measurement_entries': 0, 'routines': 5, 'weight_entries': 0, 'workout_sessions': 0, } john = next(user for user in payload['users'] if user['id'] == 'john') assert john['measurement_system'] == 'imperial' routine_names = [routine['name'] for routine in payload['routines']] assert routine_names == [ 'Monday: Chest', 'Tuesday: Back', 'Wednesday: Arms', 'Thursday: Shoulders', 'Friday: Legs', ] assert payload['routines'][0]['exercises'][0] == { 'exercise_name': 'Bench Press', 'target_sets': 3, 'target_reps': '10', } assert '3 sets of 10 reps' in payload['routines'][0]['notes'] assert 'Weights are still TBD' in payload['routines'][0]['notes'] shoulders = next(routine for routine in payload['routines'] if routine['name'] == 'Thursday: Shoulders') assert shoulders['exercises'][6] == { 'exercise_name': '7-7-7 Shoulder Press', 'target_sets': 1, 'target_reps': '7-7-7', } assert shoulders['exercises'][7] == { 'exercise_name': 'Resistance Band Punch-Out Training', 'target_sets': 3, 'target_reps': '10', } assert 'resistance band punch-out training' in shoulders['notes'] template_names = [template['name'] for template in payload['exercise_templates']] assert template_names == [ 'Resistance Band Crunches', 'Calf Raises', 'Deadlifts', 'Step Ups', 'Leg Extensions', ] def test_updating_profile_and_logging_weight_computes_bmi(tmp_path: Path) -> None: client = make_client(tmp_path) profile_response = client.put( '/api/profile/john', json={ 'display_name': 'John', 'height_inches': 68.5, }, ) assert profile_response.status_code == 200 weight_response = client.post( '/api/profile/john/weight', json={ 'recorded_at': '2026-05-21T08:30:00', 'weight_lbs': 220, 'body_fat_percent': 24.5, }, ) assert weight_response.status_code == 201 saved_entry = weight_response.json() assert saved_entry['user_id'] == 'john' assert saved_entry['weight_lbs'] == 220 assert saved_entry['body_fat_percent'] == 24.5 assert round(saved_entry['bmi'], 1) == 33.0 summary = client.get('/api/dashboard/summary').json() john = next(user for user in summary['users'] if user['id'] == 'john') assert john['current_weight_lbs'] == 220 assert round(john['current_bmi'], 1) == 33.0 def test_logging_measurements_tracks_latest_imperial_body_metrics(tmp_path: Path) -> None: client = make_client(tmp_path) response = client.post( '/api/profile/john/measurements', json={ 'recorded_at': '2026-05-21T08:45:00', 'waist_inches': 42, 'hip_inches': 41, 'chest_inches': 46, 'neck_inches': 17, 'notes': 'Post-op baseline', }, ) assert response.status_code == 201 saved_entry = response.json() assert saved_entry['user_id'] == 'john' assert saved_entry['waist_inches'] == 42 assert saved_entry['hip_inches'] == 41 assert saved_entry['chest_inches'] == 46 assert saved_entry['neck_inches'] == 17 summary = client.get('/api/dashboard/summary').json() john = next(user for user in summary['users'] if user['id'] == 'john') assert summary['counts']['measurement_entries'] == 1 assert john['latest_measurements'] == { 'recorded_at': '2026-05-21T08:45:00', 'waist_inches': 42, 'hip_inches': 41, 'chest_inches': 46, 'neck_inches': 17, } def test_can_create_and_list_exercise_templates(tmp_path: Path) -> None: client = make_client(tmp_path) create_response = client.post( '/api/exercises/templates', json={ 'name': 'Bench Press', 'primary_muscle': 'chest', 'equipment': 'barbell', 'notes': 'Flat bench barbell press.', }, ) assert create_response.status_code == 201 saved_template = create_response.json() assert saved_template['name'] == 'Bench Press' assert saved_template['equipment'] == 'barbell' list_response = client.get('/api/exercises/templates') assert list_response.status_code == 200 payload = list_response.json() assert len(payload) == 32 assert any( item['name'] == 'Bench Press' and item['notes'] == 'Flat bench barbell press.' for item in payload ) summary = client.get('/api/dashboard/summary').json() assert summary['counts']['exercise_templates'] == 32 def test_can_create_routine_before_real_program_arrives(tmp_path: Path) -> None: client = make_client(tmp_path) client.post( '/api/exercises/templates', json={ 'name': 'Bench Press', 'primary_muscle': 'chest', 'equipment': 'barbell', }, ) client.post( '/api/exercises/templates', json={ 'name': 'Incline Dumbbell Press', 'primary_muscle': 'chest', 'equipment': 'dumbbells', }, ) create_response = client.post( '/api/routines', json={ 'user_id': 'john', 'name': 'Push Day Placeholder', 'notes': 'Temporary until John sends the real split.', 'exercises': [ {'exercise_name': 'Bench Press', 'target_sets': 3, 'target_reps': '8-10'}, {'exercise_name': 'Incline Dumbbell Press', 'target_sets': 3, 'target_reps': '10-12'}, ], }, ) assert create_response.status_code == 201 saved_routine = create_response.json() assert saved_routine['user_id'] == 'john' assert saved_routine['name'] == 'Push Day Placeholder' assert len(saved_routine['exercises']) == 2 list_response = client.get('/api/routines') assert list_response.status_code == 200 payload = list_response.json() assert len(payload) == 6 assert any(routine['name'] == 'Push Day Placeholder' for routine in payload) summary = client.get('/api/dashboard/summary').json() assert summary['counts']['routines'] == 6 def test_history_endpoints_return_newest_first(tmp_path: Path) -> None: client = make_client(tmp_path) client.post( '/api/profile/john/weight', json={'recorded_at': '2026-05-20T08:30:00', 'weight_lbs': 222}, ) client.post( '/api/profile/john/weight', json={'recorded_at': '2026-05-21T08:30:00', 'weight_lbs': 220}, ) client.post( '/api/profile/john/measurements', json={'recorded_at': '2026-05-20T08:45:00', 'waist_inches': 43}, ) client.post( '/api/profile/john/measurements', json={'recorded_at': '2026-05-21T08:45:00', 'waist_inches': 42}, ) client.post( '/api/workouts', json={ 'user_id': 'john', 'performed_at': '2026-05-20T09:15:00', 'routine_name': 'Old Workout', 'exercises': [{'exercise_name': 'Bench Press', 'sets': [{'reps': 8, 'weight_lbs': 135}]}], }, ) client.post( '/api/workouts', json={ 'user_id': 'john', 'performed_at': '2026-05-21T09:15:00', 'routine_name': 'New Workout', 'exercises': [{'exercise_name': 'Bench Press', 'sets': [{'reps': 8, 'weight_lbs': 145}]}], }, ) weight_history = client.get('/api/history/john/weights') measurement_history = client.get('/api/history/john/measurements') workout_history = client.get('/api/history/john/workouts') assert weight_history.status_code == 200 assert measurement_history.status_code == 200 assert workout_history.status_code == 200 assert [entry['weight_lbs'] for entry in weight_history.json()] == [220, 222] assert [entry['waist_inches'] for entry in measurement_history.json()] == [42, 43] assert [entry['routine_name'] for entry in workout_history.json()] == ['New Workout', 'Old Workout'] def test_logging_structured_workout_updates_recent_activity(tmp_path: Path) -> None: client = make_client(tmp_path) response = client.post( '/api/workouts', json={ 'user_id': 'john', 'performed_at': '2026-05-21T09:15:00', 'routine_name': 'Push Day', 'notes': 'First pass', 'exercises': [ { 'exercise_name': 'Bench Press', 'sets': [ {'reps': 8, 'weight_lbs': 135, 'rpe': 7.5, 'rest_seconds': 120}, {'reps': 8, 'weight_lbs': 145, 'rpe': 8.0, 'rest_seconds': 120}, ], } ], }, ) assert response.status_code == 201 workout = response.json() assert workout['user_id'] == 'john' assert workout['routine_name'] == 'Push Day' assert workout['exercise_count'] == 1 assert workout['set_count'] == 2 assert workout['total_volume_lbs'] == 2240 summary = client.get('/api/dashboard/summary').json() assert summary['counts']['workout_sessions'] == 1 assert summary['recent_workouts'][0]['routine_name'] == 'Push Day' assert summary['recent_workouts'][0]['total_volume_lbs'] == 2240 def test_progression_endpoint_summarizes_exercise_bests(tmp_path: Path) -> None: client = make_client(tmp_path) client.post( '/api/workouts', json={ 'user_id': 'john', 'performed_at': '2026-05-20T09:15:00', 'routine_name': 'Push Day', 'exercises': [ { 'exercise_name': 'Bench Press', 'sets': [ {'reps': 8, 'weight_lbs': 135}, {'reps': 5, 'weight_lbs': 155}, ], } ], }, ) client.post( '/api/workouts', json={ 'user_id': 'john', 'performed_at': '2026-05-22T09:15:00', 'routine_name': 'Push Day', 'exercises': [ { 'exercise_name': 'Bench Press', 'sets': [ {'reps': 3, 'weight_lbs': 175}, ], }, { 'exercise_name': 'Incline Dumbbell Press', 'sets': [ {'reps': 10, 'weight_lbs': 55}, ], }, ], }, ) response = client.get('/api/progression/john/exercises') assert response.status_code == 200 payload = response.json() assert payload[0]['exercise_name'] == 'Bench Press' assert payload[0]['sessions'] == 2 assert payload[0]['max_weight_lbs'] == 175 assert payload[0]['best_set_estimated_1rm'] == 192.5 assert payload[0]['last_performed_at'] == '2026-05-22T09:15:00' assert payload[1]['exercise_name'] == 'Incline Dumbbell Press' assert payload[1]['sessions'] == 1 def test_homepage_renders_core_sections_and_forms(tmp_path: Path) -> None: client = make_client(tmp_path) response = client.get('/') assert response.status_code == 200 body = response.text assert 'Doris Barbell' in body assert 'Recent workouts' in body assert 'Current body metrics' in body assert 'Log weight' in body assert 'Add exercise template' in body assert 'Current routine board' in body assert 'Monday: Chest' in body def test_seeded_routine_can_render_prefilled_log_sheet(tmp_path: Path) -> None: client = make_client(tmp_path) summary = client.get('/api/dashboard/summary').json() shoulders = next(routine for routine in summary['routines'] if routine['name'] == 'Thursday: Shoulders') response = client.get(f"/routines/{shoulders['id']}/log") assert response.status_code == 200 body = response.text assert 'Thursday: Shoulders' in body assert 'Shoulder Press' in body assert 'Save shoulder day workout' in body assert 'name="exercise_0_name"' in body assert '