from __future__ import annotations from datetime import date from garmin_coach_clone.workouts import ( build_dummy_cycling_workout, calendar_entry_date, calendar_entry_id, calendar_entry_name, clone_workout_payload, estimate_duration, existing_clone_names, find_generated_calendar_entry, find_generated_workout, generated_clone_date, generated_calendar_entries, generated_workouts_older_than, generated_workouts, validate_workout_payload, workout_steps_equal, ) def test_dummy_workout_has_expected_duration_and_steps() -> None: workout = build_dummy_cycling_workout("Dummy") assert workout["workoutName"] == "Dummy" assert workout["sportType"]["sportTypeKey"] == "cycling" assert estimate_duration(workout) == 14 * 60 assert validate_workout_payload(workout) == [] def test_clone_payload_strips_ids_and_sets_prefix() -> None: source = build_dummy_cycling_workout("Coach Original") source["workoutId"] = 123 source["ownerId"] = 456 source["workoutSegments"][0]["workoutSteps"][0]["stepId"] = 789 cloned = clone_workout_payload(source, date(2026, 6, 16), "GCClone") assert cloned["workoutName"] == "GCClone 2026-06-16 Coach Original" assert "workoutId" not in cloned assert "ownerId" not in cloned assert "stepId" not in cloned["workoutSegments"][0]["workoutSteps"][0] assert validate_workout_payload(cloned) == [] def test_workout_steps_equal_ignores_ids_but_compares_steps() -> None: left = build_dummy_cycling_workout("Left") right = build_dummy_cycling_workout("Right") left["workoutId"] = 1 right["workoutId"] = 2 left["workoutSegments"][0]["workoutSteps"][0]["stepId"] = 100 right["workoutSegments"][0]["workoutSteps"][0]["stepId"] = 200 assert workout_steps_equal(left, right) right["workoutSegments"][0]["workoutSteps"][0]["endConditionValue"] = 123 assert not workout_steps_equal(left, right) def test_existing_clone_names_filters_by_prefix_and_date() -> None: names = existing_clone_names( [ {"workoutName": "GCClone 2026-06-16 Ride"}, {"workoutName": "GCClone 2026-06-17 Ride"}, {"workoutName": "Other 2026-06-16 Ride"}, ], date(2026, 6, 16), "GCClone", ) assert names == ["GCClone 2026-06-16 Ride"] def test_generated_workouts_filters_by_prefix() -> None: workouts = [ {"workoutId": 1, "workoutName": "GCClone 2026-06-16 Ride"}, {"workoutId": 2, "workoutName": "GCClone Probe Dummy 2026-06-16"}, {"workoutId": 3, "workoutName": "Real Workout"}, ] assert generated_workouts(workouts, "GCClone") == workouts[:2] assert find_generated_workout(workouts, "2", "GCClone") == workouts[1] assert find_generated_workout(workouts, "3", "GCClone") is None def test_generated_clone_date_requires_prefix_and_iso_date() -> None: assert generated_clone_date("GCClone 2026-06-16 Ride", "GCClone") == date(2026, 6, 16) assert generated_clone_date("GCClone Probe Dummy 2026-06-16", "GCClone") is None assert generated_clone_date("Other 2026-06-16 Ride", "GCClone") is None def test_generated_workouts_older_than_filters_by_clone_date() -> None: workouts = [ {"workoutId": 1, "workoutName": "GCClone 2026-06-10 Old"}, {"workoutId": 2, "workoutName": "GCClone 2026-06-11 Cutoff"}, {"workoutId": 3, "workoutName": "GCClone 2026-06-12 New"}, {"workoutId": 4, "workoutName": "GCClone Probe Dummy 2026-06-01"}, ] assert generated_workouts_older_than(workouts, date(2026, 6, 11), "GCClone") == [ workouts[0] ] def test_generated_calendar_entries_handle_nested_workout_names() -> None: generated_entry = { "scheduledWorkoutId": 99, "scheduledDate": "2026-06-16", "workout": {"workoutName": "GCClone 2026-06-16 Ride"}, } data = { "calendarItems": [ generated_entry, { "scheduledWorkoutId": 100, "scheduledDate": "2026-06-16", "workout": {"workoutName": "Real Workout"}, }, ] } entries = generated_calendar_entries(data, "GCClone") assert entries == [generated_entry] assert find_generated_calendar_entry(data, "99", "GCClone") == generated_entry assert find_generated_calendar_entry(data, "100", "GCClone") is None assert calendar_entry_id(generated_entry) == "99" assert calendar_entry_date(generated_entry) == "2026-06-16" assert calendar_entry_name(generated_entry) == "GCClone 2026-06-16 Ride" def test_generated_calendar_entries_accept_id_as_calendar_entry_id() -> None: generated_entry = { "id": 99, "scheduledDate": "2026-06-16", "workout": {"workoutName": "GCClone 2026-06-16 Ride"}, } data = {"calendarItems": [generated_entry]} assert generated_calendar_entries(data, "GCClone") == [generated_entry] assert calendar_entry_id(generated_entry) == "99" def test_validate_workout_payload_rejects_missing_segments() -> None: errors = validate_workout_payload( { "workoutName": "Broken", "sportType": {"sportTypeKey": "cycling"}, "workoutSegments": [], } ) assert "workoutSegments must be a non-empty list" in errors def test_validate_workout_payload_rejects_non_cycling() -> None: workout = build_dummy_cycling_workout("Broken") workout["sportType"] = {"sportTypeKey": "running"} errors = validate_workout_payload(workout) assert "sportType must be cycling, got running" in errors def test_validate_workout_payload_rejects_missing_step_target() -> None: workout = build_dummy_cycling_workout("Broken") workout["workoutSegments"][0]["workoutSteps"][0].pop("targetType") errors = validate_workout_payload(workout) assert "workoutSegments[1].workoutSteps[1].targetType is missing" in errors