import Foundation
import Testing
@testable import Workouts

/// Locks `GoalKind`'s persisted-order contract: the stored raw-value list drives the
/// display order, unknown raw values are dropped, and any case missing from the stored
/// list is appended in declaration order — so a newly shipped goal kind surfaces
/// automatically without a migration. A throwaway `UserDefaults` suite keeps each case
/// isolated from the real domain.
struct GoalKindTests {

    /// A fresh suite scoped to one test; cleared on teardown so nothing leaks between runs.
    private func makeDefaults(_ function: String = #function) -> UserDefaults {
        let suite = "GoalKindTests.\(function).\(UUID().uuidString)"
        let defaults = UserDefaults(suiteName: suite)!
        defaults.removePersistentDomain(forName: suite)
        return defaults
    }

    /// With nothing stored, the order is exactly declaration order (all cases present).
    @Test func emptyDefaultsGivesDeclarationOrder() {
        let defaults = makeDefaults()
        #expect(GoalKind.orderedCases(defaults: defaults) == GoalKind.allCases)
        #expect(GoalKind.orderedCases(defaults: defaults) == [.mobility, .cardio, .strength, .mindfulness])
    }

    /// A partial stored list keeps the stored ones first (in stored order), then appends
    /// the missing cases in declaration order — the "new kind appears automatically" rule.
    @Test func partialStoredListPutsStoredFirstThenMissing() {
        let defaults = makeDefaults()
        GoalKind.saveOrder([.mindfulness, .mobility], defaults: defaults)

        let ordered = GoalKind.orderedCases(defaults: defaults)
        #expect(ordered == [.mindfulness, .mobility, .cardio, .strength])
    }

    /// A full stored list is honored verbatim — a user-chosen reorder round-trips.
    @Test func fullStoredListIsHonoredVerbatim() {
        let defaults = makeDefaults()
        let chosen: [GoalKind] = [.cardio, .strength, .mindfulness, .mobility]
        GoalKind.saveOrder(chosen, defaults: defaults)
        #expect(GoalKind.orderedCases(defaults: defaults) == chosen)
    }

    /// Unknown raw values in the stored list are dropped (e.g. a kind removed in a future
    /// version), and the survivors still slot ahead of the missing declaration-order tail.
    @Test func unknownRawValuesAreDropped() {
        let defaults = makeDefaults()
        defaults.set(["mindfulness", "flexibility", "strength"], forKey: GoalKind.orderDefaultsKey)

        let ordered = GoalKind.orderedCases(defaults: defaults)
        #expect(ordered == [.mindfulness, .strength, .mobility, .cardio])
    }

    /// Duplicate raw values collapse to a single entry (first wins), so a corrupt list
    /// never yields a kind twice.
    @Test func duplicateRawValuesCollapse() {
        let defaults = makeDefaults()
        defaults.set(["strength", "strength", "mobility"], forKey: GoalKind.orderDefaultsKey)

        let ordered = GoalKind.orderedCases(defaults: defaults)
        #expect(ordered == [.strength, .mobility, .cardio, .mindfulness])
        #expect(Set(ordered).count == ordered.count)   // no dupes
    }

    /// `saveOrder` persists `[String]` of raw values — the exact shape `orderedCases`
    /// reads back.
    @Test func saveOrderStoresRawValueStrings() {
        let defaults = makeDefaults()
        GoalKind.saveOrder([.mobility, .strength], defaults: defaults)
        #expect(defaults.stringArray(forKey: GoalKind.orderDefaultsKey) == ["mobility", "strength"])
    }

    /// Every case carries a valid, distinct-hued color name `Color.color(from:)` maps
    /// to a non-default color, and stable display metadata.
    @Test func metadataIsPopulatedAndDistinct() {
        #expect(Set(GoalKind.allCases.map(\.colorName)).count == GoalKind.allCases.count)
        #expect(Set(GoalKind.allCases.map(\.systemImage)).count == GoalKind.allCases.count)
        #expect(GoalKind.strength.displayName == "Strength")
        #expect(GoalKind.mindfulness.displayName == "Mindfulness")
        // Each color name is one the palette understands (falls back to indigo otherwise).
        #expect(GoalKind.allCases.allSatisfy { availableColors.contains($0.colorName) })
    }
}
