From 1703ca5b0f5ff224d3bc8ff79e2d7d84f72f33c8 Mon Sep 17 00:00:00 2001 From: Trey t Date: Mon, 12 Jan 2026 09:24:33 -0600 Subject: [PATCH] refactor: change domain model IDs from UUID to String canonical IDs This refactor fixes the achievement system by using stable canonical string IDs (e.g., "stadium_mlb_fenway_park") instead of random UUIDs. This ensures stadium mappings for achievements are consistent across app launches and CloudKit sync operations. Changes: - Stadium, Team, Game: id property changed from UUID to String - Trip, TripStop, TripPreferences: updated to use String IDs for games/stadiums - CKModels: removed UUID parsing, use canonical IDs directly - AchievementEngine: now matches against canonical stadium IDs - All test files updated to use String IDs instead of UUID() Co-Authored-By: Claude Opus 4.5 --- .../Core/Models/CloudKit/CKModels.swift | 33 ++-- .../Domain/AchievementDefinitions.swift | 6 +- SportsTime/Core/Models/Domain/Game.swift | 18 +- SportsTime/Core/Models/Domain/Stadium.swift | 4 +- SportsTime/Core/Models/Domain/Team.swift | 8 +- SportsTime/Core/Models/Domain/Trip.swift | 2 +- .../Core/Models/Domain/TripPreferences.swift | 8 +- SportsTime/Core/Models/Domain/TripStop.swift | 8 +- .../Core/Models/Local/CanonicalModels.swift | 18 +- SportsTime/Core/Models/Local/SavedTrip.swift | 10 +- .../Core/Models/Local/StadiumProgress.swift | 21 +-- .../Core/Services/AchievementEngine.swift | 84 +++------- .../Core/Services/CloudKitService.swift | 39 +++-- SportsTime/Core/Services/DataProvider.swift | 75 ++------- SportsTime/Core/Services/GameMatcher.swift | 16 +- .../Services/RouteDescriptionGenerator.swift | 2 +- .../Services/StadiumProximityMatcher.swift | 2 +- .../Services/SuggestedTripsGenerator.swift | 32 ++-- SportsTime/Export/PDFGenerator.swift | 16 +- .../Export/Services/PDFAssetPrefetcher.swift | 10 +- .../Export/Services/RemoteImageService.swift | 12 +- .../ViewModels/PhotoImportViewModel.swift | 6 +- .../ViewModels/ProgressViewModel.swift | 12 +- .../Progress/Views/ProgressMapView.swift | 2 +- .../Progress/Views/StadiumVisitSheet.swift | 3 +- .../Progress/Views/VisitDetailView.swift | 2 +- .../ViewModels/TripCreationViewModel.swift | 10 +- .../Trip/Views/TimelineItemView.swift | 12 +- .../Trip/Views/TripCreationView.swift | 24 +-- .../Features/Trip/Views/TripDetailView.swift | 2 +- .../Planning/Engine/GameDAGRouter.swift | 24 +-- .../Planning/Engine/ScenarioAPlanner.swift | 12 +- .../Planning/Engine/ScenarioBPlanner.swift | 14 +- .../Planning/Engine/ScenarioCPlanner.swift | 22 +-- .../Planning/Engine/ScenarioDPlanner.swift | 14 +- .../Planning/Engine/ScenarioPlanner.swift | 2 +- .../Planning/Models/PlanningModels.swift | 6 +- .../Fixtures/FixtureGenerator.swift | 44 ++--- .../Helpers/BruteForceRouteVerifier.swift | 32 ++-- .../Mocks/MockAppDataProvider.swift | 12 +- .../Mocks/MockCloudKitService.swift | 16 +- .../Planning/ConcurrencyTests.swift | 16 +- SportsTimeTests/Planning/EdgeCaseTests.swift | 54 +++--- .../Planning/GameDAGRouterTests.swift | 88 +++++----- .../Planning/ItineraryBuilderTests.swift | 26 +-- .../Planning/RouteFiltersTests.swift | 4 +- .../Planning/ScenarioAPlannerTests.swift | 70 ++++---- .../Planning/ScenarioBPlannerTests.swift | 62 +++---- .../Planning/ScenarioCPlannerTests.swift | 76 ++++----- .../Planning/ScenarioDPlannerTests.swift | 158 +++++++++--------- .../Planning/TripPlanningEngineTests.swift | 62 +++---- .../Progress/AchievementEngineTests.swift | 42 ++--- SportsTimeTests/SportsTimeTests.swift | 16 +- 53 files changed, 642 insertions(+), 727 deletions(-) diff --git a/SportsTime/Core/Models/CloudKit/CKModels.swift b/SportsTime/Core/Models/CloudKit/CKModels.swift index f434b28..2378d36 100644 --- a/SportsTime/Core/Models/CloudKit/CKModels.swift +++ b/SportsTime/Core/Models/CloudKit/CKModels.swift @@ -43,7 +43,7 @@ struct CKTeam { init(team: Team, stadiumRecordID: CKRecord.ID) { let record = CKRecord(recordType: CKRecordType.team) - record[CKTeam.idKey] = team.id.uuidString + record[CKTeam.idKey] = team.id record[CKTeam.nameKey] = team.name record[CKTeam.abbreviationKey] = team.abbreviation record[CKTeam.sportKey] = team.sport.rawValue @@ -67,8 +67,8 @@ struct CKTeam { var team: Team? { // Use teamId field, or fall back to record name - let idString = (record[CKTeam.idKey] as? String) ?? record.recordID.recordName - guard let id = UUID(uuidString: idString), + let id = (record[CKTeam.idKey] as? String) ?? record.recordID.recordName + guard !id.isEmpty, let abbreviation = record[CKTeam.abbreviationKey] as? String, let sportRaw = record[CKTeam.sportKey] as? String, let sport = Sport(rawValue: sportRaw), @@ -78,14 +78,13 @@ struct CKTeam { // Name defaults to abbreviation if not provided let name = record[CKTeam.nameKey] as? String ?? abbreviation - // Stadium reference is optional - use placeholder UUID if not present - let stadiumId: UUID - if let stadiumRef = record[CKTeam.stadiumRefKey] as? CKRecord.Reference, - let refId = UUID(uuidString: stadiumRef.recordID.recordName) { - stadiumId = refId + // Stadium reference is optional - use placeholder string if not present + let stadiumId: String + if let stadiumRef = record[CKTeam.stadiumRefKey] as? CKRecord.Reference { + stadiumId = stadiumRef.recordID.recordName } else { - // Generate deterministic placeholder from team ID - stadiumId = UUID() + // Generate placeholder from team ID + stadiumId = "stadium_placeholder_\(id)" } let logoURL = (record[CKTeam.logoURLKey] as? String).flatMap { URL(string: $0) } @@ -126,7 +125,7 @@ struct CKStadium { init(stadium: Stadium) { let record = CKRecord(recordType: CKRecordType.stadium) - record[CKStadium.idKey] = stadium.id.uuidString + record[CKStadium.idKey] = stadium.id record[CKStadium.nameKey] = stadium.name record[CKStadium.cityKey] = stadium.city record[CKStadium.stateKey] = stadium.state @@ -145,8 +144,8 @@ struct CKStadium { var stadium: Stadium? { // Use stadiumId field, or fall back to record name - let idString = (record[CKStadium.idKey] as? String) ?? record.recordID.recordName - guard let id = UUID(uuidString: idString), + let id = (record[CKStadium.idKey] as? String) ?? record.recordID.recordName + guard !id.isEmpty, let name = record[CKStadium.nameKey] as? String, let city = record[CKStadium.cityKey] as? String else { return nil } @@ -199,7 +198,7 @@ struct CKGame { init(game: Game, homeTeamRecordID: CKRecord.ID, awayTeamRecordID: CKRecord.ID, stadiumRecordID: CKRecord.ID) { let record = CKRecord(recordType: CKRecordType.game) - record[CKGame.idKey] = game.id.uuidString + record[CKGame.idKey] = game.id record[CKGame.homeTeamRefKey] = CKRecord.Reference(recordID: homeTeamRecordID, action: .none) record[CKGame.awayTeamRefKey] = CKRecord.Reference(recordID: awayTeamRecordID, action: .none) record[CKGame.stadiumRefKey] = CKRecord.Reference(recordID: stadiumRecordID, action: .none) @@ -231,9 +230,9 @@ struct CKGame { record[CKGame.stadiumCanonicalIdKey] as? String } - func game(homeTeamId: UUID, awayTeamId: UUID, stadiumId: UUID) -> Game? { - guard let idString = record[CKGame.idKey] as? String, - let id = UUID(uuidString: idString), + func game(homeTeamId: String, awayTeamId: String, stadiumId: String) -> Game? { + let id = (record[CKGame.idKey] as? String) ?? record.recordID.recordName + guard !id.isEmpty, let dateTime = record[CKGame.dateTimeKey] as? Date, let sportRaw = record[CKGame.sportKey] as? String, let sport = Sport(rawValue: sportRaw), diff --git a/SportsTime/Core/Models/Domain/AchievementDefinitions.swift b/SportsTime/Core/Models/Domain/AchievementDefinitions.swift index 12fe833..d02f70b 100644 --- a/SportsTime/Core/Models/Domain/AchievementDefinitions.swift +++ b/SportsTime/Core/Models/Domain/AchievementDefinitions.swift @@ -591,7 +591,7 @@ enum AchievementRegistry { sport: .mlb, iconName: "building.columns.fill", iconColor: .green, - requirement: .specificStadium("stadium_mlb_bos"), + requirement: .specificStadium("stadium_mlb_fenway_park"), sortOrder: 600 ), AchievementDefinition( @@ -602,7 +602,7 @@ enum AchievementRegistry { sport: .mlb, iconName: "leaf.fill", iconColor: .green, - requirement: .specificStadium("stadium_mlb_chc"), + requirement: .specificStadium("stadium_mlb_wrigley_field"), sortOrder: 601 ), AchievementDefinition( @@ -613,7 +613,7 @@ enum AchievementRegistry { sport: .nba, iconName: "sparkles", iconColor: .orange, - requirement: .specificStadium("stadium_nba_nyk"), + requirement: .specificStadium("stadium_nba_madison_square_garden"), sortOrder: 602 ) ] diff --git a/SportsTime/Core/Models/Domain/Game.swift b/SportsTime/Core/Models/Domain/Game.swift index 752469c..103c772 100644 --- a/SportsTime/Core/Models/Domain/Game.swift +++ b/SportsTime/Core/Models/Domain/Game.swift @@ -6,10 +6,10 @@ import Foundation struct Game: Identifiable, Codable, Hashable { - let id: UUID - let homeTeamId: UUID - let awayTeamId: UUID - let stadiumId: UUID + let id: String // Canonical ID: "game_mlb_2026_bos_nyy_0401" + let homeTeamId: String // FK: "team_mlb_bos" + let awayTeamId: String // FK: "team_mlb_nyy" + let stadiumId: String // FK: "stadium_mlb_fenway_park" let dateTime: Date let sport: Sport let season: String @@ -17,10 +17,10 @@ struct Game: Identifiable, Codable, Hashable { let broadcastInfo: String? init( - id: UUID , - homeTeamId: UUID, - awayTeamId: UUID, - stadiumId: UUID, + id: String, + homeTeamId: String, + awayTeamId: String, + stadiumId: String, dateTime: Date, sport: Sport, season: String, @@ -78,7 +78,7 @@ struct RichGame: Identifiable, Hashable, Codable { let awayTeam: Team let stadium: Stadium - var id: UUID { game.id } + var id: String { game.id } var matchupDescription: String { "\(awayTeam.abbreviation) @ \(homeTeam.abbreviation)" diff --git a/SportsTime/Core/Models/Domain/Stadium.swift b/SportsTime/Core/Models/Domain/Stadium.swift index 254d5d5..4a4a854 100644 --- a/SportsTime/Core/Models/Domain/Stadium.swift +++ b/SportsTime/Core/Models/Domain/Stadium.swift @@ -7,7 +7,7 @@ import Foundation import CoreLocation struct Stadium: Identifiable, Codable, Hashable { - let id: UUID + let id: String // Canonical ID: "stadium_mlb_fenway_park" let name: String let city: String let state: String @@ -19,7 +19,7 @@ struct Stadium: Identifiable, Codable, Hashable { let imageURL: URL? init( - id: UUID, + id: String, name: String, city: String, state: String, diff --git a/SportsTime/Core/Models/Domain/Team.swift b/SportsTime/Core/Models/Domain/Team.swift index 4dc982e..6779a22 100644 --- a/SportsTime/Core/Models/Domain/Team.swift +++ b/SportsTime/Core/Models/Domain/Team.swift @@ -6,23 +6,23 @@ import Foundation struct Team: Identifiable, Codable, Hashable { - let id: UUID + let id: String // Canonical ID: "team_mlb_bos" let name: String let abbreviation: String let sport: Sport let city: String - let stadiumId: UUID + let stadiumId: String // FK: "stadium_mlb_fenway_park" let logoURL: URL? let primaryColor: String? let secondaryColor: String? init( - id: UUID, + id: String, name: String, abbreviation: String, sport: Sport, city: String, - stadiumId: UUID, + stadiumId: String, logoURL: URL? = nil, primaryColor: String? = nil, secondaryColor: String? = nil diff --git a/SportsTime/Core/Models/Domain/Trip.swift b/SportsTime/Core/Models/Domain/Trip.swift index 22b4d33..75fa449 100644 --- a/SportsTime/Core/Models/Domain/Trip.swift +++ b/SportsTime/Core/Models/Domain/Trip.swift @@ -174,7 +174,7 @@ struct ItineraryDay: Identifiable, Hashable { var isRestDay: Bool { stops.first?.isRestDay ?? false } var hasTravelSegment: Bool { !travelSegments.isEmpty } - var gameIds: [UUID] { stops.flatMap { $0.games } } + var gameIds: [String] { stops.flatMap { $0.games } } var hasGames: Bool { !gameIds.isEmpty } var primaryCity: String? { stops.first?.city } var totalDrivingHours: Double { travelSegments.reduce(0) { $0 + $1.durationHours } } diff --git a/SportsTime/Core/Models/Domain/TripPreferences.swift b/SportsTime/Core/Models/Domain/TripPreferences.swift index 7bcd354..a428887 100644 --- a/SportsTime/Core/Models/Domain/TripPreferences.swift +++ b/SportsTime/Core/Models/Domain/TripPreferences.swift @@ -215,7 +215,7 @@ struct TripPreferences: Codable, Hashable { var startLocation: LocationInput? var endLocation: LocationInput? var sports: Set - var mustSeeGameIds: Set + var mustSeeGameIds: Set var travelMode: TravelMode var startDate: Date var endDate: Date @@ -235,7 +235,7 @@ struct TripPreferences: Codable, Hashable { var selectedRegions: Set /// Team to follow (for Follow Team mode) - var followTeamId: UUID? + var followTeamId: String? /// Whether to start/end from a home location (vs fly-in/fly-out) var useHomeLocation: Bool @@ -248,7 +248,7 @@ struct TripPreferences: Codable, Hashable { startLocation: LocationInput? = nil, endLocation: LocationInput? = nil, sports: Set = [], - mustSeeGameIds: Set = [], + mustSeeGameIds: Set = [], travelMode: TravelMode = .drive, startDate: Date = Date(), endDate: Date = Date().addingTimeInterval(86400 * 7), @@ -264,7 +264,7 @@ struct TripPreferences: Codable, Hashable { maxDrivingHoursPerDriver: Double? = nil, allowRepeatCities: Bool = true, selectedRegions: Set = [.east, .central, .west], - followTeamId: UUID? = nil, + followTeamId: String? = nil, useHomeLocation: Bool = true, gameFirstTripDuration: Int = 7 ) { diff --git a/SportsTime/Core/Models/Domain/TripStop.swift b/SportsTime/Core/Models/Domain/TripStop.swift index 7a36194..650d5ee 100644 --- a/SportsTime/Core/Models/Domain/TripStop.swift +++ b/SportsTime/Core/Models/Domain/TripStop.swift @@ -14,8 +14,8 @@ struct TripStop: Identifiable, Codable, Hashable { let coordinate: CLLocationCoordinate2D? let arrivalDate: Date let departureDate: Date - let games: [UUID] - let stadium: UUID? + let games: [String] + let stadium: String? let lodging: LodgingSuggestion? let activities: [ActivitySuggestion] let isRestDay: Bool @@ -29,8 +29,8 @@ struct TripStop: Identifiable, Codable, Hashable { coordinate: CLLocationCoordinate2D? = nil, arrivalDate: Date, departureDate: Date, - games: [UUID] = [], - stadium: UUID? = nil, + games: [String] = [], + stadium: String? = nil, lodging: LodgingSuggestion? = nil, activities: [ActivitySuggestion] = [], isRestDay: Bool = false, diff --git a/SportsTime/Core/Models/Local/CanonicalModels.swift b/SportsTime/Core/Models/Local/CanonicalModels.swift index 805e1b9..d6aa529 100644 --- a/SportsTime/Core/Models/Local/CanonicalModels.swift +++ b/SportsTime/Core/Models/Local/CanonicalModels.swift @@ -163,7 +163,7 @@ final class CanonicalStadium { func toDomain() -> Stadium { Stadium( - id: uuid, + id: canonicalId, name: name, city: city, state: state, @@ -299,14 +299,14 @@ final class CanonicalTeam { var sportEnum: Sport? { Sport(rawValue: sport) } - func toDomain(stadiumUUID: UUID) -> Team { + func toDomain() -> Team { Team( - id: uuid, + id: canonicalId, name: name, abbreviation: abbreviation, sport: sportEnum ?? .mlb, city: city, - stadiumId: stadiumUUID, + stadiumId: stadiumCanonicalId, logoURL: logoURL.flatMap { URL(string: $0) }, primaryColor: primaryColor, secondaryColor: secondaryColor @@ -466,12 +466,12 @@ final class CanonicalGame { var sportEnum: Sport? { Sport(rawValue: sport) } - func toDomain(homeTeamUUID: UUID, awayTeamUUID: UUID, stadiumUUID: UUID) -> Game { + func toDomain() -> Game { Game( - id: uuid, - homeTeamId: homeTeamUUID, - awayTeamId: awayTeamUUID, - stadiumId: stadiumUUID, + id: canonicalId, + homeTeamId: homeTeamCanonicalId, + awayTeamId: awayTeamCanonicalId, + stadiumId: stadiumCanonicalId, dateTime: dateTime, sport: sportEnum ?? .mlb, season: season, diff --git a/SportsTime/Core/Models/Local/SavedTrip.swift b/SportsTime/Core/Models/Local/SavedTrip.swift index 13a3ec9..a915c67 100644 --- a/SportsTime/Core/Models/Local/SavedTrip.swift +++ b/SportsTime/Core/Models/Local/SavedTrip.swift @@ -16,7 +16,7 @@ final class SavedTrip { var updatedAt: Date var status: String var tripData: Data // Encoded Trip struct - var gamesData: Data? // Encoded [UUID: RichGame] dictionary + var gamesData: Data? // Encoded [String: RichGame] dictionary @Relationship(deleteRule: .cascade) var votes: [TripVote]? @@ -43,16 +43,16 @@ final class SavedTrip { try? JSONDecoder().decode(Trip.self, from: tripData) } - var games: [UUID: RichGame] { + var games: [String: RichGame] { guard let data = gamesData else { return [:] } - return (try? JSONDecoder().decode([UUID: RichGame].self, from: data)) ?? [:] + return (try? JSONDecoder().decode([String: RichGame].self, from: data)) ?? [:] } var tripStatus: TripStatus { TripStatus(rawValue: status) ?? .draft } - static func from(_ trip: Trip, games: [UUID: RichGame] = [:], status: TripStatus = .planned) -> SavedTrip? { + static func from(_ trip: Trip, games: [String: RichGame] = [:], status: TripStatus = .planned) -> SavedTrip? { guard let tripData = try? JSONEncoder().encode(trip) else { return nil } let gamesData = try? JSONEncoder().encode(games) return SavedTrip( @@ -75,7 +75,7 @@ final class TripVote { var tripId: UUID var voterId: String var voterName: String - var gameVotes: Data // [UUID: Bool] encoded + var gameVotes: Data // [String: Bool] encoded (game IDs to vote) var routeVotes: Data // [String: Int] encoded var leisurePreference: String var createdAt: Date diff --git a/SportsTime/Core/Models/Local/StadiumProgress.swift b/SportsTime/Core/Models/Local/StadiumProgress.swift index 514cc91..7e7f184 100644 --- a/SportsTime/Core/Models/Local/StadiumProgress.swift +++ b/SportsTime/Core/Models/Local/StadiumProgress.swift @@ -65,8 +65,7 @@ final class StadiumVisit { @Attribute(.unique) var id: UUID // Stadium identity (stable across renames) - var canonicalStadiumId: String // Links to CanonicalStadium.canonicalId - var stadiumUUID: UUID // Runtime UUID for display lookups + var stadiumId: String // Canonical ID: "stadium_mlb_fenway_park" var stadiumNameAtVisit: String // Frozen at visit time // Visit details @@ -75,9 +74,9 @@ final class StadiumVisit { var visitTypeRaw: String // VisitType.rawValue // Game info (optional - nil for tours/other visits) - var gameId: UUID? - var homeTeamId: UUID? - var awayTeamId: UUID? + var gameId: String? // Canonical ID: "game_mlb_2026_bos_nyy_0401" + var homeTeamId: String? // Canonical ID: "team_mlb_bos" + var awayTeamId: String? // Canonical ID: "team_mlb_nyy" var homeTeamName: String? // For display when team lookup fails var awayTeamName: String? var finalScore: String? // "5-3" format @@ -109,15 +108,14 @@ final class StadiumVisit { init( id: UUID = UUID(), - canonicalStadiumId: String, - stadiumUUID: UUID, + stadiumId: String, stadiumNameAtVisit: String, visitDate: Date, sport: Sport, visitType: VisitType = .game, - gameId: UUID? = nil, - homeTeamId: UUID? = nil, - awayTeamId: UUID? = nil, + gameId: String? = nil, + homeTeamId: String? = nil, + awayTeamId: String? = nil, homeTeamName: String? = nil, awayTeamName: String? = nil, finalScore: String? = nil, @@ -133,8 +131,7 @@ final class StadiumVisit { source: VisitSource = .manual ) { self.id = id - self.canonicalStadiumId = canonicalStadiumId - self.stadiumUUID = stadiumUUID + self.stadiumId = stadiumId self.stadiumNameAtVisit = stadiumNameAtVisit self.visitDate = visitDate self.sport = sport.rawValue diff --git a/SportsTime/Core/Services/AchievementEngine.swift b/SportsTime/Core/Services/AchievementEngine.swift index 5e2cccf..9915051 100644 --- a/SportsTime/Core/Services/AchievementEngine.swift +++ b/SportsTime/Core/Services/AchievementEngine.swift @@ -44,7 +44,7 @@ final class AchievementEngine { func recalculateAllAchievements() async throws -> AchievementDelta { // Get all visits let visits = try fetchAllVisits() - let visitedStadiumIds = Set(visits.map { $0.canonicalStadiumId }) + let visitedStadiumIds = Set(visits.map { $0.stadiumId }) // Get currently earned achievements let currentAchievements = try fetchEarnedAchievements() @@ -112,7 +112,7 @@ final class AchievementEngine { /// Quick check after new visit (incremental) func checkAchievementsForNewVisit(_ visit: StadiumVisit) async throws -> [AchievementDefinition] { let visits = try fetchAllVisits() - let visitedStadiumIds = Set(visits.map { $0.canonicalStadiumId }) + let visitedStadiumIds = Set(visits.map { $0.stadiumId }) let currentAchievements = try fetchEarnedAchievements() let currentAchievementIds = Set(currentAchievements.map { $0.achievementTypeId }) @@ -152,7 +152,7 @@ final class AchievementEngine { /// Get progress toward all achievements func getProgress() async throws -> [AchievementProgress] { let visits = try fetchAllVisits() - let visitedStadiumIds = Set(visits.map { $0.canonicalStadiumId }) + let visitedStadiumIds = Set(visits.map { $0.stadiumId }) let earnedAchievements = try fetchEarnedAchievements() let earnedIds = Set(earnedAchievements.map { $0.achievementTypeId }) @@ -196,7 +196,7 @@ final class AchievementEngine { case .visitCountForSport(let count, let sport): let sportVisits = visits.filter { $0.sport == sport.rawValue } - let sportStadiums = Set(sportVisits.map { $0.canonicalStadiumId }) + let sportStadiums = Set(sportVisits.map { $0.stadiumId }) return sportStadiums.count >= count case .completeDivision(let divisionId): @@ -214,41 +214,12 @@ final class AchievementEngine { case .multipleLeagues(let leagueCount): return checkMultipleLeagues(visits: visits, requiredLeagues: leagueCount) - case .specificStadium(let symbolicId): - // Resolve symbolic ID (e.g., "stadium_mlb_bos") to actual UUID string - guard let resolvedId = resolveSymbolicStadiumId(symbolicId) else { return false } - return visitedStadiumIds.contains(resolvedId) + case .specificStadium(let stadiumId): + // Direct comparison - canonical IDs match everywhere + return visitedStadiumIds.contains(stadiumId) } } - /// Resolves symbolic stadium IDs (e.g., "stadium_mlb_bos") to actual stadium UUID strings - private func resolveSymbolicStadiumId(_ symbolicId: String) -> String? { - // Parse symbolic ID format: "stadium_{sport}_{teamAbbrev}" - let parts = symbolicId.split(separator: "_") - guard parts.count == 3, - parts[0] == "stadium" else { - return nil - } - - // Sport raw values are uppercase (e.g., "MLB"), but symbolic IDs use lowercase - let sportString = String(parts[1]).uppercased() - guard let sport = Sport(rawValue: sportString) else { - return nil - } - - let teamAbbrev = String(parts[2]).uppercased() - - // Find team by abbreviation and sport - guard let team = dataProvider.teams.first(where: { - $0.abbreviation.uppercased() == teamAbbrev && $0.sport == sport - }) else { - return nil - } - - // Return the stadium UUID as string (matches visit's canonicalStadiumId format) - return team.stadiumId.uuidString - } - private func checkDivisionComplete(_ divisionId: String, visitedStadiumIds: Set) -> Bool { guard let division = LeagueStructure.division(byId: divisionId) else { return false } @@ -291,7 +262,7 @@ final class AchievementEngine { if daysDiff < withinDays { // Check unique stadiums in window let windowVisits = Array(sortedVisits[i..<(i + requiredVisits)]) - let uniqueStadiums = Set(windowVisits.map { $0.canonicalStadiumId }) + let uniqueStadiums = Set(windowVisits.map { $0.stadiumId }) if uniqueStadiums.count >= requiredVisits { return true } @@ -322,7 +293,7 @@ final class AchievementEngine { case .visitCountForSport(let count, let sport): let sportVisits = visits.filter { $0.sport == sport.rawValue } - let sportStadiums = Set(sportVisits.map { $0.canonicalStadiumId }) + let sportStadiums = Set(sportVisits.map { $0.stadiumId }) return (sportStadiums.count, count) case .completeDivision(let divisionId): @@ -348,10 +319,9 @@ final class AchievementEngine { let leagues = Set(visits.compactMap { Sport(rawValue: $0.sport) }) return (leagues.count, leagueCount) - case .specificStadium(let symbolicId): - // Resolve symbolic ID to actual UUID string - guard let resolvedId = resolveSymbolicStadiumId(symbolicId) else { return (0, 1) } - return (visitedStadiumIds.contains(resolvedId) ? 1 : 0, 1) + case .specificStadium(let stadiumId): + // Direct comparison - canonical IDs match everywhere + return (visitedStadiumIds.contains(stadiumId) ? 1 : 0, 1) } } @@ -368,15 +338,15 @@ final class AchievementEngine { case .completeDivision(let divisionId): let stadiumIds = Set(getStadiumIdsForDivision(divisionId)) - return visits.filter { stadiumIds.contains($0.canonicalStadiumId) }.map { $0.id } + return visits.filter { stadiumIds.contains($0.stadiumId) }.map { $0.id } case .completeConference(let conferenceId): let stadiumIds = Set(getStadiumIdsForConference(conferenceId)) - return visits.filter { stadiumIds.contains($0.canonicalStadiumId) }.map { $0.id } + return visits.filter { stadiumIds.contains($0.stadiumId) }.map { $0.id } case .completeLeague(let sport): let stadiumIds = Set(getStadiumIdsForLeague(sport)) - return visits.filter { stadiumIds.contains($0.canonicalStadiumId) }.map { $0.id } + return visits.filter { stadiumIds.contains($0.stadiumId) }.map { $0.id } case .visitsInDays(let requiredVisits, let days): // Find the qualifying window of visits @@ -391,10 +361,9 @@ final class AchievementEngine { } return [] - case .specificStadium(let symbolicId): - // Resolve symbolic ID to actual UUID string - guard let resolvedId = resolveSymbolicStadiumId(symbolicId) else { return [] } - return visits.filter { $0.canonicalStadiumId == resolvedId }.map { $0.id } + case .specificStadium(let stadiumId): + // Direct comparison - canonical IDs match everywhere + return visits.filter { $0.stadiumId == stadiumId }.map { $0.id } } } @@ -410,17 +379,8 @@ final class AchievementEngine { return [] } - // Get stadium UUIDs for these teams - // CanonicalTeam has stadiumCanonicalId, we need to find the corresponding Stadium UUID - var stadiumIds: [String] = [] - for canonicalTeam in canonicalTeams { - // Find the domain team by matching name/abbreviation to get stadium UUID - if let team = dataProvider.teams.first(where: { $0.abbreviation == canonicalTeam.abbreviation && $0.sport.rawValue == canonicalTeam.sport }) { - stadiumIds.append(team.stadiumId.uuidString) - } - } - - return stadiumIds + // Get canonical stadium IDs for these teams + return canonicalTeams.map { $0.stadiumCanonicalId } } private func getStadiumIdsForConference(_ conferenceId: String) -> [String] { @@ -434,7 +394,7 @@ final class AchievementEngine { } private func getStadiumIdsForLeague(_ sport: Sport) -> [String] { - // Get all stadiums for this sport - return UUID strings to match visit format + // Get all stadium canonical IDs for this sport return dataProvider.stadiums .filter { stadium in // Check if stadium hosts teams of this sport @@ -442,7 +402,7 @@ final class AchievementEngine { team.stadiumId == stadium.id && team.sport == sport } } - .map { $0.id.uuidString } + .map { $0.id } } // MARK: - Data Fetching diff --git a/SportsTime/Core/Services/CloudKitService.swift b/SportsTime/Core/Services/CloudKitService.swift index 229305a..1a1ff43 100644 --- a/SportsTime/Core/Services/CloudKitService.swift +++ b/SportsTime/Core/Services/CloudKitService.swift @@ -165,18 +165,18 @@ actor CloudKitService { let ckGame = CKGame(record: record) guard let homeRef = record[CKGame.homeTeamRefKey] as? CKRecord.Reference, - let awayRef = record[CKGame.awayTeamRefKey] as? CKRecord.Reference, - let homeId = UUID(uuidString: homeRef.recordID.recordName), - let awayId = UUID(uuidString: awayRef.recordID.recordName) + let awayRef = record[CKGame.awayTeamRefKey] as? CKRecord.Reference else { return nil } + let homeId = homeRef.recordID.recordName + let awayId = awayRef.recordID.recordName + // Stadium ref is optional - use placeholder if not present - let stadiumId: UUID - if let stadiumRef = record[CKGame.stadiumRefKey] as? CKRecord.Reference, - let refId = UUID(uuidString: stadiumRef.recordID.recordName) { - stadiumId = refId + let stadiumId: String + if let stadiumRef = record[CKGame.stadiumRefKey] as? CKRecord.Reference { + stadiumId = stadiumRef.recordID.recordName } else { - stadiumId = UUID() // Placeholder - will be resolved via team lookup + stadiumId = "stadium_placeholder_\(UUID().uuidString)" // Placeholder - will be resolved via team lookup } return ckGame.game(homeTeamId: homeId, awayTeamId: awayId, stadiumId: stadiumId) @@ -188,8 +188,8 @@ actor CloudKitService { return allGames.sorted { $0.dateTime < $1.dateTime } } - func fetchGame(by id: UUID) async throws -> Game? { - let predicate = NSPredicate(format: "gameId == %@", id.uuidString) + func fetchGame(by id: String) async throws -> Game? { + let predicate = NSPredicate(format: "gameId == %@", id) let query = CKQuery(recordType: CKRecordType.game, predicate: predicate) let (results, _) = try await publicDatabase.records(matching: query) @@ -201,12 +201,13 @@ actor CloudKitService { guard let homeRef = record[CKGame.homeTeamRefKey] as? CKRecord.Reference, let awayRef = record[CKGame.awayTeamRefKey] as? CKRecord.Reference, - let stadiumRef = record[CKGame.stadiumRefKey] as? CKRecord.Reference, - let homeId = UUID(uuidString: homeRef.recordID.recordName), - let awayId = UUID(uuidString: awayRef.recordID.recordName), - let stadiumId = UUID(uuidString: stadiumRef.recordID.recordName) + let stadiumRef = record[CKGame.stadiumRefKey] as? CKRecord.Reference else { return nil } + let homeId = homeRef.recordID.recordName + let awayId = awayRef.recordID.recordName + let stadiumId = stadiumRef.recordID.recordName + return ckGame.game(homeTeamId: homeId, awayTeamId: awayId, stadiumId: stadiumId) } @@ -277,13 +278,11 @@ actor CloudKitService { let stadiumCanonicalId = ckGame.stadiumCanonicalId else { return nil } - // For the Game domain object, we still need UUIDs - use placeholder - // The sync service will use canonical IDs for relationships - let placeholderUUID = UUID() + // For the Game domain object, use canonical IDs directly guard let game = ckGame.game( - homeTeamId: placeholderUUID, - awayTeamId: placeholderUUID, - stadiumId: placeholderUUID + homeTeamId: homeTeamCanonicalId, + awayTeamId: awayTeamCanonicalId, + stadiumId: stadiumCanonicalId ) else { return nil } return SyncGame( diff --git a/SportsTime/Core/Services/DataProvider.swift b/SportsTime/Core/Services/DataProvider.swift index c1e3efa..c60ca37 100644 --- a/SportsTime/Core/Services/DataProvider.swift +++ b/SportsTime/Core/Services/DataProvider.swift @@ -21,14 +21,9 @@ final class AppDataProvider: ObservableObject { @Published private(set) var error: Error? @Published private(set) var errorMessage: String? - private var teamsById: [UUID: Team] = [:] - private var stadiumsById: [UUID: Stadium] = [:] - private var stadiumsByCanonicalId: [String: Stadium] = [:] - private var teamsByCanonicalId: [String: Team] = [:] - - // Canonical ID lookups for game conversion - private var canonicalTeamUUIDs: [String: UUID] = [:] - private var canonicalStadiumUUIDs: [String: UUID] = [:] + // Lookup dictionaries - keyed by canonical ID (String) + private var teamsById: [String: Team] = [:] + private var stadiumsById: [String: Stadium] = [:] private var modelContext: ModelContext? @@ -63,11 +58,11 @@ final class AppDataProvider: ObservableObject { // Convert to domain models and build lookups var loadedStadiums: [Stadium] = [] + var stadiumLookup: [String: Stadium] = [:] for canonical in canonicalStadiums { let stadium = canonical.toDomain() loadedStadiums.append(stadium) - stadiumsByCanonicalId[canonical.canonicalId] = stadium - canonicalStadiumUUIDs[canonical.canonicalId] = stadium.id + stadiumLookup[stadium.id] = stadium } // Fetch canonical teams from SwiftData @@ -78,31 +73,17 @@ final class AppDataProvider: ObservableObject { // Convert to domain models var loadedTeams: [Team] = [] + var teamLookup: [String: Team] = [:] for canonical in canonicalTeams { - // Get stadium UUID for this team - let stadiumUUID = canonicalStadiumUUIDs[canonical.stadiumCanonicalId] ?? UUID() - let team = canonical.toDomain(stadiumUUID: stadiumUUID) + let team = canonical.toDomain() loadedTeams.append(team) - teamsByCanonicalId[canonical.canonicalId] = team - canonicalTeamUUIDs[canonical.canonicalId] = team.id + teamLookup[team.id] = team } self.teams = loadedTeams self.stadiums = loadedStadiums - - // Build lookup dictionaries (use reduce to handle potential duplicates gracefully) - self.teamsById = loadedTeams.reduce(into: [:]) { dict, team in - if dict[team.id] != nil { - print("⚠️ Duplicate team UUID: \(team.id) - \(team.name)") - } - dict[team.id] = team - } - self.stadiumsById = loadedStadiums.reduce(into: [:]) { dict, stadium in - if dict[stadium.id] != nil { - print("⚠️ Duplicate stadium UUID: \(stadium.id) - \(stadium.name)") - } - dict[stadium.id] = stadium - } + self.teamsById = teamLookup + self.stadiumsById = stadiumLookup } catch { self.error = error @@ -123,11 +104,11 @@ final class AppDataProvider: ObservableObject { // MARK: - Data Access - func team(for id: UUID) -> Team? { + func team(for id: String) -> Team? { teamsById[id] } - func stadium(for id: UUID) -> Stadium? { + func stadium(for id: String) -> Stadium? { stadiumsById[id] } @@ -156,47 +137,27 @@ final class AppDataProvider: ObservableObject { let canonicalGames = try context.fetch(descriptor) // Filter by sport and convert to domain models - let result = canonicalGames.compactMap { canonical -> Game? in + return canonicalGames.compactMap { canonical -> Game? in guard sportStrings.contains(canonical.sport) else { return nil } - - let homeTeamUUID = canonicalTeamUUIDs[canonical.homeTeamCanonicalId] ?? UUID() - let awayTeamUUID = canonicalTeamUUIDs[canonical.awayTeamCanonicalId] ?? UUID() - let stadiumUUID = canonicalStadiumUUIDs[canonical.stadiumCanonicalId] ?? UUID() - - return canonical.toDomain( - homeTeamUUID: homeTeamUUID, - awayTeamUUID: awayTeamUUID, - stadiumUUID: stadiumUUID - ) + return canonical.toDomain() } - - return result } - /// Fetch a single game by ID - func fetchGame(by id: UUID) async throws -> Game? { + /// Fetch a single game by canonical ID + func fetchGame(by id: String) async throws -> Game? { guard let context = modelContext else { throw DataProviderError.contextNotConfigured } - let idString = id.uuidString let descriptor = FetchDescriptor( - predicate: #Predicate { $0.canonicalId == idString } + predicate: #Predicate { $0.canonicalId == id } ) guard let canonical = try context.fetch(descriptor).first else { return nil } - let homeTeamUUID = canonicalTeamUUIDs[canonical.homeTeamCanonicalId] ?? UUID() - let awayTeamUUID = canonicalTeamUUIDs[canonical.awayTeamCanonicalId] ?? UUID() - let stadiumUUID = canonicalStadiumUUIDs[canonical.stadiumCanonicalId] ?? UUID() - - return canonical.toDomain( - homeTeamUUID: homeTeamUUID, - awayTeamUUID: awayTeamUUID, - stadiumUUID: stadiumUUID - ) + return canonical.toDomain() } /// Fetch games with full team and stadium data diff --git a/SportsTime/Core/Services/GameMatcher.swift b/SportsTime/Core/Services/GameMatcher.swift index fa0caab..b6b102b 100644 --- a/SportsTime/Core/Services/GameMatcher.swift +++ b/SportsTime/Core/Services/GameMatcher.swift @@ -43,7 +43,7 @@ enum NoMatchReason: Sendable { // MARK: - Game Match Result struct GameMatchCandidate: Identifiable, Sendable { - let id: UUID + let id: String let game: Game let stadium: Stadium let homeTeam: Team @@ -67,14 +67,20 @@ struct GameMatchCandidate: Identifiable, Sendable { /// Initialize from a scraped historical game init(scrapedGame: ScrapedGame, stadium: Stadium) { - self.id = UUID() + let matchId = UUID() + self.id = "scraped_match_\(matchId.uuidString)" self.stadium = stadium + // Generate synthetic IDs for scraped games + let syntheticHomeTeamId = "scraped_team_\(UUID().uuidString)" + let syntheticAwayTeamId = "scraped_team_\(UUID().uuidString)" + let syntheticGameId = "scraped_game_\(matchId.uuidString)" + // Create synthetic Team objects from scraped names // Scraped names already include city (e.g., "Chicago Cubs"), so we use empty city // to avoid duplication in fullName computed property self.homeTeam = Team( - id: UUID(), + id: syntheticHomeTeamId, name: scrapedGame.homeTeam, abbreviation: String(scrapedGame.homeTeam.suffix(3)).uppercased(), sport: scrapedGame.sport, @@ -83,7 +89,7 @@ struct GameMatchCandidate: Identifiable, Sendable { ) self.awayTeam = Team( - id: UUID(), + id: syntheticAwayTeamId, name: scrapedGame.awayTeam, abbreviation: String(scrapedGame.awayTeam.suffix(3)).uppercased(), sport: scrapedGame.sport, @@ -94,7 +100,7 @@ struct GameMatchCandidate: Identifiable, Sendable { // Create synthetic Game object let year = Calendar.current.component(.year, from: scrapedGame.date) self.game = Game( - id: self.id, + id: syntheticGameId, homeTeamId: self.homeTeam.id, awayTeamId: self.awayTeam.id, stadiumId: stadium.id, diff --git a/SportsTime/Core/Services/RouteDescriptionGenerator.swift b/SportsTime/Core/Services/RouteDescriptionGenerator.swift index 655ec2c..18a1fa9 100644 --- a/SportsTime/Core/Services/RouteDescriptionGenerator.swift +++ b/SportsTime/Core/Services/RouteDescriptionGenerator.swift @@ -132,7 +132,7 @@ struct RouteDescriptionInput: Identifiable { let totalMiles: Double let totalDrivingHours: Double - init(from option: ItineraryOption, games: [UUID: RichGame]) { + init(from option: ItineraryOption, games: [String: RichGame]) { self.id = option.id self.cities = Array(NSOrderedSet(array: option.stops.map { $0.city })) as? [String] ?? [] diff --git a/SportsTime/Core/Services/StadiumProximityMatcher.swift b/SportsTime/Core/Services/StadiumProximityMatcher.swift index 3125961..fc24b64 100644 --- a/SportsTime/Core/Services/StadiumProximityMatcher.swift +++ b/SportsTime/Core/Services/StadiumProximityMatcher.swift @@ -60,7 +60,7 @@ extension MatchConfidence: Comparable { // MARK: - Stadium Match struct StadiumMatch: Identifiable, Sendable { - let id: UUID + let id: String let stadium: Stadium let distance: CLLocationDistance let confidence: MatchConfidence diff --git a/SportsTime/Core/Services/SuggestedTripsGenerator.swift b/SportsTime/Core/Services/SuggestedTripsGenerator.swift index 8860ef6..d3c0ba4 100644 --- a/SportsTime/Core/Services/SuggestedTripsGenerator.swift +++ b/SportsTime/Core/Services/SuggestedTripsGenerator.swift @@ -16,7 +16,7 @@ struct SuggestedTrip: Identifiable { let region: Region let isSingleSport: Bool let trip: Trip - let richGames: [UUID: RichGame] + let richGames: [String: RichGame] let sports: Set var displaySports: [Sport] { @@ -119,8 +119,8 @@ final class SuggestedTripsGenerator { } // Build lookups (use reduce to handle potential duplicate UUIDs gracefully) - let stadiumsById = dataProvider.stadiums.reduce(into: [UUID: Stadium]()) { $0[$1.id] = $1 } - let teamsById = dataProvider.teams.reduce(into: [UUID: Team]()) { $0[$1.id] = $1 } + let stadiumsById = dataProvider.stadiums.reduce(into: [String: Stadium]()) { $0[$1.id] = $1 } + let teamsById = dataProvider.teams.reduce(into: [String: Team]()) { $0[$1.id] = $1 } var generatedTrips: [SuggestedTrip] = [] @@ -208,8 +208,8 @@ final class SuggestedTripsGenerator { games: [Game], region: Region, singleSport: Bool, - stadiums: [UUID: Stadium], - teams: [UUID: Team], + stadiums: [String: Stadium], + teams: [String: Team], startDate: Date, endDate: Date, excludingSport: Sport? = nil @@ -292,8 +292,8 @@ final class SuggestedTripsGenerator { } } - private func buildRichGames(from games: [Game], teams: [UUID: Team], stadiums: [UUID: Stadium]) -> [UUID: RichGame] { - var result: [UUID: RichGame] = [:] + private func buildRichGames(from games: [Game], teams: [String: Team], stadiums: [String: Stadium]) -> [String: RichGame] { + var result: [String: RichGame] = [:] for game in games { guard let homeTeam = teams[game.homeTeamId], let awayTeam = teams[game.awayTeamId], @@ -305,8 +305,8 @@ final class SuggestedTripsGenerator { private func generateCrossCountryTrip( games: [Game], - stadiums: [UUID: Stadium], - teams: [UUID: Team], + stadiums: [String: Stadium], + teams: [String: Team], startDate: Date, endDate: Date, excludeGames: [Game] @@ -392,7 +392,7 @@ final class SuggestedTripsGenerator { // Build stops by grouping consecutive games at the same stadium var tripStops: [TripStop] = [] - var currentStadiumId: UUID? = nil + var currentStadiumId: String? = nil var currentGames: [Game] = [] for game in selectedGames { @@ -463,7 +463,7 @@ final class SuggestedTripsGenerator { } /// Builds a TripStop from a group of games at the same stadium - private func buildTripStop(from games: [Game], stadiumId: UUID, stadiums: [UUID: Stadium], stopNumber: Int) -> TripStop? { + private func buildTripStop(from games: [Game], stadiumId: String, stadiums: [String: Stadium], stopNumber: Int) -> TripStop? { guard !games.isEmpty else { return nil } let sortedGames = games.sorted { $0.dateTime < $1.dateTime } @@ -488,7 +488,7 @@ final class SuggestedTripsGenerator { } /// Builds travel segments between consecutive stops using TravelEstimator - private func buildTravelSegments(from stops: [TripStop], stadiums: [UUID: Stadium]) -> [TravelSegment] { + private func buildTravelSegments(from stops: [TripStop], stadiums: [String: Stadium]) -> [TravelSegment] { guard stops.count >= 2 else { return [] } var segments: [TravelSegment] = [] @@ -560,7 +560,7 @@ final class SuggestedTripsGenerator { /// Builds a trip following a geographic corridor (moving consistently east or west) private func buildCorridorTrip( games: [(game: Game, lon: Double)], - stadiums: [UUID: Stadium], + stadiums: [String: Stadium], direction: Direction, calendar: Calendar ) -> [Game] { @@ -631,7 +631,7 @@ final class SuggestedTripsGenerator { startGames: [Game], middleGames: [Game], endGames: [Game], - stadiums: [UUID: Stadium], + stadiums: [String: Stadium], calendar: Calendar ) -> [Game] { // Sort all games by date @@ -668,7 +668,7 @@ final class SuggestedTripsGenerator { var totalDistance: Double = 0 // Track all days with games and their stadiums for conflict detection - var gamesByDay: [Date: UUID] = [:] + var gamesByDay: [Date: String] = [:] gamesByDay[calendar.startOfDay(for: startGame.dateTime)] = startGame.stadiumId // Find middle games that fit between start and end (limit to 2-3 middle stops) @@ -774,7 +774,7 @@ final class SuggestedTripsGenerator { /// Validates that no two games in the route are on the same calendar day at different stadiums private func validateNoSameDayConflicts(_ games: [Game], calendar: Calendar) -> Bool { - var gamesByDay: [Date: UUID] = [:] + var gamesByDay: [Date: String] = [:] for game in games { let day = calendar.startOfDay(for: game.dateTime) if let existingStadiumId = gamesByDay[day] { diff --git a/SportsTime/Export/PDFGenerator.swift b/SportsTime/Export/PDFGenerator.swift index 9f15b56..23b7bc7 100644 --- a/SportsTime/Export/PDFGenerator.swift +++ b/SportsTime/Export/PDFGenerator.swift @@ -28,7 +28,7 @@ final class PDFGenerator { func generatePDF( for trip: Trip, - games: [UUID: RichGame], + games: [String: RichGame], assets: PDFAssetPrefetcher.PrefetchedAssets? = nil ) async throws -> Data { let pdfRenderer = UIGraphicsPDFRenderer( @@ -244,7 +244,7 @@ final class PDFGenerator { private func drawItineraryPages( context: UIGraphicsPDFRendererContext, trip: Trip, - games: [UUID: RichGame], + games: [String: RichGame], assets: PDFAssetPrefetcher.PrefetchedAssets? ) { var pageNumber = 3 @@ -283,7 +283,7 @@ final class PDFGenerator { drawFooter(context: context, pageNumber: pageNumber) } - private func estimateDayHeight(day: ItineraryDay, games: [UUID: RichGame]) -> CGFloat { + private func estimateDayHeight(day: ItineraryDay, games: [String: RichGame]) -> CGFloat { var height: CGFloat = 60 // Day header + city // Games @@ -305,7 +305,7 @@ final class PDFGenerator { private func drawDay( context: UIGraphicsPDFRendererContext, day: ItineraryDay, - games: [UUID: RichGame], + games: [String: RichGame], assets: PDFAssetPrefetcher.PrefetchedAssets?, y: CGFloat ) -> CGFloat { @@ -520,7 +520,7 @@ final class PDFGenerator { private func drawCitySpotlightPages( context: UIGraphicsPDFRendererContext, trip: Trip, - games: [UUID: RichGame], + games: [String: RichGame], assets: PDFAssetPrefetcher.PrefetchedAssets? ) { guard let cityPOIs = assets?.cityPOIs, !cityPOIs.isEmpty else { return } @@ -661,7 +661,7 @@ final class PDFGenerator { private func drawSummaryPage( context: UIGraphicsPDFRendererContext, trip: Trip, - games: [UUID: RichGame] + games: [String: RichGame] ) { var y: CGFloat = margin @@ -896,7 +896,7 @@ final class ExportService { /// Export trip to PDF with full prefetched assets func exportToPDF( trip: Trip, - games: [UUID: RichGame], + games: [String: RichGame], progressCallback: ((PDFAssetPrefetcher.PrefetchProgress) async -> Void)? = nil ) async throws -> URL { // Prefetch all assets @@ -918,7 +918,7 @@ final class ExportService { } /// Quick export without prefetching (basic PDF) - func exportToPDFBasic(trip: Trip, games: [UUID: RichGame]) async throws -> URL { + func exportToPDFBasic(trip: Trip, games: [String: RichGame]) async throws -> URL { let data = try await pdfGenerator.generatePDF(for: trip, games: games, assets: nil) let fileName = "\(trip.name.replacingOccurrences(of: " ", with: "_"))_\(Date().timeIntervalSince1970).pdf" diff --git a/SportsTime/Export/Services/PDFAssetPrefetcher.swift b/SportsTime/Export/Services/PDFAssetPrefetcher.swift index b2bc389..2ab016d 100644 --- a/SportsTime/Export/Services/PDFAssetPrefetcher.swift +++ b/SportsTime/Export/Services/PDFAssetPrefetcher.swift @@ -15,8 +15,8 @@ actor PDFAssetPrefetcher { struct PrefetchedAssets { let routeMap: UIImage? let cityMaps: [String: UIImage] - let teamLogos: [UUID: UIImage] - let stadiumPhotos: [UUID: UIImage] + let teamLogos: [String: UIImage] + let stadiumPhotos: [String: UIImage] let cityPOIs: [String: [POISearchService.POI]] var isEmpty: Bool { @@ -63,7 +63,7 @@ actor PDFAssetPrefetcher { /// - Returns: All prefetched assets func prefetchAssets( for trip: Trip, - games: [UUID: RichGame], + games: [String: RichGame], progressCallback: ((PrefetchProgress) async -> Void)? = nil ) async -> PrefetchedAssets { var progress = PrefetchProgress() @@ -71,8 +71,8 @@ actor PDFAssetPrefetcher { // Collect unique teams and stadiums from games var teams: [Team] = [] var stadiums: [Stadium] = [] - var seenTeamIds: Set = [] - var seenStadiumIds: Set = [] + var seenTeamIds: Set = [] + var seenStadiumIds: Set = [] for (_, richGame) in games { if !seenTeamIds.contains(richGame.homeTeam.id) { diff --git a/SportsTime/Export/Services/RemoteImageService.swift b/SportsTime/Export/Services/RemoteImageService.swift index 7c732f3..a950c62 100644 --- a/SportsTime/Export/Services/RemoteImageService.swift +++ b/SportsTime/Export/Services/RemoteImageService.swift @@ -111,8 +111,8 @@ actor RemoteImageService { } /// Fetch team logos by team ID - func fetchTeamLogos(teams: [Team]) async -> [UUID: UIImage] { - let urlToTeam: [URL: UUID] = Dictionary( + func fetchTeamLogos(teams: [Team]) async -> [String: UIImage] { + let urlToTeam: [URL: String] = Dictionary( uniqueKeysWithValues: teams.compactMap { team in guard let logoURL = team.logoURL else { return nil } return (logoURL, team.id) @@ -121,7 +121,7 @@ actor RemoteImageService { let images = await fetchImages(from: Array(urlToTeam.keys)) - var result: [UUID: UIImage] = [:] + var result: [String: UIImage] = [:] for (url, image) in images { if let teamId = urlToTeam[url] { result[teamId] = image @@ -132,8 +132,8 @@ actor RemoteImageService { } /// Fetch stadium photos by stadium ID - func fetchStadiumPhotos(stadiums: [Stadium]) async -> [UUID: UIImage] { - let urlToStadium: [URL: UUID] = Dictionary( + func fetchStadiumPhotos(stadiums: [Stadium]) async -> [String: UIImage] { + let urlToStadium: [URL: String] = Dictionary( uniqueKeysWithValues: stadiums.compactMap { stadium in guard let imageURL = stadium.imageURL else { return nil } return (imageURL, stadium.id) @@ -142,7 +142,7 @@ actor RemoteImageService { let images = await fetchImages(from: Array(urlToStadium.keys)) - var result: [UUID: UIImage] = [:] + var result: [String: UIImage] = [:] for (url, image) in images { if let stadiumId = urlToStadium[url] { result[stadiumId] = image diff --git a/SportsTime/Features/Progress/ViewModels/PhotoImportViewModel.swift b/SportsTime/Features/Progress/ViewModels/PhotoImportViewModel.swift index e90291a..d6ace6a 100644 --- a/SportsTime/Features/Progress/ViewModels/PhotoImportViewModel.swift +++ b/SportsTime/Features/Progress/ViewModels/PhotoImportViewModel.swift @@ -174,12 +174,14 @@ final class PhotoImportViewModel { // Create the visit let visit = StadiumVisit( - canonicalStadiumId: match.stadium.id.uuidString, - stadiumUUID: match.stadium.id, + stadiumId: match.stadium.id, stadiumNameAtVisit: match.stadium.name, visitDate: match.game.dateTime, sport: match.game.sport, visitType: .game, + gameId: match.game.id, + homeTeamId: match.homeTeam.id, + awayTeamId: match.awayTeam.id, homeTeamName: match.homeTeam.fullName, awayTeamName: match.awayTeam.fullName, finalScore: match.formattedFinalScore, diff --git a/SportsTime/Features/Progress/ViewModels/ProgressViewModel.swift b/SportsTime/Features/Progress/ViewModels/ProgressViewModel.swift index d9e56f2..9491651 100644 --- a/SportsTime/Features/Progress/ViewModels/ProgressViewModel.swift +++ b/SportsTime/Features/Progress/ViewModels/ProgressViewModel.swift @@ -41,10 +41,10 @@ final class ProgressViewModel { let visitedStadiumIds = Set( visits .filter { $0.sportEnum == selectedSport } - .compactMap { visit -> UUID? in + .compactMap { visit -> String? in // Match visit's canonical stadium ID to a stadium stadiums.first { stadium in - stadium.id == visit.stadiumUUID + stadium.id == visit.stadiumId }?.id } ) @@ -62,11 +62,11 @@ final class ProgressViewModel { } /// Stadium visit status indexed by stadium ID - var stadiumVisitStatus: [UUID: StadiumVisitStatus] { - var statusMap: [UUID: StadiumVisitStatus] = [:] + var stadiumVisitStatus: [String: StadiumVisitStatus] { + var statusMap: [String: StadiumVisitStatus] = [:] // Group visits by stadium - let visitsByStadium = Dictionary(grouping: visits.filter { $0.sportEnum == selectedSport }) { $0.stadiumUUID } + let visitsByStadium = Dictionary(grouping: visits.filter { $0.sportEnum == selectedSport }) { $0.stadiumId } for stadium in stadiums { if let stadiumVisits = visitsByStadium[stadium.id], !stadiumVisits.isEmpty { @@ -114,7 +114,7 @@ final class ProgressViewModel { .sorted { $0.visitDate > $1.visitDate } .prefix(10) .compactMap { visit -> VisitSummary? in - guard let stadium = stadiums.first(where: { $0.id == visit.stadiumUUID }), + guard let stadium = stadiums.first(where: { $0.id == visit.stadiumId }), let sport = visit.sportEnum else { return nil } diff --git a/SportsTime/Features/Progress/Views/ProgressMapView.swift b/SportsTime/Features/Progress/Views/ProgressMapView.swift index 037b834..0bc6484 100644 --- a/SportsTime/Features/Progress/Views/ProgressMapView.swift +++ b/SportsTime/Features/Progress/Views/ProgressMapView.swift @@ -12,7 +12,7 @@ import MapKit struct ProgressMapView: View { let stadiums: [Stadium] - let visitStatus: [UUID: StadiumVisitStatus] + let visitStatus: [String: StadiumVisitStatus] @Binding var selectedStadium: Stadium? // Fixed region for continental US - map is locked to this view diff --git a/SportsTime/Features/Progress/Views/StadiumVisitSheet.swift b/SportsTime/Features/Progress/Views/StadiumVisitSheet.swift index f1761e0..ca464b0 100644 --- a/SportsTime/Features/Progress/Views/StadiumVisitSheet.swift +++ b/SportsTime/Features/Progress/Views/StadiumVisitSheet.swift @@ -389,8 +389,7 @@ struct StadiumVisitSheet: View { // Create the visit let visit = StadiumVisit( - canonicalStadiumId: stadium.id.uuidString, // Simplified - in production use StadiumIdentityService - stadiumUUID: stadium.id, + stadiumId: stadium.id, stadiumNameAtVisit: stadium.name, visitDate: visitDate, sport: selectedSport, diff --git a/SportsTime/Features/Progress/Views/VisitDetailView.swift b/SportsTime/Features/Progress/Views/VisitDetailView.swift index 5ad359f..27bf1c2 100644 --- a/SportsTime/Features/Progress/Views/VisitDetailView.swift +++ b/SportsTime/Features/Progress/Views/VisitDetailView.swift @@ -523,7 +523,7 @@ extension VisitSource { #Preview { let stadium = Stadium( - id: UUID(), + id: "stadium_preview_oracle_park", name: "Oracle Park", city: "San Francisco", state: "CA", diff --git a/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift b/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift index 75eda2f..c479484 100644 --- a/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift +++ b/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift @@ -108,7 +108,7 @@ final class TripCreationViewModel { } // Games - var mustSeeGameIds: Set = [] + var mustSeeGameIds: Set = [] var availableGames: [RichGame] = [] var isLoadingGames: Bool = false @@ -134,7 +134,7 @@ final class TripCreationViewModel { var selectedRegions: Set = [.east, .central, .west] // Follow Team Mode - var followTeamId: UUID? + var followTeamId: String? var useHomeLocation: Bool = true // Game First Mode - Trip duration for sliding windows @@ -148,8 +148,8 @@ final class TripCreationViewModel { // MARK: - Cached Data - private var teams: [UUID: Team] = [:] - private var stadiums: [UUID: Stadium] = [:] + private var teams: [String: Team] = [:] + private var stadiums: [String: Stadium] = [:] private var games: [Game] = [] private(set) var currentPreferences: TripPreferences? @@ -454,7 +454,7 @@ final class TripCreationViewModel { } } - func toggleMustSeeGame(_ gameId: UUID) { + func toggleMustSeeGame(_ gameId: String) { if mustSeeGameIds.contains(gameId) { mustSeeGameIds.remove(gameId) } else { diff --git a/SportsTime/Features/Trip/Views/TimelineItemView.swift b/SportsTime/Features/Trip/Views/TimelineItemView.swift index 26b4c65..d0dd9a8 100644 --- a/SportsTime/Features/Trip/Views/TimelineItemView.swift +++ b/SportsTime/Features/Trip/Views/TimelineItemView.swift @@ -13,13 +13,13 @@ import SwiftUI /// Renders a single timeline item (stop, travel, or rest). struct TimelineItemView: View { let item: TimelineItem - let games: [UUID: RichGame] + let games: [String: RichGame] let isFirst: Bool let isLast: Bool init( item: TimelineItem, - games: [UUID: RichGame], + games: [String: RichGame], isFirst: Bool = false, isLast: Bool = false ) { @@ -122,7 +122,7 @@ struct TimelineItemView: View { struct StopItemContent: View { let stop: ItineraryStop - let games: [UUID: RichGame] + let games: [String: RichGame] private var gamesAtStop: [RichGame] { stop.games.compactMap { games[$0] } @@ -291,7 +291,7 @@ struct TimelineGameRow: View { /// Full timeline view for an itinerary option. struct TimelineView: View { let option: ItineraryOption - let games: [UUID: RichGame] + let games: [String: RichGame] private var timeline: [TimelineItem] { option.generateTimeline() @@ -316,7 +316,7 @@ struct TimelineView: View { /// Horizontal scrolling timeline for compact display. struct HorizontalTimelineView: View { let option: ItineraryOption - let games: [UUID: RichGame] + let games: [String: RichGame] private var timeline: [TimelineItem] { option.generateTimeline() @@ -368,7 +368,7 @@ struct HorizontalTimelineView: View { struct HorizontalTimelineItemView: View { let item: TimelineItem - let games: [UUID: RichGame] + let games: [String: RichGame] var body: some View { VStack(spacing: 4) { diff --git a/SportsTime/Features/Trip/Views/TripCreationView.swift b/SportsTime/Features/Trip/Views/TripCreationView.swift index f7bab58..2597a19 100644 --- a/SportsTime/Features/Trip/Views/TripCreationView.swift +++ b/SportsTime/Features/Trip/Views/TripCreationView.swift @@ -926,7 +926,7 @@ struct TripCreationView: View { ) } - private func buildGamesDictionary() -> [UUID: RichGame] { + private func buildGamesDictionary() -> [String: RichGame] { viewModel.availableGames.reduce(into: [:]) { $0[$1.id] = $1 } } } @@ -949,16 +949,16 @@ extension TripCreationViewModel.ViewState { struct GamePickerSheet: View { let games: [RichGame] - @Binding var selectedIds: Set + @Binding var selectedIds: Set @Environment(\.dismiss) private var dismiss @Environment(\.colorScheme) private var colorScheme @State private var expandedSports: Set = [] - @State private var expandedTeams: Set = [] + @State private var expandedTeams: Set = [] // Group games by Sport → Team (home team only to avoid duplicates) private var gamesBySport: [Sport: [TeamWithGames]] { - var result: [Sport: [UUID: TeamWithGames]] = [:] + var result: [Sport: [String: TeamWithGames]] = [:] for game in games { let sport = game.game.sport @@ -1063,9 +1063,9 @@ struct GamePickerSheet: View { struct SportSection: View { let sport: Sport let teams: [TeamWithGames] - @Binding var selectedIds: Set + @Binding var selectedIds: Set @Binding var expandedSports: Set - @Binding var expandedTeams: Set + @Binding var expandedTeams: Set let selectedCount: Int @Environment(\.colorScheme) private var colorScheme @@ -1146,8 +1146,8 @@ struct SportSection: View { struct TeamSection: View { let teamData: TeamWithGames - @Binding var selectedIds: Set - @Binding var expandedTeams: Set + @Binding var selectedIds: Set + @Binding var expandedTeams: Set @Environment(\.colorScheme) private var colorScheme @@ -1310,7 +1310,7 @@ struct TeamWithGames: Identifiable { let sport: Sport var games: [RichGame] - var id: UUID { team.id } + var id: String { team.id } var sortedGames: [RichGame] { games.sorted { $0.game.dateTime < $1.game.dateTime } @@ -1504,7 +1504,7 @@ enum CitiesFilter: Int, CaseIterable, Identifiable { struct TripOptionsView: View { let options: [ItineraryOption] - let games: [UUID: RichGame] + let games: [String: RichGame] let preferences: TripPreferences? let convertToTrip: (ItineraryOption) -> Trip @@ -1774,7 +1774,7 @@ struct TripOptionsView: View { struct TripOptionCard: View { let option: ItineraryOption - let games: [UUID: RichGame] + let games: [String: RichGame] let onSelect: () -> Void @Environment(\.colorScheme) private var colorScheme @@ -2393,7 +2393,7 @@ struct DayCell: View { // MARK: - Team Picker Sheet struct TeamPickerSheet: View { - @Binding var selectedTeamId: UUID? + @Binding var selectedTeamId: String? let teamsBySport: [Sport: [Team]] @Environment(\.dismiss) private var dismiss diff --git a/SportsTime/Features/Trip/Views/TripDetailView.swift b/SportsTime/Features/Trip/Views/TripDetailView.swift index 2e72580..c39c462 100644 --- a/SportsTime/Features/Trip/Views/TripDetailView.swift +++ b/SportsTime/Features/Trip/Views/TripDetailView.swift @@ -12,7 +12,7 @@ struct TripDetailView: View { @Environment(\.colorScheme) private var colorScheme let trip: Trip - let games: [UUID: RichGame] + let games: [String: RichGame] @State private var selectedDay: ItineraryDay? @State private var showExportSheet = false diff --git a/SportsTime/Planning/Engine/GameDAGRouter.swift b/SportsTime/Planning/Engine/GameDAGRouter.swift index 3bfbed2..40a04ab 100644 --- a/SportsTime/Planning/Engine/GameDAGRouter.swift +++ b/SportsTime/Planning/Engine/GameDAGRouter.swift @@ -80,7 +80,7 @@ enum GameDAGRouter { /// Composite key for exact deduplication var uniqueKey: String { - route.map { $0.id.uuidString }.joined(separator: "-") + route.map { $0.id }.joined(separator: "-") } } @@ -106,9 +106,9 @@ enum GameDAGRouter { /// static func findRoutes( games: [Game], - stadiums: [UUID: Stadium], + stadiums: [String: Stadium], constraints: DrivingConstraints, - anchorGameIds: Set = [], + anchorGameIds: Set = [], allowRepeatCities: Bool = true, beamWidth: Int = defaultBeamWidth ) -> [[Game]] { @@ -219,10 +219,10 @@ enum GameDAGRouter { /// Compatibility wrapper that matches GeographicRouteExplorer's interface. static func findAllSensibleRoutes( from games: [Game], - stadiums: [UUID: Stadium], - anchorGameIds: Set = [], + stadiums: [String: Stadium], + anchorGameIds: Set = [], allowRepeatCities: Bool = true, - stopBuilder: ([Game], [UUID: Stadium]) -> [ItineraryStop] + stopBuilder: ([Game], [String: Stadium]) -> [ItineraryStop] ) -> [[Game]] { let constraints = DrivingConstraints.default return findRoutes( @@ -244,7 +244,7 @@ enum GameDAGRouter { /// - Short duration AND long duration private static func selectDiverseRoutes( _ routes: [[Game]], - stadiums: [UUID: Stadium], + stadiums: [String: Stadium], maxCount: Int ) -> [[Game]] { guard !routes.isEmpty else { return [] } @@ -362,14 +362,14 @@ enum GameDAGRouter { /// Keeps routes that span the diversity space rather than just high-scoring ones. private static func diversityPrune( _ paths: [[Game]], - stadiums: [UUID: Stadium], + stadiums: [String: Stadium], targetCount: Int ) -> [[Game]] { // Remove exact duplicates first var uniquePaths: [[Game]] = [] var seen = Set() for path in paths { - let key = path.map { $0.id.uuidString }.joined(separator: "-") + let key = path.map { $0.id }.joined(separator: "-") if !seen.contains(key) { seen.insert(key) uniquePaths.append(path) @@ -425,7 +425,7 @@ enum GameDAGRouter { } /// Builds a profile for a route. - private static func buildProfile(for route: [Game], stadiums: [UUID: Stadium]) -> RouteProfile { + private static func buildProfile(for route: [Game], stadiums: [String: Stadium]) -> RouteProfile { let gameCount = route.count let cities = Set(route.compactMap { stadiums[$0.stadiumId]?.city }) let cityCount = cities.count @@ -488,7 +488,7 @@ enum GameDAGRouter { private static func canTransition( from: Game, to: Game, - stadiums: [UUID: Stadium], + stadiums: [String: Stadium], constraints: DrivingConstraints ) -> Bool { // Time must move forward @@ -562,7 +562,7 @@ enum GameDAGRouter { private static func estimateDistanceMiles( from: Game, to: Game, - stadiums: [UUID: Stadium] + stadiums: [String: Stadium] ) -> Double { if from.stadiumId == to.stadiumId { return 0 } diff --git a/SportsTime/Planning/Engine/ScenarioAPlanner.swift b/SportsTime/Planning/Engine/ScenarioAPlanner.swift index 350e720..8f81162 100644 --- a/SportsTime/Planning/Engine/ScenarioAPlanner.swift +++ b/SportsTime/Planning/Engine/ScenarioAPlanner.swift @@ -298,7 +298,7 @@ final class ScenarioAPlanner: ScenarioPlanner { /// private func buildStops( from games: [Game], - stadiums: [UUID: Stadium] + stadiums: [String: Stadium] ) -> [ItineraryStop] { guard !games.isEmpty else { return [] } @@ -308,7 +308,7 @@ final class ScenarioAPlanner: ScenarioPlanner { // Group consecutive games at the same stadium into stops // If you visit A, then B, then A again, that's 3 stops (A, B, A) var stops: [ItineraryStop] = [] - var currentStadiumId: UUID? = nil + var currentStadiumId: String? = nil var currentGames: [Game] = [] for game in sortedGames { @@ -340,8 +340,8 @@ final class ScenarioAPlanner: ScenarioPlanner { /// Creates an ItineraryStop from a group of games at the same stadium. private func createStop( from games: [Game], - stadiumId: UUID, - stadiums: [UUID: Stadium] + stadiumId: String, + stadiums: [String: Stadium] ) -> ItineraryStop? { guard !games.isEmpty else { return nil } @@ -380,7 +380,7 @@ final class ScenarioAPlanner: ScenarioPlanner { var unique: [[Game]] = [] for route in routes { - let key = route.map { $0.id.uuidString }.sorted().joined(separator: "-") + let key = route.map { $0.id }.sorted().joined(separator: "-") if !seen.contains(key) { seen.insert(key) unique.append(route) @@ -396,7 +396,7 @@ final class ScenarioAPlanner: ScenarioPlanner { /// This ensures we get diverse options from East, Central, and West coasts. private func findRoutesPerRegion( games: [Game], - stadiums: [UUID: Stadium], + stadiums: [String: Stadium], allowRepeatCities: Bool ) -> [[Game]] { // Partition games by region diff --git a/SportsTime/Planning/Engine/ScenarioBPlanner.swift b/SportsTime/Planning/Engine/ScenarioBPlanner.swift index 460e85d..c59d12c 100644 --- a/SportsTime/Planning/Engine/ScenarioBPlanner.swift +++ b/SportsTime/Planning/Engine/ScenarioBPlanner.swift @@ -316,7 +316,7 @@ final class ScenarioBPlanner: ScenarioPlanner { /// Creates separate stops when visiting the same city with other cities in between. private func buildStops( from games: [Game], - stadiums: [UUID: Stadium] + stadiums: [String: Stadium] ) -> [ItineraryStop] { guard !games.isEmpty else { return [] } @@ -325,7 +325,7 @@ final class ScenarioBPlanner: ScenarioPlanner { // Group consecutive games at the same stadium var stops: [ItineraryStop] = [] - var currentStadiumId: UUID? = nil + var currentStadiumId: String? = nil var currentGames: [Game] = [] for game in sortedGames { @@ -357,8 +357,8 @@ final class ScenarioBPlanner: ScenarioPlanner { /// Creates an ItineraryStop from a group of games at the same stadium. private func createStop( from games: [Game], - stadiumId: UUID, - stadiums: [UUID: Stadium] + stadiumId: String, + stadiums: [String: Stadium] ) -> ItineraryStop? { guard !games.isEmpty else { return nil } @@ -396,8 +396,8 @@ final class ScenarioBPlanner: ScenarioPlanner { /// For Scenario B, routes must still contain all anchor games. private func findRoutesPerRegion( games: [Game], - stadiums: [UUID: Stadium], - anchorGameIds: Set, + stadiums: [String: Stadium], + anchorGameIds: Set, allowRepeatCities: Bool ) -> [[Game]] { // First, determine which region(s) the anchor games are in @@ -459,7 +459,7 @@ final class ScenarioBPlanner: ScenarioPlanner { var unique: [[Game]] = [] for route in routes { - let key = route.map { $0.id.uuidString }.sorted().joined(separator: "-") + let key = route.map { $0.id }.sorted().joined(separator: "-") if !seen.contains(key) { seen.insert(key) unique.append(route) diff --git a/SportsTime/Planning/Engine/ScenarioCPlanner.swift b/SportsTime/Planning/Engine/ScenarioCPlanner.swift index 5ce5102..22799ae 100644 --- a/SportsTime/Planning/Engine/ScenarioCPlanner.swift +++ b/SportsTime/Planning/Engine/ScenarioCPlanner.swift @@ -272,7 +272,7 @@ final class ScenarioCPlanner: ScenarioPlanner { /// Finds all stadiums in a given city (case-insensitive match). private func findStadiumsInCity( cityName: String, - stadiums: [UUID: Stadium] + stadiums: [String: Stadium] ) -> [Stadium] { let normalizedCity = cityName.lowercased().trimmingCharacters(in: .whitespaces) return stadiums.values.filter { stadium in @@ -296,14 +296,14 @@ final class ScenarioCPlanner: ScenarioPlanner { private func findDirectionalStadiums( from start: CLLocationCoordinate2D, to end: CLLocationCoordinate2D, - stadiums: [UUID: Stadium] - ) -> Set { + stadiums: [String: Stadium] + ) -> Set { let directDistance = distanceBetween(start, end) // Allow detours up to 50% longer than direct distance let maxDetourDistance = directDistance * 1.5 - var directionalIds: Set = [] + var directionalIds: Set = [] for (id, stadium) in stadiums { let stadiumCoord = stadium.coordinate @@ -349,8 +349,8 @@ final class ScenarioCPlanner: ScenarioPlanner { /// Create a date range from start_game.date to end_game.date /// private func generateDateRanges( - startStadiumIds: Set, - endStadiumIds: Set, + startStadiumIds: Set, + endStadiumIds: Set, allGames: [Game], request: PlanningRequest ) -> [DateInterval] { @@ -417,7 +417,7 @@ final class ScenarioCPlanner: ScenarioPlanner { /// Creates separate stops when visiting the same city with other cities in between. private func buildStops( from games: [Game], - stadiums: [UUID: Stadium] + stadiums: [String: Stadium] ) -> [ItineraryStop] { guard !games.isEmpty else { return [] } @@ -426,7 +426,7 @@ final class ScenarioCPlanner: ScenarioPlanner { // Group consecutive games at the same stadium var stops: [ItineraryStop] = [] - var currentStadiumId: UUID? = nil + var currentStadiumId: String? = nil var currentGames: [Game] = [] for game in sortedGames { @@ -458,8 +458,8 @@ final class ScenarioCPlanner: ScenarioPlanner { /// Creates an ItineraryStop from a group of games at the same stadium. private func createStop( from games: [Game], - stadiumId: UUID, - stadiums: [UUID: Stadium] + stadiumId: String, + stadiums: [String: Stadium] ) -> ItineraryStop? { guard !games.isEmpty else { return nil } @@ -496,7 +496,7 @@ final class ScenarioCPlanner: ScenarioPlanner { start: LocationInput, end: LocationInput, games: [Game], - stadiums: [UUID: Stadium] + stadiums: [String: Stadium] ) -> [ItineraryStop] { var stops: [ItineraryStop] = [] diff --git a/SportsTime/Planning/Engine/ScenarioDPlanner.swift b/SportsTime/Planning/Engine/ScenarioDPlanner.swift index 3017f90..6a45bbb 100644 --- a/SportsTime/Planning/Engine/ScenarioDPlanner.swift +++ b/SportsTime/Planning/Engine/ScenarioDPlanner.swift @@ -275,7 +275,7 @@ final class ScenarioDPlanner: ScenarioPlanner { // MARK: - Team Filtering /// Filters games to those involving the followed team (home or away). - private func filterToTeam(_ games: [Game], teamId: UUID) -> [Game] { + private func filterToTeam(_ games: [Game], teamId: String) -> [Game] { games.filter { game in game.homeTeamId == teamId || game.awayTeamId == teamId } @@ -287,7 +287,7 @@ final class ScenarioDPlanner: ScenarioPlanner { private func applyRepeatCityFilter( _ games: [Game], allowRepeat: Bool, - stadiums: [UUID: Stadium] + stadiums: [String: Stadium] ) -> [Game] { guard !allowRepeat else { print("🔍 applyRepeatCityFilter: allowRepeat=true, returning all \(games.count) games") @@ -317,14 +317,14 @@ final class ScenarioDPlanner: ScenarioPlanner { /// Same logic as ScenarioAPlanner. private func buildStops( from games: [Game], - stadiums: [UUID: Stadium] + stadiums: [String: Stadium] ) -> [ItineraryStop] { guard !games.isEmpty else { return [] } let sortedGames = games.sorted { $0.startTime < $1.startTime } var stops: [ItineraryStop] = [] - var currentStadiumId: UUID? = nil + var currentStadiumId: String? = nil var currentGames: [Game] = [] for game in sortedGames { @@ -354,8 +354,8 @@ final class ScenarioDPlanner: ScenarioPlanner { /// Creates an ItineraryStop from a group of games at the same stadium. private func createStop( from games: [Game], - stadiumId: UUID, - stadiums: [UUID: Stadium] + stadiumId: String, + stadiums: [String: Stadium] ) -> ItineraryStop? { guard !games.isEmpty else { return nil } @@ -393,7 +393,7 @@ final class ScenarioDPlanner: ScenarioPlanner { var unique: [[Game]] = [] for route in routes { - let key = route.map { $0.id.uuidString }.sorted().joined(separator: "-") + let key = route.map { $0.id }.sorted().joined(separator: "-") if !seen.contains(key) { seen.insert(key) unique.append(route) diff --git a/SportsTime/Planning/Engine/ScenarioPlanner.swift b/SportsTime/Planning/Engine/ScenarioPlanner.swift index d80bdfd..e37f377 100644 --- a/SportsTime/Planning/Engine/ScenarioPlanner.swift +++ b/SportsTime/Planning/Engine/ScenarioPlanner.swift @@ -23,7 +23,7 @@ enum ScenarioPlannerFactory { /// Creates the appropriate planner based on the request inputs static func planner(for request: PlanningRequest) -> ScenarioPlanner { print("🔍 ScenarioPlannerFactory: Selecting planner...") - print(" - followTeamId: \(request.preferences.followTeamId?.uuidString ?? "nil")") + print(" - followTeamId: \(request.preferences.followTeamId ?? "nil")") print(" - selectedGames.count: \(request.selectedGames.count)") print(" - startLocation: \(request.startLocation?.name ?? "nil")") print(" - endLocation: \(request.endLocation?.name ?? "nil")") diff --git a/SportsTime/Planning/Models/PlanningModels.swift b/SportsTime/Planning/Models/PlanningModels.swift index 62d5572..6ef737e 100644 --- a/SportsTime/Planning/Models/PlanningModels.swift +++ b/SportsTime/Planning/Models/PlanningModels.swift @@ -251,7 +251,7 @@ struct ItineraryStop: Identifiable, Hashable { let city: String let state: String let coordinate: CLLocationCoordinate2D? - let games: [UUID] + let games: [String] // Canonical game IDs let arrivalDate: Date let departureDate: Date let location: LocationInput @@ -490,8 +490,8 @@ extension ItineraryOption { struct PlanningRequest { let preferences: TripPreferences let availableGames: [Game] - let teams: [UUID: Team] - let stadiums: [UUID: Stadium] + let teams: [String: Team] // Keyed by canonical ID + let stadiums: [String: Stadium] // Keyed by canonical ID // MARK: - Computed Properties for Engine diff --git a/SportsTimeTests/Fixtures/FixtureGenerator.swift b/SportsTimeTests/Fixtures/FixtureGenerator.swift index baea180..305fcfc 100644 --- a/SportsTimeTests/Fixtures/FixtureGenerator.swift +++ b/SportsTimeTests/Fixtures/FixtureGenerator.swift @@ -68,8 +68,8 @@ struct FixtureGenerator { let stadiums: [Stadium] let teams: [Team] let games: [Game] - let stadiumsById: [UUID: Stadium] - let teamsById: [UUID: Team] + let stadiumsById: [String: Stadium] + let teamsById: [String: Team] func richGame(from game: Game) -> RichGame? { guard let homeTeam = teamsById[game.homeTeamId], @@ -168,7 +168,7 @@ struct FixtureGenerator { return cities.enumerated().map { index, city in let sport = config.sports.randomElement(using: &rng) ?? .mlb return Stadium( - id: UUID(), + id: "stadium_test_\(city.name.lowercased().replacingOccurrences(of: " ", with: "_"))_\(index)", name: "\(city.name) \(sport.rawValue) Stadium", city: city.name, state: city.state, @@ -203,8 +203,9 @@ struct FixtureGenerator { usedNames.insert("\(stadium.city) \(teamName)") + let teamId = "team_test_\(stadium.city.lowercased().replacingOccurrences(of: " ", with: "_"))_\(teamName.lowercased())_\(teams.count)" teams.append(Team( - id: UUID(), + id: teamId, name: teamName, abbreviation: String(teamName.prefix(3)).uppercased(), sport: stadium.sport, @@ -251,8 +252,9 @@ struct FixtureGenerator { let hour = Int.random(in: 13...21, using: &rng) let gameDateTime = calendar.date(bySettingHour: hour, minute: 0, second: 0, of: gameDate)! + let gameId = "game_test_\(games.count)_\(homeTeam.abbreviation)_\(awayTeam.abbreviation)" games.append(Game( - id: UUID(), + id: gameId, homeTeamId: homeTeam.id, awayTeamId: awayTeam.id, stadiumId: stadium.id, @@ -331,8 +333,8 @@ struct FixtureGenerator { coordinate: CLLocationCoordinate2D(latitude: city.lat, longitude: city.lon), arrivalDate: currentDate, departureDate: departureDate, - games: [UUID()], - stadium: UUID() + games: ["game_test_\(i)"], + stadium: "stadium_test_\(i)" )) currentDate = departureDate @@ -351,12 +353,12 @@ struct FixtureGenerator { cities: [(name: String, lat: Double, lon: Double)], rng: inout SeededRandomNumberGenerator ) -> [Game] { - cities.map { city in - let stadiumId = UUID() + cities.enumerated().map { index, city in + let stadiumId = "stadium_conflict_\(city.name.lowercased().replacingOccurrences(of: " ", with: "_"))_\(index)" return Game( - id: UUID(), - homeTeamId: UUID(), - awayTeamId: UUID(), + id: "game_conflict_\(index)_\(city.name.lowercased().replacingOccurrences(of: " ", with: "_"))", + homeTeamId: "team_conflict_home_\(index)", + awayTeamId: "team_conflict_away_\(index)", stadiumId: stadiumId, dateTime: date, sport: .mlb, @@ -367,7 +369,7 @@ struct FixtureGenerator { /// Generate a stadium at a specific location static func makeStadium( - id: UUID = UUID(), + id: String = "stadium_test_\(UUID().uuidString)", name: String = "Test Stadium", city: String = "Test City", state: String = "TS", @@ -390,12 +392,12 @@ struct FixtureGenerator { /// Generate a team static func makeTeam( - id: UUID = UUID(), + id: String = "team_test_\(UUID().uuidString)", name: String = "Test Team", abbreviation: String = "TST", sport: Sport = .mlb, city: String = "Test City", - stadiumId: UUID + stadiumId: String ) -> Team { Team( id: id, @@ -409,10 +411,10 @@ struct FixtureGenerator { /// Generate a game static func makeGame( - id: UUID = UUID(), - homeTeamId: UUID, - awayTeamId: UUID, - stadiumId: UUID, + id: String = "game_test_\(UUID().uuidString)", + homeTeamId: String, + awayTeamId: String, + stadiumId: String, dateTime: Date = Date(), sport: Sport = .mlb, season: String = "2026", @@ -454,8 +456,8 @@ struct FixtureGenerator { coordinate: CLLocationCoordinate2D? = nil, arrivalDate: Date = Date(), departureDate: Date? = nil, - games: [UUID] = [], - stadium: UUID? = nil, + games: [String] = [], + stadium: String? = nil, isRestDay: Bool = false ) -> TripStop { TripStop( diff --git a/SportsTimeTests/Helpers/BruteForceRouteVerifier.swift b/SportsTimeTests/Helpers/BruteForceRouteVerifier.swift index c28f147..aca9f66 100644 --- a/SportsTimeTests/Helpers/BruteForceRouteVerifier.swift +++ b/SportsTimeTests/Helpers/BruteForceRouteVerifier.swift @@ -20,7 +20,7 @@ struct BruteForceRouteVerifier { let isOptimal: Bool let proposedRouteDistance: Double let optimalRouteDistance: Double - let optimalRoute: [UUID]? + let optimalRoute: [String]? let improvement: Double? // Percentage improvement if not optimal let permutationsChecked: Int @@ -39,8 +39,8 @@ struct BruteForceRouteVerifier { /// - tolerance: Percentage tolerance for "near-optimal" (default 0 = must be exactly optimal) /// - Returns: Verification result static func verify( - proposedRoute: [UUID], - stops: [UUID: CLLocationCoordinate2D], + proposedRoute: [String], + stops: [String: CLLocationCoordinate2D], tolerance: Double = 0 ) -> VerificationResult { guard proposedRoute.count <= TestConstants.bruteForceMaxStops else { @@ -64,7 +64,7 @@ struct BruteForceRouteVerifier { // Find optimal route by checking all permutations let allPermutations = permutations(of: proposedRoute) var optimalDistance = Double.infinity - var optimalRoute: [UUID] = [] + var optimalRoute: [String] = [] for permutation in allPermutations { let distance = calculateRouteDistance(permutation, stops: stops) @@ -102,10 +102,10 @@ struct BruteForceRouteVerifier { /// Verify a route is optimal with a fixed start and end point static func verifyWithFixedEndpoints( - proposedRoute: [UUID], - stops: [UUID: CLLocationCoordinate2D], - startId: UUID, - endId: UUID, + proposedRoute: [String], + stops: [String: CLLocationCoordinate2D], + startId: String, + endId: String, tolerance: Double = 0 ) -> VerificationResult { guard proposedRoute.first == startId && proposedRoute.last == endId else { @@ -132,7 +132,7 @@ struct BruteForceRouteVerifier { // Generate all permutations of intermediate stops let allPermutations = permutations(of: Array(intermediateStops)) var optimalDistance = Double.infinity - var optimalRoute: [UUID] = [] + var optimalRoute: [String] = [] for permutation in allPermutations { var fullRoute = [startId] @@ -172,8 +172,8 @@ struct BruteForceRouteVerifier { /// Check if there's an obviously better route (significantly shorter) static func hasObviouslyBetterRoute( - proposedRoute: [UUID], - stops: [UUID: CLLocationCoordinate2D], + proposedRoute: [String], + stops: [String: CLLocationCoordinate2D], threshold: Double = 0.1 // 10% improvement threshold ) -> (hasBetter: Bool, improvement: Double?) { let result = verify(proposedRoute: proposedRoute, stops: stops, tolerance: threshold) @@ -184,8 +184,8 @@ struct BruteForceRouteVerifier { /// Calculate total route distance using haversine formula static func calculateRouteDistance( - _ route: [UUID], - stops: [UUID: CLLocationCoordinate2D] + _ route: [String], + stops: [String: CLLocationCoordinate2D] ) -> Double { guard route.count >= 2 else { return 0 } @@ -263,15 +263,15 @@ struct BruteForceRouteVerifier { extension BruteForceRouteVerifier { /// Verify a trip's route is optimal static func verifyTrip(_ trip: Trip) -> VerificationResult { - var stops: [UUID: CLLocationCoordinate2D] = [:] + var stops: [String: CLLocationCoordinate2D] = [:] for stop in trip.stops { if let coord = stop.coordinate { - stops[stop.id] = coord + stops[stop.id.uuidString] = coord } } - let routeIds = trip.stops.map { $0.id } + let routeIds = trip.stops.map { $0.id.uuidString } return verify(proposedRoute: routeIds, stops: stops) } diff --git a/SportsTimeTests/Mocks/MockAppDataProvider.swift b/SportsTimeTests/Mocks/MockAppDataProvider.swift index 3278d6f..0650c4a 100644 --- a/SportsTimeTests/Mocks/MockAppDataProvider.swift +++ b/SportsTimeTests/Mocks/MockAppDataProvider.swift @@ -24,10 +24,10 @@ final class MockAppDataProvider: ObservableObject { // MARK: - Internal Storage - private var teamsById: [UUID: Team] = [:] - private var stadiumsById: [UUID: Stadium] = [:] + private var teamsById: [String: Team] = [:] + private var stadiumsById: [String: Stadium] = [:] private var games: [Game] = [] - private var gamesById: [UUID: Game] = [:] + private var gamesById: [String: Game] = [:] // MARK: - Configuration @@ -140,11 +140,11 @@ final class MockAppDataProvider: ObservableObject { // MARK: - Data Access - func team(for id: UUID) -> Team? { + func team(for id: String) -> Team? { teamsById[id] } - func stadium(for id: UUID) -> Stadium? { + func stadium(for id: String) -> Stadium? { stadiumsById[id] } @@ -169,7 +169,7 @@ final class MockAppDataProvider: ObservableObject { }.sorted { $0.dateTime < $1.dateTime } } - func fetchGame(by id: UUID) async throws -> Game? { + func fetchGame(by id: String) async throws -> Game? { await simulateLatency() if config.shouldFailOnFetch { diff --git a/SportsTimeTests/Mocks/MockCloudKitService.swift b/SportsTimeTests/Mocks/MockCloudKitService.swift index 832d721..385ee50 100644 --- a/SportsTimeTests/Mocks/MockCloudKitService.swift +++ b/SportsTimeTests/Mocks/MockCloudKitService.swift @@ -155,7 +155,7 @@ actor MockCloudKitService { }.sorted { $0.dateTime < $1.dateTime } } - func fetchGame(by id: UUID) async throws -> Game? { + func fetchGame(by id: String) async throws -> Game? { try await simulateNetwork() return games.first { $0.id == id } } @@ -167,7 +167,7 @@ actor MockCloudKitService { return stadiums.map { stadium in CloudKitService.SyncStadium( stadium: stadium, - canonicalId: stadium.id.uuidString + canonicalId: stadium.id ) } } @@ -177,8 +177,8 @@ actor MockCloudKitService { return teams.filter { $0.sport == sport }.map { team in CloudKitService.SyncTeam( team: team, - canonicalId: team.id.uuidString, - stadiumCanonicalId: team.stadiumId.uuidString + canonicalId: team.id, + stadiumCanonicalId: team.stadiumId ) } } @@ -197,10 +197,10 @@ actor MockCloudKitService { }.map { game in CloudKitService.SyncGame( game: game, - canonicalId: game.id.uuidString, - homeTeamCanonicalId: game.homeTeamId.uuidString, - awayTeamCanonicalId: game.awayTeamId.uuidString, - stadiumCanonicalId: game.stadiumId.uuidString + canonicalId: game.id, + homeTeamCanonicalId: game.homeTeamId, + awayTeamCanonicalId: game.awayTeamId, + stadiumCanonicalId: game.stadiumId ) } } diff --git a/SportsTimeTests/Planning/ConcurrencyTests.swift b/SportsTimeTests/Planning/ConcurrencyTests.swift index 58caf7c..d6c9475 100644 --- a/SportsTimeTests/Planning/ConcurrencyTests.swift +++ b/SportsTimeTests/Planning/ConcurrencyTests.swift @@ -30,7 +30,7 @@ struct ConcurrencyTests { /// Creates a stadium at a known location private func makeStadium( - id: UUID = UUID(), + id: String = "stadium_test_\(UUID().uuidString)", city: String, lat: Double, lon: Double, @@ -50,10 +50,10 @@ struct ConcurrencyTests { /// Creates a game at a stadium private func makeGame( - id: UUID = UUID(), - stadiumId: UUID, - homeTeamId: UUID = UUID(), - awayTeamId: UUID = UUID(), + id: String = "game_test_\(UUID().uuidString)", + stadiumId: String, + homeTeamId: String = "team_test_\(UUID().uuidString)", + awayTeamId: String = "team_test_\(UUID().uuidString)", dateTime: Date, sport: Sport = .mlb ) -> Game { @@ -73,7 +73,7 @@ struct ConcurrencyTests { startDate: Date, endDate: Date, games: [Game], - stadiums: [UUID: Stadium] + stadiums: [String: Stadium] ) -> PlanningRequest { let preferences = TripPreferences( planningMode: .dateRange, @@ -107,8 +107,8 @@ struct ConcurrencyTests { let pair = cityPairs[requestIndex % cityPairs.count] - let stadium1Id = UUID() - let stadium2Id = UUID() + let stadium1Id = "stadium_1_\(UUID().uuidString)" + let stadium2Id = "stadium_2_\(UUID().uuidString)" let stadium1 = makeStadium(id: stadium1Id, city: pair.city1.0, lat: pair.city1.1, lon: pair.city1.2) let stadium2 = makeStadium(id: stadium2Id, city: pair.city2.0, lat: pair.city2.1, lon: pair.city2.2) diff --git a/SportsTimeTests/Planning/EdgeCaseTests.swift b/SportsTimeTests/Planning/EdgeCaseTests.swift index 9e2b935..1db8051 100644 --- a/SportsTimeTests/Planning/EdgeCaseTests.swift +++ b/SportsTimeTests/Planning/EdgeCaseTests.swift @@ -31,7 +31,7 @@ struct EdgeCaseTests { /// Creates a stadium at a known location private func makeStadium( - id: UUID = UUID(), + id: String = "stadium_test_\(UUID().uuidString)", city: String, state: String = "ST", lat: Double, @@ -52,10 +52,10 @@ struct EdgeCaseTests { /// Creates a game at a stadium private func makeGame( - id: UUID = UUID(), - stadiumId: UUID, - homeTeamId: UUID = UUID(), - awayTeamId: UUID = UUID(), + id: String = "game_test_\(UUID().uuidString)", + stadiumId: String, + homeTeamId: String = "team_test_\(UUID().uuidString)", + awayTeamId: String = "team_test_\(UUID().uuidString)", dateTime: Date, sport: Sport = .mlb ) -> Game { @@ -75,7 +75,7 @@ struct EdgeCaseTests { city: String, state: String = "ST", coordinate: CLLocationCoordinate2D? = nil, - games: [UUID] = [], + games: [String] = [], arrivalDate: Date = Date() ) -> ItineraryStop { ItineraryStop( @@ -95,8 +95,8 @@ struct EdgeCaseTests { @Test("11.1 - Nil stadium ID handled gracefully") func test_nilStadium_HandlesGracefully() { // Setup: Create games where stadium lookup would return nil - let validStadiumId = UUID() - let nonExistentStadiumId = UUID() + let validStadiumId = "stadium_valid_\(UUID().uuidString)" + let nonExistentStadiumId = "stadium_nonexistent_\(UUID().uuidString)" let chicago = makeStadium(id: validStadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [validStadiumId: chicago] @@ -133,7 +133,7 @@ struct EdgeCaseTests { @Test("11.2 - Malformed date handled gracefully") func test_malformedDate_HandlesGracefully() { // Setup: Create games with dates at extremes - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: chicago] @@ -172,9 +172,9 @@ struct EdgeCaseTests { @Test("11.3 - Invalid coordinates handled gracefully") func test_invalidCoordinates_HandlesGracefully() { // Setup: Create stadiums with invalid coordinates - let validId = UUID() - let invalidLatId = UUID() - let invalidLonId = UUID() + let validId = "stadium_valid_\(UUID().uuidString)" + let invalidLatId = "stadium_invalidlat_\(UUID().uuidString)" + let invalidLonId = "stadium_invalidlon_\(UUID().uuidString)" // Valid stadium let validStadium = makeStadium(id: validId, city: "Chicago", lat: 41.8781, lon: -87.6298) @@ -239,7 +239,7 @@ struct EdgeCaseTests { @Test("11.4 - Missing required fields handled gracefully") func test_missingRequiredFields_HandlesGracefully() { // Setup: Test with empty games array - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: chicago] @@ -258,7 +258,7 @@ struct EdgeCaseTests { // Test with empty stadiums dictionary let game = makeGame(stadiumId: stadiumId, dateTime: makeDate(day: 5, hour: 19)) - let emptyStadiums: [UUID: Stadium] = [:] + let emptyStadiums: [String: Stadium] = [:] let routes2 = GameDAGRouter.findRoutes( games: [game], @@ -271,9 +271,9 @@ struct EdgeCaseTests { // Test with mismatched team IDs (homeTeamId and awayTeamId don't exist) let game2 = Game( - id: UUID(), - homeTeamId: UUID(), // Non-existent team - awayTeamId: UUID(), // Non-existent team + id: "game_test_\(UUID().uuidString)", + homeTeamId: "team_nonexistent_\(UUID().uuidString)", // Non-existent team + awayTeamId: "team_nonexistent_\(UUID().uuidString)", // Non-existent team stadiumId: stadiumId, dateTime: makeDate(day: 5, hour: 19), sport: .mlb, @@ -298,8 +298,8 @@ struct EdgeCaseTests { // Default: 8 hours/day * 60 mph * 2 days = 960 miles max // With 1.3 road factor, haversine distance should be 960/1.3 ≈ 738 miles - let stadiumId1 = UUID() - let stadiumId2 = UUID() + let stadiumId1 = "stadium_1_\(UUID().uuidString)" + let stadiumId2 = "stadium_2_\(UUID().uuidString)" // NYC and Chicago are about 790 miles apart (haversine) // With road factor 1.3, that's ~1027 road miles @@ -344,8 +344,8 @@ struct EdgeCaseTests { // NYC to LA is ~2,451 miles haversine, ~3,186 with road factor // At 60 mph, that's ~53 hours - way over 16 hour limit - let stadiumId1 = UUID() - let stadiumId2 = UUID() + let stadiumId1 = "stadium_1_\(UUID().uuidString)" + let stadiumId2 = "stadium_2_\(UUID().uuidString)" let nyc = makeStadium(id: stadiumId1, city: "New York", lat: 40.7128, lon: -73.9352) let la = makeStadium(id: stadiumId2, city: "Los Angeles", lat: 34.0522, lon: -118.2437) @@ -398,7 +398,7 @@ struct EdgeCaseTests { // Calculate a point exactly 50 miles south (along a corridor) // 1 degree of latitude ≈ 69 miles // 50 miles ≈ 0.725 degrees - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let exactlyAtBoundary = makeStadium( id: stadiumId, city: "BoundaryCity", @@ -434,7 +434,7 @@ struct EdgeCaseTests { // Calculate a point 51 miles south (just outside the radius) // 1 degree of latitude ≈ 69 miles // 51 miles ≈ 0.739 degrees - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let justOutsideBoundary = makeStadium( id: stadiumId, city: "OutsideCity", @@ -464,8 +464,8 @@ struct EdgeCaseTests { @Test("11.9 - Game in different time zone normalizes correctly") func test_gameInDifferentTimeZone_NormalizesToUTC() { // Setup: Create games in different time zones - let stadiumId1 = UUID() - let stadiumId2 = UUID() + let stadiumId1 = "stadium_1_\(UUID().uuidString)" + let stadiumId2 = "stadium_2_\(UUID().uuidString)" let nyc = makeStadium(id: stadiumId1, city: "New York", lat: 40.7128, lon: -73.9352) let la = makeStadium(id: stadiumId2, city: "Los Angeles", lat: 34.0522, lon: -118.2437) @@ -508,7 +508,7 @@ struct EdgeCaseTests { @Test("11.10 - DST spring forward handled correctly") func test_dstSpringForward_HandlesCorrectly() { // Setup: Test around DST transition (Spring forward: March 8, 2026, 2 AM -> 3 AM) - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: chicago] @@ -553,7 +553,7 @@ struct EdgeCaseTests { @Test("11.11 - DST fall back handled correctly") func test_dstFallBack_HandlesCorrectly() { // Setup: Test around DST transition (Fall back: November 1, 2026, 2 AM -> 1 AM) - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let chicago = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: chicago] diff --git a/SportsTimeTests/Planning/GameDAGRouterTests.swift b/SportsTimeTests/Planning/GameDAGRouterTests.swift index 7a5685c..c648bea 100644 --- a/SportsTimeTests/Planning/GameDAGRouterTests.swift +++ b/SportsTimeTests/Planning/GameDAGRouterTests.swift @@ -29,7 +29,7 @@ struct GameDAGRouterTests { // Create a stadium at a known location private func makeStadium( - id: UUID = UUID(), + id: String = "stadium_test_\(UUID().uuidString)", city: String, lat: Double, lon: Double @@ -48,14 +48,14 @@ struct GameDAGRouterTests { // Create a game at a stadium private func makeGame( - id: UUID = UUID(), - stadiumId: UUID, + id: String = "game_test_\(UUID().uuidString)", + stadiumId: String, dateTime: Date ) -> Game { Game( id: id, - homeTeamId: UUID(), - awayTeamId: UUID(), + homeTeamId: "team_test_\(UUID().uuidString)", + awayTeamId: "team_test_\(UUID().uuidString)", stadiumId: stadiumId, dateTime: dateTime, sport: .mlb, @@ -78,7 +78,7 @@ struct GameDAGRouterTests { @Test("2.2 - Single game returns single route") func test_findRoutes_SingleGame_ReturnsSingleRoute() { - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let game = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1)) @@ -95,7 +95,7 @@ struct GameDAGRouterTests { @Test("2.3 - Single game with matching anchor returns single route") func test_findRoutes_SingleGame_WithMatchingAnchor_ReturnsSingleRoute() { - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let game = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1)) @@ -112,10 +112,10 @@ struct GameDAGRouterTests { @Test("2.4 - Single game with non-matching anchor returns empty") func test_findRoutes_SingleGame_WithNonMatchingAnchor_ReturnsEmpty() { - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let game = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1)) - let nonExistentAnchor = UUID() + let nonExistentAnchor = "stadium_nonexistent_\(UUID().uuidString)" let routes = GameDAGRouter.findRoutes( games: [game], @@ -132,8 +132,8 @@ struct GameDAGRouterTests { @Test("2.5 - Two games with feasible transition returns both in order") func test_findRoutes_TwoGames_FeasibleTransition_ReturnsBothInOrder() { // Chicago to Milwaukee is ~90 miles - easily feasible - let chicagoStadiumId = UUID() - let milwaukeeStadiumId = UUID() + let chicagoStadiumId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeStadiumId = "stadium_milwaukee_\(UUID().uuidString)" let chicagoStadium = makeStadium(id: chicagoStadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukeeStadium = makeStadium(id: milwaukeeStadiumId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -162,8 +162,8 @@ struct GameDAGRouterTests { @Test("2.6 - Two games with infeasible transition returns separate routes") func test_findRoutes_TwoGames_InfeasibleTransition_ReturnsSeparateRoutes() { // NYC to LA on same day is infeasible - let nycStadiumId = UUID() - let laStadiumId = UUID() + let nycStadiumId = "stadium_nyc_\(UUID().uuidString)" + let laStadiumId = "stadium_la_\(UUID().uuidString)" let nycStadium = makeStadium(id: nycStadiumId, city: "New York", lat: 40.7128, lon: -73.9352) let laStadium = makeStadium(id: laStadiumId, city: "Los Angeles", lat: 34.0522, lon: -118.2437) @@ -191,7 +191,7 @@ struct GameDAGRouterTests { @Test("2.7 - Two games same stadium same day (doubleheader) succeeds") func test_findRoutes_TwoGames_SameStadiumSameDay_Succeeds() { - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) // Doubleheader: 1pm and 7pm same day, same stadium @@ -219,7 +219,7 @@ struct GameDAGRouterTests { @Test("2.8 - With anchors only returns routes containing all anchors") func test_findRoutes_WithAnchors_OnlyReturnsRoutesContainingAllAnchors() { - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1)) @@ -246,8 +246,8 @@ struct GameDAGRouterTests { @Test("2.9 - Impossible anchors returns empty") func test_findRoutes_ImpossibleAnchors_ReturnsEmpty() { // Two anchors at opposite ends of country on same day - impossible to attend both - let nycStadiumId = UUID() - let laStadiumId = UUID() + let nycStadiumId = "stadium_nyc_\(UUID().uuidString)" + let laStadiumId = "stadium_la_\(UUID().uuidString)" let nycStadium = makeStadium(id: nycStadiumId, city: "New York", lat: 40.7128, lon: -73.9352) let laStadium = makeStadium(id: laStadiumId, city: "Los Angeles", lat: 34.0522, lon: -118.2437) @@ -271,9 +271,9 @@ struct GameDAGRouterTests { @Test("2.10 - Multiple anchors route must contain all") func test_findRoutes_MultipleAnchors_RouteMustContainAll() { // Three games in nearby cities over 3 days - all feasible - let chicagoId = UUID() - let milwaukeeId = UUID() - let detroitId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" + let detroitId = "stadium_detroit_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -306,7 +306,7 @@ struct GameDAGRouterTests { @Test("2.11 - Allow repeat cities same city multiple days allowed") func test_findRoutes_AllowRepeatCities_SameCityMultipleDays_Allowed() { - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) // Three games in Chicago over 3 days @@ -330,8 +330,8 @@ struct GameDAGRouterTests { @Test("2.12 - Disallow repeat cities skips second visit") func test_findRoutes_DisallowRepeatCities_SkipsSecondVisit() { - let chicagoId = UUID() - let milwaukeeId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -360,7 +360,7 @@ struct GameDAGRouterTests { @Test("2.13 - Disallow repeat cities only option is repeat overrides with warning") func test_findRoutes_DisallowRepeatCities_OnlyOptionIsRepeat_OverridesWithWarning() { // When only games available are in the same city, we still need to produce routes - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) // Only Chicago games available @@ -388,8 +388,8 @@ struct GameDAGRouterTests { @Test("2.14 - Exceeds max daily driving transition rejected") func test_findRoutes_ExceedsMaxDailyDriving_TransitionRejected() { // NYC to Denver is ~1,800 miles, way over 8 hours of driving (480 miles at 60mph) - let nycId = UUID() - let denverId = UUID() + let nycId = "stadium_nyc_\(UUID().uuidString)" + let denverId = "stadium_denver_\(UUID().uuidString)" let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352) let denver = makeStadium(id: denverId, city: "Denver", lat: 39.7392, lon: -104.9903) @@ -417,8 +417,8 @@ struct GameDAGRouterTests { @Test("2.15 - Multi-day drive allowed if within daily limits") func test_findRoutes_MultiDayDrive_Allowed_IfWithinDailyLimits() { // NYC to Chicago is ~790 miles - doable over multiple days - let nycId = UUID() - let chicagoId = UUID() + let nycId = "stadium_nyc_\(UUID().uuidString)" + let chicagoId = "stadium_chicago_\(UUID().uuidString)" let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352) let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) @@ -443,8 +443,8 @@ struct GameDAGRouterTests { @Test("2.16 - Same day different stadiums checks available time") func test_findRoutes_SameDayDifferentStadiums_ChecksAvailableTime() { // Chicago to Milwaukee is ~90 miles (~1.5 hours driving) - let chicagoId = UUID() - let milwaukeeId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -484,7 +484,7 @@ struct GameDAGRouterTests { @Test("2.17 - Max day lookahead respects limit") func test_findRoutes_MaxDayLookahead_RespectsLimit() { // Games more than 5 days apart should not connect directly - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1)) @@ -514,7 +514,7 @@ struct GameDAGRouterTests { @Test("2.18 - DST transition handles correctly") func test_findRoutes_DSTTransition_HandlesCorrectly() { // Test around DST transition (March 9, 2026 - spring forward) - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) // Create dates around DST transition @@ -550,7 +550,7 @@ struct GameDAGRouterTests { @Test("2.19 - Midnight game assigns to correct day") func test_findRoutes_MidnightGame_AssignsToCorrectDay() { - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) // Game at 12:05 AM belongs to the new day @@ -583,7 +583,7 @@ struct GameDAGRouterTests { @Test("2.20 - Select diverse routes includes short and long trips") func test_selectDiverseRoutes_ShortAndLongTrips_BothRepresented() { // Create a mix of games over a week - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) var games: [Game] = [] @@ -611,9 +611,9 @@ struct GameDAGRouterTests { @Test("2.21 - Select diverse routes includes high and low mileage") func test_selectDiverseRoutes_HighAndLowMileage_BothRepresented() { // Create games in both nearby and distant cities - let chicagoId = UUID() - let milwaukeeId = UUID() - let laId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" + let laId = "stadium_la_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -647,11 +647,11 @@ struct GameDAGRouterTests { ("Cleveland", 41.4993, -81.6944), ] - var stadiums: [UUID: Stadium] = [:] + var stadiums: [String: Stadium] = [:] var games: [Game] = [] for (index, city) in cities.enumerated() { - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" stadiums[stadiumId] = makeStadium(id: stadiumId, city: city.0, lat: city.1, lon: city.2) games.append(makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: index + 1))) } @@ -675,7 +675,7 @@ struct GameDAGRouterTests { @Test("2.23 - Select diverse routes deduplicates") func test_selectDiverseRoutes_DuplicateRoutes_Deduplicated() { - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let game1 = makeGame(stadiumId: stadiumId, dateTime: gameDate(daysFromNow: 1)) @@ -693,7 +693,7 @@ struct GameDAGRouterTests { // Check for duplicates var seen = Set() for route in routes { - let key = route.map { $0.id.uuidString }.joined(separator: "-") + let key = route.map { $0.id }.joined(separator: "-") #expect(!seen.contains(key), "Routes should be deduplicated") seen.insert(key) } @@ -704,9 +704,9 @@ struct GameDAGRouterTests { @Test("2.24 - Graph with potential cycle handles silently") func test_findRoutes_GraphWithPotentialCycle_HandlesSilently() { // Create a scenario where a naive algorithm might get stuck in a loop - let chicagoId = UUID() - let milwaukeeId = UUID() - let detroitId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" + let detroitId = "stadium_detroit_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) diff --git a/SportsTimeTests/Planning/ItineraryBuilderTests.swift b/SportsTimeTests/Planning/ItineraryBuilderTests.swift index 6852687..0574ced 100644 --- a/SportsTimeTests/Planning/ItineraryBuilderTests.swift +++ b/SportsTimeTests/Planning/ItineraryBuilderTests.swift @@ -29,7 +29,7 @@ struct ItineraryBuilderTests { @Test("Single stop creates itinerary with one stop and no travel segments") func test_builder_SingleGame_CreatesSingleDay() { // Arrange - let gameId = UUID() + let gameId = "game_test_\(UUID().uuidString)" let stop = makeItineraryStop( city: "New York", state: "NY", @@ -60,9 +60,9 @@ struct ItineraryBuilderTests { func test_builder_MultiCity_CreatesTravelSegmentsBetween() { // Arrange let stops = [ - makeItineraryStop(city: "Boston", state: "MA", coordinate: boston, games: [UUID()]), - makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: [UUID()]), - makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: [UUID()]) + makeItineraryStop(city: "Boston", state: "MA", coordinate: boston, games: ["game_boston_\(UUID().uuidString)"]), + makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: ["game_nyc_\(UUID().uuidString)"]), + makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: ["game_chicago_\(UUID().uuidString)"]) ] // Act @@ -103,8 +103,8 @@ struct ItineraryBuilderTests { func test_builder_SameCity_MultipleGames_GroupsOnSameDay() { // Arrange - Two stops in the same city (different games, same location) let stops = [ - makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: [UUID()]), - makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: [UUID()]) + makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: ["game_nyc_1_\(UUID().uuidString)"]), + makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: ["game_nyc_2_\(UUID().uuidString)"]) ] // Act @@ -141,8 +141,8 @@ struct ItineraryBuilderTests { // Boston to Chicago is ~850 miles haversine, ~1100 with road factor // At 60 mph, that's ~18 hours = 3 travel days (ceil(18/8) = 3) let stops = [ - makeItineraryStop(city: "Boston", state: "MA", coordinate: boston, games: [UUID()]), - makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: [UUID()]) + makeItineraryStop(city: "Boston", state: "MA", coordinate: boston, games: ["game_boston_\(UUID().uuidString)"]), + makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: ["game_chicago_\(UUID().uuidString)"]) ] // Use constraints that allow long trips @@ -192,7 +192,7 @@ struct ItineraryBuilderTests { city: "Boston", state: "MA", coordinate: boston, - games: [UUID()], + games: ["game_boston_\(UUID().uuidString)"], departureDate: now ) @@ -201,7 +201,7 @@ struct ItineraryBuilderTests { city: "New York", state: "NY", coordinate: nyc, - games: [UUID()], + games: ["game_nyc_\(UUID().uuidString)"], firstGameStart: gameStartSoon ) @@ -231,7 +231,7 @@ struct ItineraryBuilderTests { city: "Boston", state: "MA", coordinate: boston, - games: [UUID()], + games: ["game_boston_\(UUID().uuidString)"], departureDate: now ) @@ -239,7 +239,7 @@ struct ItineraryBuilderTests { city: "New York", state: "NY", coordinate: nyc, - games: [UUID()], + games: ["game_nyc_\(UUID().uuidString)"], firstGameStart: gameLater ) @@ -283,7 +283,7 @@ struct ItineraryBuilderTests { city: String, state: String, coordinate: CLLocationCoordinate2D? = nil, - games: [UUID] = [], + games: [String] = [], arrivalDate: Date = Date(), departureDate: Date? = nil, firstGameStart: Date? = nil diff --git a/SportsTimeTests/Planning/RouteFiltersTests.swift b/SportsTimeTests/Planning/RouteFiltersTests.swift index c30960a..b7151e5 100644 --- a/SportsTimeTests/Planning/RouteFiltersTests.swift +++ b/SportsTimeTests/Planning/RouteFiltersTests.swift @@ -39,8 +39,8 @@ struct RouteFiltersTests { coordinate: nil, arrivalDate: startDate, departureDate: end, - games: [UUID()], - stadium: UUID() + games: ["game_test_\(UUID().uuidString)"], + stadium: "stadium_test_\(UUID().uuidString)" ) return Trip( diff --git a/SportsTimeTests/Planning/ScenarioAPlannerTests.swift b/SportsTimeTests/Planning/ScenarioAPlannerTests.swift index cc5be25..bc0a0fb 100644 --- a/SportsTimeTests/Planning/ScenarioAPlannerTests.swift +++ b/SportsTimeTests/Planning/ScenarioAPlannerTests.swift @@ -31,7 +31,7 @@ struct ScenarioAPlannerTests { /// Creates a stadium at a known location private func makeStadium( - id: UUID = UUID(), + id: String = "stadium_test_\(UUID().uuidString)", city: String, lat: Double, lon: Double, @@ -51,10 +51,10 @@ struct ScenarioAPlannerTests { /// Creates a game at a stadium private func makeGame( - id: UUID = UUID(), - stadiumId: UUID, - homeTeamId: UUID = UUID(), - awayTeamId: UUID = UUID(), + id: String = "game_test_\(UUID().uuidString)", + stadiumId: String, + homeTeamId: String = "team_test_\(UUID().uuidString)", + awayTeamId: String = "team_test_\(UUID().uuidString)", dateTime: Date, sport: Sport = .mlb ) -> Game { @@ -74,8 +74,8 @@ struct ScenarioAPlannerTests { startDate: Date, endDate: Date, games: [Game], - stadiums: [UUID: Stadium], - teams: [UUID: Team] = [:], + stadiums: [String: Stadium], + teams: [String: Team] = [:], allowRepeatCities: Bool = true, numberOfDrivers: Int = 1, maxDrivingHoursPerDriver: Double = 8.0, @@ -106,9 +106,9 @@ struct ScenarioAPlannerTests { @Test("4.1 - Valid date range returns games in range") func test_planByDates_ValidDateRange_ReturnsGamesInRange() { // Setup: 3 games across nearby cities over 5 days - let chicagoId = UUID() - let milwaukeeId = UUID() - let detroitId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" + let detroitId = "stadium_detroit_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -144,7 +144,7 @@ struct ScenarioAPlannerTests { @Test("4.2 - Single day range returns games on that day") func test_planByDates_SingleDayRange_ReturnsGamesOnThatDay() { // Setup: Multiple games on a single day at the same stadium - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] @@ -182,10 +182,10 @@ struct ScenarioAPlannerTests { @Test("4.3 - Multi-week range returns multiple games") func test_planByDates_MultiWeekRange_ReturnsMultipleGames() { // Setup: Games spread across 3 weeks in nearby cities - let chicagoId = UUID() - let milwaukeeId = UUID() - let detroitId = UUID() - let clevelandId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" + let detroitId = "stadium_detroit_\(UUID().uuidString)" + let clevelandId = "stadium_cleveland_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -230,7 +230,7 @@ struct ScenarioAPlannerTests { @Test("4.4 - No games in range returns failure") func test_planByDates_NoGamesInRange_ThrowsError() { // Setup: Games outside the requested date range - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] @@ -257,7 +257,7 @@ struct ScenarioAPlannerTests { @Test("4.5 - End date before start date returns failure") func test_planByDates_EndDateBeforeStartDate_ThrowsError() { // Setup: Invalid date range where end < start - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] @@ -283,7 +283,7 @@ struct ScenarioAPlannerTests { @Test("4.6 - Single game in range returns single game route") func test_planByDates_SingleGameInRange_ReturnsSingleGameRoute() { // Setup: Only one game in the date range - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] @@ -354,9 +354,9 @@ struct ScenarioAPlannerTests { func test_planByDates_UsesDAGRouterForRouting() { // Setup: Games that require DAG routing logic // Create games in multiple cities with feasible transitions - let chicagoId = UUID() - let milwaukeeId = UUID() - let detroitId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" + let detroitId = "stadium_detroit_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -403,8 +403,8 @@ struct ScenarioAPlannerTests { @Test("4.9 - Respects driver constraints") func test_planByDates_RespectsDriverConstraints() { // Setup: Games that would require excessive daily driving if constraints are loose - let nycId = UUID() - let chicagoId = UUID() + let nycId = "stadium_nyc_\(UUID().uuidString)" + let chicagoId = "stadium_chicago_\(UUID().uuidString)" // NYC to Chicago is ~790 miles (~13 hours of driving) let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352) @@ -474,9 +474,9 @@ struct ScenarioAPlannerTests { func test_planByDates_MustStop_FiltersToGamesInCity() { // Setup: Games in Chicago, Milwaukee, Detroit // Must-stop = Chicago → should only return Chicago games - let chicagoId = UUID() - let milwaukeeId = UUID() - let detroitId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" + let detroitId = "stadium_detroit_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -518,8 +518,8 @@ struct ScenarioAPlannerTests { func test_planByDates_MustStop_NoMatchingGames_ReturnsFailure() { // Setup: Games only in Milwaukee and Detroit // Must-stop = Chicago → no games there, should fail - let milwaukeeId = UUID() - let detroitId = UUID() + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" + let detroitId = "stadium_detroit_\(UUID().uuidString)" let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) let detroit = makeStadium(id: detroitId, city: "Detroit", lat: 42.3314, lon: -83.0458) @@ -553,10 +553,10 @@ struct ScenarioAPlannerTests { // Setup: Cubs home game in Chicago + Cubs away game in Milwaukee (playing at Milwaukee) // Must-stop = Chicago → should ONLY return the Chicago home game // This tests Issue #8: "Must stop needs to be home team" - let chicagoStadiumId = UUID() - let milwaukeeStadiumId = UUID() - let cubsTeamId = UUID() - let brewersTeamId = UUID() + let chicagoStadiumId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeStadiumId = "stadium_milwaukee_\(UUID().uuidString)" + let cubsTeamId = "team_cubs_\(UUID().uuidString)" + let brewersTeamId = "team_brewers_\(UUID().uuidString)" let wrigleyField = makeStadium(id: chicagoStadiumId, city: "Chicago", lat: 41.9484, lon: -87.6553) let millerPark = makeStadium(id: milwaukeeStadiumId, city: "Milwaukee", lat: 43.0280, lon: -87.9712) @@ -608,7 +608,7 @@ struct ScenarioAPlannerTests { func test_planByDates_MustStop_PartialCityMatch_Works() { // Setup: User types "Chicago" but stadium city is "Chicago, IL" // Should still match via contains - let chicagoId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [chicagoId: chicago] @@ -636,7 +636,7 @@ struct ScenarioAPlannerTests { @Test("4.14 - Must-stop case insensitive") func test_planByDates_MustStop_CaseInsensitive() { // Setup: Must-stop = "CHICAGO" (uppercase) should match "Chicago" - let chicagoId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [chicagoId: chicago] @@ -662,7 +662,7 @@ struct ScenarioAPlannerTests { @Test("4.15 - Multiple games in must-stop city all included") func test_planByDates_MustStop_MultipleGamesInCity_AllIncluded() { // Setup: Multiple games in Chicago on different days - let chicagoId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [chicagoId: chicago] diff --git a/SportsTimeTests/Planning/ScenarioBPlannerTests.swift b/SportsTimeTests/Planning/ScenarioBPlannerTests.swift index cd1fec1..088e9ac 100644 --- a/SportsTimeTests/Planning/ScenarioBPlannerTests.swift +++ b/SportsTimeTests/Planning/ScenarioBPlannerTests.swift @@ -31,7 +31,7 @@ struct ScenarioBPlannerTests { /// Creates a stadium at a known location private func makeStadium( - id: UUID = UUID(), + id: String = "stadium_test_\(UUID().uuidString)", city: String, lat: Double, lon: Double, @@ -51,10 +51,10 @@ struct ScenarioBPlannerTests { /// Creates a game at a stadium private func makeGame( - id: UUID = UUID(), - stadiumId: UUID, - homeTeamId: UUID = UUID(), - awayTeamId: UUID = UUID(), + id: String = "game_test_\(UUID().uuidString)", + stadiumId: String, + homeTeamId: String = "team_test_\(UUID().uuidString)", + awayTeamId: String = "team_test_\(UUID().uuidString)", dateTime: Date, sport: Sport = .mlb ) -> Game { @@ -74,9 +74,9 @@ struct ScenarioBPlannerTests { startDate: Date, endDate: Date, allGames: [Game], - mustSeeGameIds: Set, - stadiums: [UUID: Stadium], - teams: [UUID: Team] = [:], + mustSeeGameIds: Set, + stadiums: [String: Stadium], + teams: [String: Team] = [:], allowRepeatCities: Bool = true, numberOfDrivers: Int = 1, maxDrivingHoursPerDriver: Double = 8.0 @@ -106,11 +106,11 @@ struct ScenarioBPlannerTests { @Test("5.1 - Single must-see game returns trip with that game") func test_mustSeeGames_SingleGame_ReturnsTripWithThatGame() { // Setup: Single must-see game - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] - let gameId = UUID() + let gameId = "game_test_\(UUID().uuidString)" let game = makeGame(id: gameId, stadiumId: stadiumId, dateTime: makeDate(day: 10, hour: 19)) let request = makePlanningRequest( @@ -139,9 +139,9 @@ struct ScenarioBPlannerTests { func test_mustSeeGames_MultipleGames_ReturnsOptimalRoute() { // Setup: 3 must-see games in nearby cities (all Central region for single-region search) // Region boundary: Central is -110 to -85 longitude - let chicagoId = UUID() - let milwaukeeId = UUID() - let stLouisId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" + let stLouisId = "stadium_stlouis_\(UUID().uuidString)" // All cities in Central region (longitude between -110 and -85) let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) @@ -150,9 +150,9 @@ struct ScenarioBPlannerTests { let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee, stLouisId: stLouis] - let game1Id = UUID() - let game2Id = UUID() - let game3Id = UUID() + let game1Id = "game_test_1_\(UUID().uuidString)" + let game2Id = "game_test_2_\(UUID().uuidString)" + let game3Id = "game_test_3_\(UUID().uuidString)" let game1 = makeGame(id: game1Id, stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19)) let game2 = makeGame(id: game2Id, stadiumId: milwaukeeId, dateTime: makeDate(day: 7, hour: 19)) @@ -187,8 +187,8 @@ struct ScenarioBPlannerTests { @Test("5.3 - Games in different cities are connected") func test_mustSeeGames_GamesInDifferentCities_ConnectsThem() { // Setup: 2 must-see games in distant but reachable cities - let nycId = UUID() - let bostonId = UUID() + let nycId = "stadium_nyc_\(UUID().uuidString)" + let bostonId = "stadium_boston_\(UUID().uuidString)" // NYC to Boston is ~215 miles (~4 hours driving) let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352) @@ -196,8 +196,8 @@ struct ScenarioBPlannerTests { let stadiums = [nycId: nyc, bostonId: boston] - let game1Id = UUID() - let game2Id = UUID() + let game1Id = "game_test_1_\(UUID().uuidString)" + let game2Id = "game_test_2_\(UUID().uuidString)" // Games 2 days apart - plenty of time to drive let game1 = makeGame(id: game1Id, stadiumId: nycId, dateTime: makeDate(day: 5, hour: 19)) @@ -238,7 +238,7 @@ struct ScenarioBPlannerTests { @Test("5.4 - Empty selection returns failure") func test_mustSeeGames_EmptySelection_ThrowsError() { // Setup: No must-see games selected - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] @@ -266,8 +266,8 @@ struct ScenarioBPlannerTests { func test_mustSeeGames_ImpossibleToConnect_ThrowsError() { // Setup: Games on same day in cities ~850 miles apart (impossible in 8 hours) // Both cities in East region (> -85 longitude) so regional search covers both - let nycId = UUID() - let atlantaId = UUID() + let nycId = "stadium_nyc_\(UUID().uuidString)" + let atlantaId = "stadium_atlanta_\(UUID().uuidString)" // NYC to Atlanta is ~850 miles (~13 hours driving) - impossible with 8-hour limit let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352) @@ -275,8 +275,8 @@ struct ScenarioBPlannerTests { let stadiums = [nycId: nyc, atlantaId: atlanta] - let game1Id = UUID() - let game2Id = UUID() + let game1Id = "game_test_1_\(UUID().uuidString)" + let game2Id = "game_test_2_\(UUID().uuidString)" // Same day games 6 hours apart - even if you left right after game 1, // you can't drive 850 miles in 6 hours with 8-hour daily limit @@ -409,10 +409,10 @@ struct ScenarioBPlannerTests { } // Build coordinate map for brute force verification - var stopCoordinates: [UUID: CLLocationCoordinate2D] = [:] + var stopCoordinates: [String: CLLocationCoordinate2D] = [:] for stop in firstOption.stops { if let coord = stop.coordinate { - stopCoordinates[stop.id] = coord + stopCoordinates[stop.id.uuidString] = coord } } @@ -421,7 +421,7 @@ struct ScenarioBPlannerTests { return } - let stopIds = firstOption.stops.map { $0.id } + let stopIds = firstOption.stops.map { $0.id.uuidString } let verificationResult = BruteForceRouteVerifier.verify( proposedRoute: stopIds, stops: stopCoordinates, @@ -471,17 +471,17 @@ struct ScenarioBPlannerTests { #expect(!includedMustSee.isEmpty, "Should include some must-see games") // Build coordinate map - var stopCoordinates: [UUID: CLLocationCoordinate2D] = [:] + var stopCoordinates: [String: CLLocationCoordinate2D] = [:] for stop in firstOption.stops { if let coord = stop.coordinate { - stopCoordinates[stop.id] = coord + stopCoordinates[stop.id.uuidString] = coord } } // Check that there's no obviously better route (10% threshold) guard stopCoordinates.count >= 2 else { return } - let stopIds = firstOption.stops.map { $0.id } + let stopIds = firstOption.stops.map { $0.id.uuidString } let (hasBetter, improvement) = BruteForceRouteVerifier.hasObviouslyBetterRoute( proposedRoute: stopIds, stops: stopCoordinates, diff --git a/SportsTimeTests/Planning/ScenarioCPlannerTests.swift b/SportsTimeTests/Planning/ScenarioCPlannerTests.swift index e27d7f8..e220cb9 100644 --- a/SportsTimeTests/Planning/ScenarioCPlannerTests.swift +++ b/SportsTimeTests/Planning/ScenarioCPlannerTests.swift @@ -32,7 +32,7 @@ struct ScenarioCPlannerTests { /// Creates a stadium at a known location private func makeStadium( - id: UUID = UUID(), + id: String = "stadium_test_\(UUID().uuidString)", city: String, state: String = "ST", lat: Double, @@ -53,10 +53,10 @@ struct ScenarioCPlannerTests { /// Creates a game at a stadium private func makeGame( - id: UUID = UUID(), - stadiumId: UUID, - homeTeamId: UUID = UUID(), - awayTeamId: UUID = UUID(), + id: String = "game_test_\(UUID().uuidString)", + stadiumId: String, + homeTeamId: String = "team_test_\(UUID().uuidString)", + awayTeamId: String = "team_test_\(UUID().uuidString)", dateTime: Date, sport: Sport = .mlb ) -> Game { @@ -91,8 +91,8 @@ struct ScenarioCPlannerTests { startDate: Date, endDate: Date, allGames: [Game], - stadiums: [UUID: Stadium], - teams: [UUID: Team] = [:], + stadiums: [String: Stadium], + teams: [String: Team] = [:], mustStopLocations: [LocationInput] = [], allowRepeatCities: Bool = true, numberOfDrivers: Int = 1, @@ -127,8 +127,8 @@ struct ScenarioCPlannerTests { func test_departReturn_SameCity_ReturnsRoundTrip() { // Setup: Start and end in Chicago // Create stadiums in Chicago and a nearby city (Milwaukee) for games along the route - let chicagoId = UUID() - let milwaukeeId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -169,10 +169,10 @@ struct ScenarioCPlannerTests { @Test("6.2 - Different cities creates one-way route") func test_departReturn_DifferentCities_ReturnsOneWayRoute() { // Setup: Boston to Washington DC corridor (East Coast) - let bostonId = UUID() - let nycId = UUID() - let phillyId = UUID() - let dcId = UUID() + let bostonId = "stadium_boston_\(UUID().uuidString)" + let nycId = "stadium_nyc_\(UUID().uuidString)" + let phillyId = "stadium_philly_\(UUID().uuidString)" + let dcId = "stadium_dc_\(UUID().uuidString)" // East Coast corridor from north to south let boston = makeStadium(id: bostonId, city: "Boston", state: "MA", lat: 42.3601, lon: -71.0589) @@ -220,10 +220,10 @@ struct ScenarioCPlannerTests { func test_departReturn_GamesAlongCorridor_IncludesNearbyGames() { // Setup: Chicago to St. Louis corridor // Include games that are "along the way" (directional) - let chicagoId = UUID() - let springfieldId = UUID() - let stLouisId = UUID() - let milwaukeeId = UUID() // This is NOT along the route (north of Chicago) + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let springfieldId = "stadium_springfield_\(UUID().uuidString)" + let stLouisId = "stadium_stlouis_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" // This is NOT along the route (north of Chicago) // Chicago to St. Louis is ~300 miles south let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) @@ -280,9 +280,9 @@ struct ScenarioCPlannerTests { @Test("6.4 - No games along route returns failure") func test_departReturn_NoGamesAlongRoute_ThrowsError() { // Setup: Start/end cities have no games - let chicagoId = UUID() - let stLouisId = UUID() - let seattleId = UUID() // Games here, but not along Chicago-St. Louis route + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let stLouisId = "stadium_stlouis_\(UUID().uuidString)" + let seattleId = "stadium_seattle_\(UUID().uuidString)" // Games here, but not along Chicago-St. Louis route let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stLouis = makeStadium(id: stLouisId, city: "St. Louis", lat: 38.6270, lon: -90.1994) @@ -325,7 +325,7 @@ struct ScenarioCPlannerTests { @Test("6.5 - Invalid city (no stadiums) returns failure") func test_departReturn_InvalidCity_ThrowsError() { // Setup: Start location is a city with no stadium - let chicagoId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [chicagoId: chicago] @@ -357,10 +357,10 @@ struct ScenarioCPlannerTests { func test_departReturn_ExtremeDistance_RespectsConstraints() { // Setup: NYC to LA route (~2,800 miles) // With 8 hours/day at 60 mph = 480 miles/day, this takes ~6 days just driving - let nycId = UUID() - let laId = UUID() - let chicagoId = UUID() // Along the route - let denverID = UUID() // Along the route + let nycId = "stadium_nyc_\(UUID().uuidString)" + let laId = "stadium_la_\(UUID().uuidString)" + let chicagoId = "stadium_chicago_\(UUID().uuidString)" // Along the route + let denverID = "stadium_denver_\(UUID().uuidString)" // Along the route let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352) let la = makeStadium(id: laId, city: "Los Angeles", lat: 34.0522, lon: -118.2437) @@ -426,9 +426,9 @@ struct ScenarioCPlannerTests { @Test("6.7 - Must-stop location is included in route") func test_departReturn_WithMustStopLocation_IncludesStop() { // Setup: Boston to DC with must-stop in Philadelphia - let bostonId = UUID() - let phillyId = UUID() - let dcId = UUID() + let bostonId = "stadium_boston_\(UUID().uuidString)" + let phillyId = "stadium_philly_\(UUID().uuidString)" + let dcId = "stadium_dc_\(UUID().uuidString)" let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589) let philly = makeStadium(id: phillyId, city: "Philadelphia", lat: 39.9526, lon: -75.1652) @@ -476,8 +476,8 @@ struct ScenarioCPlannerTests { @Test("6.8 - Must-stop with no nearby games is still included") func test_departReturn_MustStopNoNearbyGames_IncludesStopAnyway() { // Setup: Boston to DC with must-stop in a city without games - let bostonId = UUID() - let dcId = UUID() + let bostonId = "stadium_boston_\(UUID().uuidString)" + let dcId = "stadium_dc_\(UUID().uuidString)" let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589) let dc = makeStadium(id: dcId, city: "Washington", lat: 38.9072, lon: -77.0369) @@ -528,10 +528,10 @@ struct ScenarioCPlannerTests { @Test("6.9 - Multiple must-stops are all included") func test_departReturn_MultipleMustStops_AllIncluded() { // Setup: Boston to DC with must-stops in NYC and Philadelphia - let bostonId = UUID() - let nycId = UUID() - let phillyId = UUID() - let dcId = UUID() + let bostonId = "stadium_boston_\(UUID().uuidString)" + let nycId = "stadium_nyc_\(UUID().uuidString)" + let phillyId = "stadium_philly_\(UUID().uuidString)" + let dcId = "stadium_dc_\(UUID().uuidString)" let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589) let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352) @@ -588,10 +588,10 @@ struct ScenarioCPlannerTests { func test_departReturn_MustStopConflictsWithRoute_FindsCompromise() { // Setup: Boston to DC with must-stop that's slightly off the optimal route // Cleveland is west of the Boston-DC corridor but could be included with detour - let bostonId = UUID() - let dcId = UUID() - let clevelandId = UUID() - let pittsburghId = UUID() + let bostonId = "stadium_boston_\(UUID().uuidString)" + let dcId = "stadium_dc_\(UUID().uuidString)" + let clevelandId = "stadium_cleveland_\(UUID().uuidString)" + let pittsburghId = "stadium_pittsburgh_\(UUID().uuidString)" let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589) let dc = makeStadium(id: dcId, city: "Washington", lat: 38.9072, lon: -77.0369) diff --git a/SportsTimeTests/Planning/ScenarioDPlannerTests.swift b/SportsTimeTests/Planning/ScenarioDPlannerTests.swift index e799e0b..b4cd07f 100644 --- a/SportsTimeTests/Planning/ScenarioDPlannerTests.swift +++ b/SportsTimeTests/Planning/ScenarioDPlannerTests.swift @@ -31,7 +31,7 @@ struct ScenarioDPlannerTests { /// Creates a stadium at a known location private func makeStadium( - id: UUID = UUID(), + id: String = "stadium_test_\(UUID().uuidString)", city: String, lat: Double, lon: Double, @@ -51,9 +51,9 @@ struct ScenarioDPlannerTests { /// Creates a team private func makeTeam( - id: UUID = UUID(), + id: String = "team_test_\(UUID().uuidString)", name: String, - stadiumId: UUID, + stadiumId: String, sport: Sport = .mlb ) -> Team { Team( @@ -71,10 +71,10 @@ struct ScenarioDPlannerTests { /// Creates a game at a stadium private func makeGame( - id: UUID = UUID(), - stadiumId: UUID, - homeTeamId: UUID, - awayTeamId: UUID, + id: String = "game_test_\(UUID().uuidString)", + stadiumId: String, + homeTeamId: String, + awayTeamId: String, dateTime: Date, sport: Sport = .mlb ) -> Game { @@ -93,10 +93,10 @@ struct ScenarioDPlannerTests { private func makePlanningRequest( startDate: Date, endDate: Date, - followTeamId: UUID?, + followTeamId: String?, allGames: [Game], - stadiums: [UUID: Stadium], - teams: [UUID: Team] = [:], + stadiums: [String: Stadium], + teams: [String: Team] = [:], selectedRegions: Set = [], allowRepeatCities: Bool = true, useHomeLocation: Bool = false, @@ -132,12 +132,12 @@ struct ScenarioDPlannerTests { @Test("D.1.1 - Single team with home games returns trip with those games") func test_followTeam_HomeGames_ReturnsTrip() { // Setup: Team with 2 home games - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] - let teamId = UUID() - let opponentId = UUID() + let teamId = "team_test_\(UUID().uuidString)" + let opponentId = "team_opponent_\(UUID().uuidString)" let team = makeTeam(id: teamId, name: "Chicago Cubs", stadiumId: stadiumId) let game1 = makeGame( @@ -179,8 +179,8 @@ struct ScenarioDPlannerTests { @Test("D.1.2 - Team with away games includes those games") func test_followTeam_AwayGames_IncludesAwayGames() { // Setup: Team with one home game and one away game (2 cities for simpler route) - let homeStadiumId = UUID() - let awayStadiumId = UUID() + let homeStadiumId = "stadium_home_\(UUID().uuidString)" + let awayStadiumId = "stadium_away_\(UUID().uuidString)" let homeStadium = makeStadium(id: homeStadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let awayStadium = makeStadium(id: awayStadiumId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -190,8 +190,8 @@ struct ScenarioDPlannerTests { awayStadiumId: awayStadium ] - let teamId = UUID() - let opponentId = UUID() + let teamId = "team_test_\(UUID().uuidString)" + let opponentId = "team_opponent_\(UUID().uuidString)" // Home game let homeGame = makeGame( @@ -237,8 +237,8 @@ struct ScenarioDPlannerTests { @Test("D.1.3 - Team games filtered by selected regions") func test_followTeam_RegionFilter_FiltersGames() { // Setup: Team with games in multiple regions - let eastStadiumId = UUID() - let centralStadiumId = UUID() + let eastStadiumId = "stadium_east_\(UUID().uuidString)" + let centralStadiumId = "stadium_central_\(UUID().uuidString)" // East region (> -85 longitude) let eastStadium = makeStadium(id: eastStadiumId, city: "New York", lat: 40.7128, lon: -73.9352) @@ -247,8 +247,8 @@ struct ScenarioDPlannerTests { let stadiums = [eastStadiumId: eastStadium, centralStadiumId: centralStadium] - let teamId = UUID() - let opponentId = UUID() + let teamId = "team_test_\(UUID().uuidString)" + let opponentId = "team_opponent_\(UUID().uuidString)" let eastGame = makeGame( stadiumId: eastStadiumId, @@ -292,14 +292,14 @@ struct ScenarioDPlannerTests { @Test("D.2.1 - No team selected returns missingTeamSelection failure") func test_followTeam_NoTeamSelected_ReturnsMissingTeamSelection() { // Setup: No team ID - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] let game = makeGame( stadiumId: stadiumId, - homeTeamId: UUID(), - awayTeamId: UUID(), + homeTeamId: "team_test_\(UUID().uuidString)", + awayTeamId: "team_opponent_\(UUID().uuidString)", dateTime: makeDate(day: 5, hour: 19) ) @@ -323,17 +323,17 @@ struct ScenarioDPlannerTests { @Test("D.2.2 - Team with no games in date range returns noGamesInRange failure") func test_followTeam_NoGamesInRange_ReturnsNoGamesFailure() { // Setup: Team's games are outside date range - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] - let teamId = UUID() + let teamId = "team_test_\(UUID().uuidString)" // Game is in July, but we search June let game = makeGame( stadiumId: stadiumId, homeTeamId: teamId, - awayTeamId: UUID(), + awayTeamId: "team_opponent_\(UUID().uuidString)", dateTime: makeDate(month: 7, day: 15, hour: 19) ) @@ -357,13 +357,13 @@ struct ScenarioDPlannerTests { @Test("D.2.3 - Team not involved in any games returns noGamesInRange failure") func test_followTeam_TeamNotInGames_ReturnsNoGamesFailure() { // Setup: Games exist but team isn't playing - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] - let teamId = UUID() - let otherTeam1 = UUID() - let otherTeam2 = UUID() + let teamId = "team_test_\(UUID().uuidString)" + let otherTeam1 = "team_other1_\(UUID().uuidString)" + let otherTeam2 = "team_other2_\(UUID().uuidString)" // Game between other teams let game = makeGame( @@ -393,12 +393,12 @@ struct ScenarioDPlannerTests { @Test("D.2.4 - Repeat city filter removes duplicate city visits") func test_followTeam_RepeatCityFilter_RemovesDuplicates() { // Setup: Team has multiple games at same stadium - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] - let teamId = UUID() - let opponentId = UUID() + let teamId = "team_test_\(UUID().uuidString)" + let opponentId = "team_opponent_\(UUID().uuidString)" let game1 = makeGame( stadiumId: stadiumId, @@ -445,16 +445,16 @@ struct ScenarioDPlannerTests { @Test("D.2.5 - Missing date range returns missingDateRange failure") func test_followTeam_MissingDateRange_ReturnsMissingDateRangeFailure() { // Setup: Invalid date range (end before start) - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] - let teamId = UUID() + let teamId = "team_test_\(UUID().uuidString)" let game = makeGame( stadiumId: stadiumId, homeTeamId: teamId, - awayTeamId: UUID(), + awayTeamId: "team_opponent_\(UUID().uuidString)", dateTime: makeDate(day: 5, hour: 19) ) @@ -481,16 +481,16 @@ struct ScenarioDPlannerTests { @Test("D.3.1 - Route connects team games chronologically") func test_followTeam_RouteIsChronological() { // Setup: Team with games in 2 nearby cities chronologically - let chicagoId = UUID() - let milwaukeeId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee] - let teamId = UUID() - let opponentId = UUID() + let teamId = "team_test_\(UUID().uuidString)" + let opponentId = "team_opponent_\(UUID().uuidString)" // Games in chronological order: Chicago → Milwaukee let game1 = makeGame( @@ -534,16 +534,16 @@ struct ScenarioDPlannerTests { @Test("D.3.2 - Travel segments connect stops correctly") func test_followTeam_TravelSegmentsConnectStops() { // Setup: Team with 2 games in different cities - let nycId = UUID() - let bostonId = UUID() + let nycId = "stadium_nyc_\(UUID().uuidString)" + let bostonId = "stadium_boston_\(UUID().uuidString)" let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352) let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589) let stadiums = [nycId: nyc, bostonId: boston] - let teamId = UUID() - let opponentId = UUID() + let teamId = "team_test_\(UUID().uuidString)" + let opponentId = "team_opponent_\(UUID().uuidString)" let game1 = makeGame( stadiumId: nycId, @@ -597,9 +597,9 @@ struct ScenarioDPlannerTests { // Setup: Simulates Houston → Chicago → Anaheim (Astros July 20-29 scenario) // Houston to Chicago: ~1000 miles, Chicago to Anaheim: ~2000 miles // With 4+ days between each leg, both should be feasible - let houstonId = UUID() - let chicagoId = UUID() - let anaheimId = UUID() + let houstonId = "stadium_houston_\(UUID().uuidString)" + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let anaheimId = "stadium_anaheim_\(UUID().uuidString)" let houston = makeStadium(id: houstonId, city: "Houston", lat: 29.7604, lon: -95.3698) let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) @@ -607,10 +607,10 @@ struct ScenarioDPlannerTests { let stadiums = [houstonId: houston, chicagoId: chicago, anaheimId: anaheim] - let teamId = UUID() - let opponent1 = UUID() - let opponent2 = UUID() - let opponent3 = UUID() + let teamId = "team_test_\(UUID().uuidString)" + let opponent1 = "team_opponent1_\(UUID().uuidString)" + let opponent2 = "team_opponent2_\(UUID().uuidString)" + let opponent3 = "team_opponent3_\(UUID().uuidString)" // Houston home games: July 20-22 let houstonGame = makeGame( @@ -674,9 +674,9 @@ struct ScenarioDPlannerTests { func test_followTeam_ThreeCityRoute_InsufficientTime_ExcludesUnreachableCities() { // Setup: Same cities but games too close together // Chicago to Anaheim needs ~37 hours driving, but only 1 day between games - let houstonId = UUID() - let chicagoId = UUID() - let anaheimId = UUID() + let houstonId = "stadium_houston_\(UUID().uuidString)" + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let anaheimId = "stadium_anaheim_\(UUID().uuidString)" let houston = makeStadium(id: houstonId, city: "Houston", lat: 29.7604, lon: -95.3698) let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) @@ -684,10 +684,10 @@ struct ScenarioDPlannerTests { let stadiums = [houstonId: houston, chicagoId: chicago, anaheimId: anaheim] - let teamId = UUID() - let opponent1 = UUID() - let opponent2 = UUID() - let opponent3 = UUID() + let teamId = "team_test_\(UUID().uuidString)" + let opponent1 = "team_opponent1_\(UUID().uuidString)" + let opponent2 = "team_opponent2_\(UUID().uuidString)" + let opponent3 = "team_opponent3_\(UUID().uuidString)" // Houston: July 20 let houstonGame = makeGame( @@ -744,17 +744,17 @@ struct ScenarioDPlannerTests { func test_followTeam_PicksOptimalGamePerCity_ForRouteFeasibility() { // Setup: Team has 3 games in each city (series) // With allowRepeatCities=false, router should pick games that make the route work - let chicagoId = UUID() - let anaheimId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let anaheimId = "stadium_anaheim_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let anaheim = makeStadium(id: anaheimId, city: "Anaheim", lat: 33.8003, lon: -117.8827) let stadiums = [chicagoId: chicago, anaheimId: anaheim] - let teamId = UUID() - let opponent1 = UUID() - let opponent2 = UUID() + let teamId = "team_test_\(UUID().uuidString)" + let opponent1 = "team_opponent1_\(UUID().uuidString)" + let opponent2 = "team_opponent2_\(UUID().uuidString)" // Chicago series: July 24, 25, 26 let chicagoGame1 = makeGame( @@ -826,8 +826,8 @@ struct ScenarioDPlannerTests { func test_followTeam_FiveDaySegment_AtLimit_Succeeds() { // Setup: ~38 hours of driving with exactly 5 days between games // 5 days × 8 hours = 40 hours max, which should pass - let seattleId = UUID() - let miamiId = UUID() + let seattleId = "stadium_seattle_\(UUID().uuidString)" + let miamiId = "stadium_denver_\(UUID().uuidString)" // Seattle to Miami: ~3,300 miles straight line × 1.3 = ~4,300 miles // At 60 mph = ~72 hours - this is too far even for 5 days @@ -837,9 +837,9 @@ struct ScenarioDPlannerTests { let stadiums = [seattleId: seattle, miamiId: denver] - let teamId = UUID() - let opponent1 = UUID() - let opponent2 = UUID() + let teamId = "team_test_\(UUID().uuidString)" + let opponent1 = "team_opponent1_\(UUID().uuidString)" + let opponent2 = "team_opponent2_\(UUID().uuidString)" let seattleGame = makeGame( stadiumId: seattleId, @@ -889,17 +889,17 @@ struct ScenarioDPlannerTests { // Setup: Distance that would take > 40 hours to drive // Seattle to Miami: ~3,300 miles straight line × 1.3 = ~4,300 miles // At 60 mph = ~72 hours - exceeds 40 hour (5 day) limit - let seattleId = UUID() - let miamiId = UUID() + let seattleId = "stadium_seattle_\(UUID().uuidString)" + let miamiId = "stadium_miami_\(UUID().uuidString)" let seattle = makeStadium(id: seattleId, city: "Seattle", lat: 47.6062, lon: -122.3321) let miami = makeStadium(id: miamiId, city: "Miami", lat: 25.7617, lon: -80.1918) let stadiums = [seattleId: seattle, miamiId: miami] - let teamId = UUID() - let opponent1 = UUID() - let opponent2 = UUID() + let teamId = "team_test_\(UUID().uuidString)" + let opponent1 = "team_opponent1_\(UUID().uuidString)" + let opponent2 = "team_opponent2_\(UUID().uuidString)" let seattleGame = makeGame( stadiumId: seattleId, @@ -950,17 +950,17 @@ struct ScenarioDPlannerTests { // Setup: Same Chicago→Anaheim route but with 2 drivers // With 2 drivers × 8 hours = 16 hours/day // Chicago to Anaheim in 3 days = 48 hours available (vs 24 hours with 1 driver) - let chicagoId = UUID() - let anaheimId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let anaheimId = "stadium_anaheim_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let anaheim = makeStadium(id: anaheimId, city: "Anaheim", lat: 33.8003, lon: -117.8827) let stadiums = [chicagoId: chicago, anaheimId: anaheim] - let teamId = UUID() - let opponent1 = UUID() - let opponent2 = UUID() + let teamId = "team_test_\(UUID().uuidString)" + let opponent1 = "team_opponent1_\(UUID().uuidString)" + let opponent2 = "team_opponent2_\(UUID().uuidString)" let chicagoGame = makeGame( stadiumId: chicagoId, diff --git a/SportsTimeTests/Planning/TripPlanningEngineTests.swift b/SportsTimeTests/Planning/TripPlanningEngineTests.swift index a1a0330..200055c 100644 --- a/SportsTimeTests/Planning/TripPlanningEngineTests.swift +++ b/SportsTimeTests/Planning/TripPlanningEngineTests.swift @@ -35,7 +35,7 @@ struct TripPlanningEngineTests { /// Creates a stadium at a known location private func makeStadium( - id: UUID = UUID(), + id: String = "stadium_test_\(UUID().uuidString)", city: String, lat: Double, lon: Double, @@ -55,10 +55,10 @@ struct TripPlanningEngineTests { /// Creates a game at a stadium private func makeGame( - id: UUID = UUID(), - stadiumId: UUID, - homeTeamId: UUID = UUID(), - awayTeamId: UUID = UUID(), + id: String = "game_test_\(UUID().uuidString)", + stadiumId: String, + homeTeamId: String = "team_test_\(UUID().uuidString)", + awayTeamId: String = "team_test_\(UUID().uuidString)", dateTime: Date, sport: Sport = .mlb ) -> Game { @@ -78,7 +78,7 @@ struct TripPlanningEngineTests { startDate: Date, endDate: Date, games: [Game], - stadiums: [UUID: Stadium], + stadiums: [String: Stadium], numberOfDrivers: Int = 1, maxDrivingHoursPerDriver: Double = 8.0, allowRepeatCities: Bool = true @@ -104,11 +104,11 @@ struct TripPlanningEngineTests { /// Creates a PlanningRequest for Scenario B (selected games) private func makeScenarioBRequest( - mustSeeGameIds: Set, + mustSeeGameIds: Set, startDate: Date, endDate: Date, games: [Game], - stadiums: [UUID: Stadium], + stadiums: [String: Stadium], numberOfDrivers: Int = 1, maxDrivingHoursPerDriver: Double = 8.0, allowRepeatCities: Bool = true @@ -140,7 +140,7 @@ struct TripPlanningEngineTests { startDate: Date, endDate: Date, games: [Game], - stadiums: [UUID: Stadium], + stadiums: [String: Stadium], numberOfDrivers: Int = 1, maxDrivingHoursPerDriver: Double = 8.0 ) -> PlanningRequest { @@ -169,8 +169,8 @@ struct TripPlanningEngineTests { @Test("7.1 - Engine delegates to Scenario A correctly") func test_engine_ScenarioA_DelegatesCorrectly() { // Setup: Date range only request (Scenario A) - let chicagoId = UUID() - let milwaukeeId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -202,8 +202,8 @@ struct TripPlanningEngineTests { @Test("7.2 - Engine delegates to Scenario B correctly") func test_engine_ScenarioB_DelegatesCorrectly() { // Setup: Selected games request (Scenario B) - let chicagoId = UUID() - let milwaukeeId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -244,9 +244,9 @@ struct TripPlanningEngineTests { @Test("7.3 - Engine delegates to Scenario C correctly") func test_engine_ScenarioC_DelegatesCorrectly() { // Setup: Start/end locations request (Scenario C) - let chicagoId = UUID() - let clevelandId = UUID() - let detroitId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let clevelandId = "stadium_cleveland_\(UUID().uuidString)" + let detroitId = "stadium_detroit_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let cleveland = makeStadium(id: clevelandId, city: "Cleveland", lat: 41.4993, lon: -81.6944) @@ -294,8 +294,8 @@ struct TripPlanningEngineTests { @Test("7.4 - Scenarios are mutually exclusive") func test_engine_ScenariosAreMutuallyExclusive() { // Setup: Create requests that could theoretically match multiple scenarios - let chicagoId = UUID() - let clevelandId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let clevelandId = "stadium_cleveland_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let cleveland = makeStadium(id: clevelandId, city: "Cleveland", lat: 41.4993, lon: -81.6944) @@ -350,9 +350,9 @@ struct TripPlanningEngineTests { @Test("7.5 - Result contains travel segments") func test_engine_Result_ContainsTravelSegments() { // Setup: Multi-city trip that requires travel - let chicagoId = UUID() - let milwaukeeId = UUID() - let detroitId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" + let detroitId = "stadium_detroit_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -396,8 +396,8 @@ struct TripPlanningEngineTests { @Test("7.6 - Result contains itinerary days") func test_engine_Result_ContainsItineraryDays() { // Setup: Multi-day trip - let chicagoId = UUID() - let milwaukeeId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -445,7 +445,7 @@ struct TripPlanningEngineTests { func test_engine_Result_IncludesWarnings_WhenApplicable() { // Setup: Request that would normally violate repeat cities // but allowRepeatCities=true so it should succeed without warnings - let chicagoId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) @@ -497,8 +497,8 @@ struct TripPlanningEngineTests { func test_engine_NumberOfDrivers_AffectsMaxDailyDriving() { // Setup: Long distance trip that requires significant driving // NYC to Chicago is ~790 miles (~13 hours of driving) - let nycId = UUID() - let chicagoId = UUID() + let nycId = "stadium_nyc_\(UUID().uuidString)" + let chicagoId = "stadium_chicago_\(UUID().uuidString)" let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352) let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) @@ -593,8 +593,8 @@ struct TripPlanningEngineTests { @Test("7.10 - AllowRepeatCities is propagated to DAG") func test_engine_AllowRepeatCities_PropagatedToDAG() { // Setup: Games that would require visiting the same city twice - let chicagoId = UUID() - let milwaukeeId = UUID() + let chicagoId = "stadium_chicago_\(UUID().uuidString)" + let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) @@ -657,8 +657,8 @@ struct TripPlanningEngineTests { func test_engine_ImpossibleConstraints_ReturnsNoResult() { // Setup: Create an impossible constraint scenario // Games at the same time on same day in cities far apart (can't make both) - let nycId = UUID() - let laId = UUID() + let nycId = "stadium_nyc_\(UUID().uuidString)" + let laId = "stadium_la_\(UUID().uuidString)" // NYC to LA is ~2,800 miles - impossible to drive same day let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352) @@ -711,7 +711,7 @@ struct TripPlanningEngineTests { @Test("7.12 - Empty input returns error") func test_engine_EmptyInput_ThrowsError() { // Setup: Request with no games - let stadiumId = UUID() + let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] diff --git a/SportsTimeTests/Progress/AchievementEngineTests.swift b/SportsTimeTests/Progress/AchievementEngineTests.swift index b4a2045..7d18acc 100644 --- a/SportsTimeTests/Progress/AchievementEngineTests.swift +++ b/SportsTimeTests/Progress/AchievementEngineTests.swift @@ -181,10 +181,10 @@ final class AchievementEngineIntegrationTests: XCTestCase { var modelContainer: ModelContainer! var modelContext: ModelContext! - // Test UUIDs for stadiums - let fenwayUUID = UUID(uuidString: "11111111-1111-1111-1111-111111111111")! - let wrigleyUUID = UUID(uuidString: "22222222-2222-2222-2222-222222222222")! - let msgUUID = UUID(uuidString: "33333333-3333-3333-3333-333333333333")! + // Canonical IDs for test stadiums + let fenwayId = "stadium_mlb_bos" + let wrigleyId = "stadium_mlb_chc" + let msgId = "stadium_nba_nyk" override func setUp() async throws { try await super.setUp() @@ -231,8 +231,7 @@ final class AchievementEngineIntegrationTests: XCTestCase { private func setupTestData() async { // Create Fenway Park stadium let fenway = CanonicalStadium( - canonicalId: "stadium_mlb_bos", - uuid: fenwayUUID, + canonicalId: fenwayId, name: "Fenway Park", city: "Boston", state: "MA", @@ -245,8 +244,7 @@ final class AchievementEngineIntegrationTests: XCTestCase { // Create Wrigley Field stadium let wrigley = CanonicalStadium( - canonicalId: "stadium_mlb_chc", - uuid: wrigleyUUID, + canonicalId: wrigleyId, name: "Wrigley Field", city: "Chicago", state: "IL", @@ -259,8 +257,7 @@ final class AchievementEngineIntegrationTests: XCTestCase { // Create MSG stadium let msg = CanonicalStadium( - canonicalId: "stadium_nba_nyk", - uuid: msgUUID, + canonicalId: msgId, name: "Madison Square Garden", city: "New York", state: "NY", @@ -317,10 +314,9 @@ final class AchievementEngineIntegrationTests: XCTestCase { // Create engine let engine = AchievementEngine(modelContext: modelContext, dataProvider: AppDataProvider.shared) - // Create a visit to Fenway using the actual stadium UUID format + // Create a visit to Fenway using the canonical stadium ID let visit = StadiumVisit( - canonicalStadiumId: fenwayUUID.uuidString, - stadiumUUID: fenwayUUID, + stadiumId: fenwayId, stadiumNameAtVisit: "Fenway Park", visitDate: Date(), sport: .mlb @@ -347,8 +343,7 @@ final class AchievementEngineIntegrationTests: XCTestCase { let engine = AchievementEngine(modelContext: modelContext, dataProvider: AppDataProvider.shared) let visit = StadiumVisit( - canonicalStadiumId: wrigleyUUID.uuidString, - stadiumUUID: wrigleyUUID, + stadiumId: wrigleyId, stadiumNameAtVisit: "Wrigley Field", visitDate: Date(), sport: .mlb @@ -386,8 +381,7 @@ final class AchievementEngineIntegrationTests: XCTestCase { // Add a visit let visit = StadiumVisit( - canonicalStadiumId: fenwayUUID.uuidString, - stadiumUUID: fenwayUUID, + stadiumId: fenwayId, stadiumNameAtVisit: "Fenway Park", visitDate: Date(), sport: .mlb @@ -406,10 +400,9 @@ final class AchievementEngineIntegrationTests: XCTestCase { let engine = AchievementEngine(modelContext: modelContext, dataProvider: AppDataProvider.shared) // Add TD Garden for NHL - let tdGardenUUID = UUID() + let tdGardenId = "stadium_nhl_bos" let tdGarden = CanonicalStadium( - canonicalId: "stadium_nhl_bos", - uuid: tdGardenUUID, + canonicalId: tdGardenId, name: "TD Garden", city: "Boston", state: "MA", @@ -437,8 +430,7 @@ final class AchievementEngineIntegrationTests: XCTestCase { // Visit MLB stadium only - not enough let mlbVisit = StadiumVisit( - canonicalStadiumId: fenwayUUID.uuidString, - stadiumUUID: fenwayUUID, + stadiumId: fenwayId, stadiumNameAtVisit: "Fenway Park", visitDate: Date(), sport: .mlb @@ -453,8 +445,7 @@ final class AchievementEngineIntegrationTests: XCTestCase { // Visit NBA stadium - still not enough let nbaVisit = StadiumVisit( - canonicalStadiumId: msgUUID.uuidString, - stadiumUUID: msgUUID, + stadiumId: msgId, stadiumNameAtVisit: "MSG", visitDate: Date(), sport: .nba @@ -469,8 +460,7 @@ final class AchievementEngineIntegrationTests: XCTestCase { // Visit NHL stadium - now earned! let nhlVisit = StadiumVisit( - canonicalStadiumId: tdGardenUUID.uuidString, - stadiumUUID: tdGardenUUID, + stadiumId: tdGardenId, stadiumNameAtVisit: "TD Garden", visitDate: Date(), sport: .nhl diff --git a/SportsTimeTests/SportsTimeTests.swift b/SportsTimeTests/SportsTimeTests.swift index 04ef815..0cdccde 100644 --- a/SportsTimeTests/SportsTimeTests.swift +++ b/SportsTimeTests/SportsTimeTests.swift @@ -20,7 +20,7 @@ struct DuplicateGameIdTests { private func makeStadium(sport: Sport = .mlb) -> Stadium { Stadium( - id: UUID(), + id: "stadium_test_\(UUID().uuidString)", name: "Test Stadium", city: "Test City", state: "TS", @@ -31,9 +31,9 @@ struct DuplicateGameIdTests { ) } - private func makeTeam(sport: Sport = .mlb, stadiumId: UUID) -> Team { + private func makeTeam(sport: Sport = .mlb, stadiumId: String) -> Team { Team( - id: UUID(), + id: "team_test_\(UUID().uuidString)", name: "Test Team", abbreviation: "TST", sport: sport, @@ -42,7 +42,7 @@ struct DuplicateGameIdTests { ) } - private func makeGame(id: UUID, homeTeamId: UUID, awayTeamId: UUID, stadiumId: UUID, dateTime: Date) -> Game { + private func makeGame(id: String, homeTeamId: String, awayTeamId: String, stadiumId: String, dateTime: Date) -> Game { Game( id: id, homeTeamId: homeTeamId, @@ -59,16 +59,16 @@ struct DuplicateGameIdTests { @Test("Duplicate games are deduplicated at load time") func gamesArray_DeduplicatesById() { // Simulate the deduplication logic used in StubDataProvider - let gameId = UUID() + let gameId = "game_test_\(UUID().uuidString)" let dateTime = Date() - let game1 = makeGame(id: gameId, homeTeamId: UUID(), awayTeamId: UUID(), stadiumId: UUID(), dateTime: dateTime) - let game2 = makeGame(id: gameId, homeTeamId: UUID(), awayTeamId: UUID(), stadiumId: UUID(), dateTime: dateTime.addingTimeInterval(3600)) + let game1 = makeGame(id: gameId, homeTeamId: "team_home_\(UUID().uuidString)", awayTeamId: "team_away_\(UUID().uuidString)", stadiumId: "stadium_test_\(UUID().uuidString)", dateTime: dateTime) + let game2 = makeGame(id: gameId, homeTeamId: "team_home_\(UUID().uuidString)", awayTeamId: "team_away_\(UUID().uuidString)", stadiumId: "stadium_test_\(UUID().uuidString)", dateTime: dateTime.addingTimeInterval(3600)) let games = [game1, game2] // Deduplication logic from StubDataProvider - var seenIds = Set() + var seenIds = Set() let uniqueGames = games.filter { game in if seenIds.contains(game.id) { return false