import Foundation
import SwiftData
import Testing
import IndieSync
@testable import Workouts

/// Locks the bundled starter-routine library end to end: the catalog decodes and
/// orders correctly, the pinned seed ids never drift, a pristine save re-emits
/// byte-identical JSON (no phantom iCloud churn), every seed passes the
/// forward-compatibility gate, and `isPristine` tracks edits correctly through
/// both the raw document and a SwiftData cache round-trip.
struct SeedLibraryTests {

    @Test func catalogLoadsFourteenSeedsInOrder() {
        let seeds = SeedLibrary.seeds
        #expect(seeds.count == 14)
        #expect(seeds.map(\.doc.order) == Array(0...13))
        #expect(seeds.map(\.doc.name) == [
            "Upper Body", "Core", "Lower Body", "Bodyweight Core",
            "Full Body Machines", "Free Weight Basics",
            "Upper Body Warm-Up", "Lower Body Warm-Up",
            "Morning Wake-Up", "Morning Mobility",
            "Full Body Stretch", "Evening Stretch", "Cardio", "Meditation",
        ])
    }

    /// These ids are minted from a frozen 2020 timestamp and are already shipped
    /// inside installed app bundles. Changing any of them breaks cross-device
    /// convergence (two devices would mint different ids for "the same" starter
    /// routine) and voids the permanent-tombstone exemption that lets an
    /// edited-then-deleted seed's stub veto resurrection forever — regenerate
    /// only with extreme care, and never for a seed that has already shipped.
    @Test func seedIDsArePinned() {
        #expect(SeedLibrary.seedIDs == [
            "01DXF6DT0038BDC2WC3EVX8ZJ5", // Upper Body
            "01DXF6DT001MA0TM7FHJZT098Z", // Core
            "01DXF6DT006QRF1PMGK17FV505", // Lower Body
            "01DXF6DT00RV99WG172YFW4NKA", // Bodyweight Core
            "01DXF6DT00SZ0NPXPN36415NKM", // Full Body Machines
            "01DXF6DT00WY3HMCH6E841YQE7", // Free Weight Basics
            "01DXF6DT00G56WC0HS6RJWMFMD", // Upper Body Warm-Up
            "01DXF6DT003QB4ASSR8JB1JT6A", // Lower Body Warm-Up
            "01DXF6DT00FJ5F1HRENQPC1DTG", // Morning Wake-Up
            "01DXF6DT00QHJ9G4SDMEX64NZD", // Morning Mobility
            "01DXF6DT003PRJB3FGYSSGTQ5X", // Full Body Stretch
            "01DXF6DT00M5YRFPHJQ2FS2B9P", // Evening Stretch
            "01DXF6DT00BXJ6N63ZBX9M9VNH", // Cardio
            "01DXF6DT00ANFJR7GX9G808G4M", // Meditation
        ])
    }

    /// A pristine save must re-emit the exact bundle bytes, or iCloud would see a
    /// modification on every install/upgrade that touches a never-edited seed.
    @Test func encodingReproducesBundleBytesExactly() throws {
        for seed in SeedLibrary.seeds {
            #expect(try DocumentCoder.encode(seed.doc) == seed.data)
        }
    }

    @Test func allSeedsAreReadableAtCurrentSchemaVersion() {
        for seed in SeedLibrary.seeds {
            #expect(seed.doc.isReadable)
            #expect(seed.doc.schemaVersion == RoutineDocument.currentSchemaVersion)
        }
    }

    @Test func pristineSeedsAreThemselvesPristine() {
        for seed in SeedLibrary.seeds {
            #expect(SeedLibrary.isPristine(seed.doc))
        }
    }

    /// Edit sheets stamp `updatedAt` on every Save, so a no-op save must not read
    /// as an edit — otherwise saving without changing anything would fork the seed.
    @Test func pristineIgnoresUpdatedAtOnlyChange() throws {
        let seed = try #require(SeedLibrary.seeds.first)
        var edited = seed.doc
        edited.updatedAt = Date()
        #expect(SeedLibrary.isPristine(edited))
    }

    @Test func pristineDetectsExerciseWeightChange() throws {
        let seed = try #require(SeedLibrary.seeds.first)
        var edited = seed.doc
        edited.exercises[0].weight += 5
        #expect(!SeedLibrary.isPristine(edited))
    }

    @Test func pristineDetectsNameChange() throws {
        let seed = try #require(SeedLibrary.seeds.first)
        var edited = seed.doc
        edited.name = "Renamed"
        #expect(!SeedLibrary.isPristine(edited))
    }

    /// Dragging a starter to reorder the Library list writes a new `order` to it. Order
    /// is presentation, not content, so a reorder must NOT fork the seed — an order-only
    /// change reads as pristine.
    @Test func pristineIgnoresOrderOnlyChange() throws {
        let seed = try #require(SeedLibrary.seeds.first)
        var edited = seed.doc
        edited.order += 5
        #expect(SeedLibrary.isPristine(edited))
    }

    /// A reorder save also stamps `updatedAt`; order + updatedAt together must still read
    /// as pristine so a drag never forks a starter.
    @Test func pristineIgnoresOrderAndUpdatedAtChange() throws {
        let seed = try #require(SeedLibrary.seeds.first)
        var edited = seed.doc
        edited.order += 3
        edited.updatedAt = Date()
        #expect(SeedLibrary.isPristine(edited))
    }

    /// Order tolerance must not mask a genuine content edit: a real change (here, a
    /// different rest length) is still detected even when `order` also differs.
    @Test func pristineDetectsContentChangeDespiteOrderChange() throws {
        let seed = try #require(SeedLibrary.seeds.first)
        var edited = seed.doc
        edited.order += 2
        edited.restSeconds = (seed.doc.restSeconds ?? 60) + 30
        #expect(!SeedLibrary.isPristine(edited))
    }

    /// A non-seed id has no seed to compare against; `isPristine` defaults to
    /// true since callers are expected to gate on `isSeed` before asking.
    @Test func pristineDefaultsTrueForNonSeedID() throws {
        let seed = try #require(SeedLibrary.seeds.first)
        var notASeed = seed.doc
        notASeed.id = ULID.make()
        #expect(SeedLibrary.isPristine(notASeed))
    }

    /// Round-trips every seed through the SwiftData cache the way the metadata
    /// observer would (document → `CacheMapper.upsertRoutine` → entity), then
    /// rebuilds a document from that entity the way the write path would. The
    /// result must still read as pristine, or every user's very first cache
    /// rebuild would silently fork their starter routines.
    @MainActor
    @Test func mapperRoundTripPreservesPristineness() throws {
        let schema = Schema([Routine.self, Exercise.self, Workout.self, WorkoutLog.self])
        // cloudKitDatabase: .none matches WorkoutsModelContainer — without it SwiftData
        // applies CloudKit schema validation (all relationships optional), which the
        // cache entities intentionally don't satisfy.
        let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true, cloudKitDatabase: .none)
        let container = try ModelContainer(for: schema, configurations: [config])
        let context = ModelContext(container)

        for seed in SeedLibrary.seeds {
            CacheMapper.upsertRoutine(seed.doc, relativePath: seed.doc.relativePath, into: context)
            let entity = try #require(CacheMapper.fetchRoutine(id: seed.id, in: context))
            let rebuilt = RoutineDocument(from: entity)
            #expect(SeedLibrary.isPristine(rebuilt))
        }
    }
}
