import Foundation import Testing @testable import Workouts /// Locks `ScheduleRepairPlanner`'s pure decision rules — schedule/routine cache /// snapshots in, [(scheduleID, newRoutineID)] out. The engine's apply pass (file /// rewrite + cache upsert + single notify) is deliberately untested here; the /// planner carries all the judgment. struct ScheduleRepairPlannerTests { private func schedule(id: String = "S1", routineID: String, routineName: String) -> ScheduleRepairRef { ScheduleRepairRef(id: id, routineID: routineID, routineName: routineName) } private func routine(id: String, name: String) -> RoutineRepairRef { RoutineRepairRef(id: id, name: name) } // MARK: - Repairs @Test func brokenReferenceWithUniqueNameMatchIsRepaired() { let repairs = ScheduleRepairPlanner.plans( schedules: [schedule(routineID: "DEAD-SEED", routineName: "Cardio")], liveRoutines: [routine(id: "CLONE-1", name: "Cardio"), routine(id: "R-2", name: "Upper Body")]) #expect(repairs == [ScheduleRepair(scheduleID: "S1", newRoutineID: "CLONE-1")]) } @Test func multipleBrokenSchedulesEachRepairToTheirOwnNameMatch() { let repairs = ScheduleRepairPlanner.plans( schedules: [ schedule(id: "S1", routineID: "DEAD-A", routineName: "Cardio"), schedule(id: "S2", routineID: "DEAD-B", routineName: "Upper Body"), ], liveRoutines: [routine(id: "R-1", name: "Cardio"), routine(id: "R-2", name: "Upper Body")]) #expect(repairs == [ ScheduleRepair(scheduleID: "S1", newRoutineID: "R-1"), ScheduleRepair(scheduleID: "S2", newRoutineID: "R-2"), ]) } // MARK: - Left alone @Test func brokenReferenceWithNoNameMatchIsUntouched() { let repairs = ScheduleRepairPlanner.plans( schedules: [schedule(routineID: "DEAD-SEED", routineName: "Cardio")], liveRoutines: [routine(id: "R-2", name: "Upper Body")]) #expect(repairs.isEmpty) } @Test func brokenReferenceWithTwoSameNameLiveRoutinesIsUntouched() { // Two live routines both named "Cardio" — the match would be a guess, so the // schedule is left for the edit form's "no longer available" flow. let repairs = ScheduleRepairPlanner.plans( schedules: [schedule(routineID: "DEAD-SEED", routineName: "Cardio")], liveRoutines: [routine(id: "R-1", name: "Cardio"), routine(id: "R-2", name: "Cardio")]) #expect(repairs.isEmpty) } @Test func healthyReferenceIsUntouchedEvenWhenAnotherRoutineSharesTheName() { // The schedule's routineID resolves fine — a same-name routine elsewhere must // not lure it away. let repairs = ScheduleRepairPlanner.plans( schedules: [schedule(routineID: "R-1", routineName: "Cardio")], liveRoutines: [routine(id: "R-1", name: "Cardio"), routine(id: "R-9", name: "Cardio")]) #expect(repairs.isEmpty) } // MARK: - Degenerate inputs @Test func emptyInputsProduceNoRepairs() { #expect(ScheduleRepairPlanner.plans(schedules: [], liveRoutines: []).isEmpty) #expect(ScheduleRepairPlanner.plans( schedules: [], liveRoutines: [routine(id: "R-1", name: "Cardio")]).isEmpty) // Broken schedule, no live routines at all → nothing to repair to. #expect(ScheduleRepairPlanner.plans( schedules: [schedule(routineID: "DEAD", routineName: "Cardio")], liveRoutines: []).isEmpty) } }