import Foundation
import Testing
import IndieSync
@testable import Workouts

/// Round-trips `RoutineDocument` (and the embedded exercise / log shapes) through
/// `DocumentCoder` — the exact JSON codec (ISO-8601 dates, sorted keys,
/// pretty-printed) `DocumentFileStore` uses to write and read every document in
/// this app's iCloud container. Also pins the schema-v2 migration: an old v1
/// routine file (with the removed weight-reminder / category keys) must still decode
/// and round-trip at the current version.
struct RoutineDocumentCodableTests {
    /// A whole-second epoch so ISO-8601 round-trips (no fractional-second loss).
    private static let stableTimestamp = Date(timeIntervalSince1970: 1_700_000_000)

    @Test func encodeDecodeRoundTrip() throws {
        let exercise = ExerciseDocument(
            id: ULID.make(),
            name: "Leg Press",
            order: 0,
            sets: 4,
            reps: 10,
            weight: 135,
            loadType: 0,
            durationSeconds: 0,
            machineSettings: [
                MachineSetting(name: "Seat Height", value: "4"),
                MachineSetting(name: "Back Rest Incline", value: "45°")
            ]
        )
        let original = RoutineDocument(
            schemaVersion: RoutineDocument.currentSchemaVersion,
            id: ULID.make(),
            name: "Push Day",
            color: "blue",
            systemImage: "dumbbell.fill",
            order: 0,
            createdAt: Self.stableTimestamp,
            updatedAt: Self.stableTimestamp,
            exercises: [exercise],
            activityType: nil
        )

        let data = try DocumentCoder.encode(original)
        let decoded = try DocumentCoder.decode(RoutineDocument.self, from: data)

        #expect(decoded == original)
        #expect(decoded.schemaVersion == 3)
        // PINNED: routine documents live under the on-disk "Splits/" directory.
        #expect(decoded.relativePath == "Splits/\(original.id).json")
        #expect(decoded.exercises.first?.machineSettings?.count == 2)
    }

    /// An `ExerciseDocument` with no machine settings encodes without a
    /// `machineSettings` key (nil optionals are omitted) and decodes back to nil —
    /// the "not a machine exercise" state.
    @Test func nonMachineExerciseOmitsMachineSettings() throws {
        let exercise = ExerciseDocument(
            id: ULID.make(), name: "Plank", order: 0, sets: 3, reps: 0,
            weight: 0, loadType: 2, durationSeconds: 45
        )
        let data = try DocumentCoder.encode(exercise)
        let json = String(decoding: data, as: UTF8.self)
        #expect(!json.contains("machineSettings"))

        let decoded = try DocumentCoder.decode(ExerciseDocument.self, from: data)
        #expect(decoded.machineSettings == nil)
    }

    /// `machineSettings` round-trips on `ExerciseDocument`, including the empty case
    /// ("machine-based, nothing recorded yet") which must stay distinct from nil.
    @Test func machineSettingsRoundTripOnExercise() throws {
        for settings: [MachineSetting] in [
            [MachineSetting(name: "Pin", value: "3rd hole")],
            []  // empty ≠ nil: a machine exercise with nothing recorded
        ] {
            let exercise = ExerciseDocument(
                id: ULID.make(), name: "Chest Press", order: 0, sets: 4, reps: 10,
                weight: 40, loadType: 1, durationSeconds: 0, machineSettings: settings
            )
            let data = try DocumentCoder.encode(exercise)
            let decoded = try DocumentCoder.decode(ExerciseDocument.self, from: data)
            #expect(decoded.machineSettings == settings)
        }
    }

    /// `machineSettings` round-trips on `WorkoutLogDocument` — the plan-time snapshot.
    @Test func machineSettingsRoundTripOnWorkoutLog() throws {
        let log = WorkoutLogDocument(
            id: ULID.make(), exerciseName: "Leg Press", order: 0, sets: 4, reps: 10,
            weight: 140, loadType: 1, durationSeconds: 0, currentStateIndex: 0,
            status: WorkoutStatus.notStarted.rawValue, notes: nil,
            date: Self.stableTimestamp,
            machineSettings: [MachineSetting(name: "Seat Height", value: "4")]
        )
        let data = try DocumentCoder.encode(log)
        let decoded = try DocumentCoder.decode(WorkoutLogDocument.self, from: data)
        #expect(decoded == log)
        #expect(decoded.machineSettings == [MachineSetting(name: "Seat Height", value: "4")])
    }

    /// A v1 routine file carrying the since-removed keys (`weightLastUpdated`,
    /// `weightReminderWeeks`, `category`) must still decode — Codable ignores the
    /// extra keys, every remaining field is present in old files, and the integer
    /// `weight` decodes natively into today's `Double`. Re-stamping `schemaVersion`
    /// to the current value (what the app does on any rewrite via the
    /// cache→document mapper) round-trips cleanly at the current version.
    @Test func decodesLegacyV1RoutineAndRoundTripsAtCurrent() throws {
        let json = """
        {
            "schemaVersion": 1,
            "id": "01HZZZZZZZZZZZZZZZZZZZZZZZ",
            "name": "Push Day",
            "color": "blue",
            "systemImage": "dumbbell.fill",
            "order": 0,
            "createdAt": "2023-11-14T22:13:20Z",
            "updatedAt": "2023-11-14T22:13:20Z",
            "exercises": [{
                "id": "01HZZZZZZZZZZZZZZZZZZZZZZY",
                "name": "Bench Press",
                "order": 0,
                "sets": 4,
                "reps": 10,
                "weight": 135,
                "loadType": 1,
                "durationSeconds": 0,
                "weightLastUpdated": "2023-11-14T22:13:20Z",
                "weightReminderWeeks": 4,
                "category": 1
            }]
        }
        """
        var decoded = try DocumentCoder.decode(RoutineDocument.self, from: Data(json.utf8))
        // Removed keys are ignored; the surviving fields decode and machineSettings
        // defaults to nil (the file predates it).
        #expect(decoded.schemaVersion == 1)
        #expect(decoded.exercises.count == 1)
        #expect(decoded.exercises.first?.name == "Bench Press")
        #expect(decoded.exercises.first?.weight == 135)
        #expect(decoded.exercises.first?.machineSettings == nil)

        // The app re-stamps to the current version on rewrite; that must round-trip.
        decoded.schemaVersion = RoutineDocument.currentSchemaVersion
        let reData = try DocumentCoder.encode(decoded)
        let reDecoded = try DocumentCoder.decode(RoutineDocument.self, from: reData)
        #expect(reDecoded == decoded)
        #expect(reDecoded.schemaVersion == 3)
    }

    /// A workout-log file written before `completed` was removed still carries the
    /// key. It must decode fine (Codable ignores it) — completion is now derived
    /// from `status`.
    @Test func decodesLegacyWorkoutLogWithCompletedKey() throws {
        let json = """
        {
            "id": "01HZZZZZZZZZZZZZZZZZZZZZZW",
            "exerciseName": "Bench Press",
            "order": 0,
            "sets": 4,
            "reps": 10,
            "weight": 135,
            "loadType": 1,
            "durationSeconds": 0,
            "currentStateIndex": 4,
            "completed": true,
            "status": "completed",
            "date": "2023-11-14T22:13:20Z"
        }
        """
        let decoded = try DocumentCoder.decode(WorkoutLogDocument.self, from: Data(json.utf8))
        #expect(decoded.status == WorkoutStatus.completed.rawValue)
        #expect(decoded.exerciseName == "Bench Press")
        #expect(decoded.machineSettings == nil)
    }
}
