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)
    }
}
