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