// // SportsTimeTests.swift // SportsTimeTests // // Created by Trey Tartt on 1/6/26. // import Testing @testable import SportsTime import Foundation // MARK: - DayCard Tests /// Tests for DayCard conflict detection and display logic struct DayCardTests { // MARK: - Test Data Helpers private func makeGame(id: UUID, dateTime: Date, stadiumId: UUID, sport: Sport = .mlb) -> Game { Game( id: id, homeTeamId: UUID(), awayTeamId: UUID(), stadiumId: stadiumId, dateTime: dateTime, sport: sport, season: "2026" ) } private func makeRichGame(game: Game, homeTeamName: String = "Home", awayTeamName: String = "Away") -> RichGame { let stadiumId = game.stadiumId let homeTeam = Team( id: game.homeTeamId, name: homeTeamName, abbreviation: "HOM", sport: game.sport, city: "Home City", stadiumId: stadiumId ) let awayTeam = Team( id: game.awayTeamId, name: awayTeamName, abbreviation: "AWY", sport: game.sport, city: "Away City", stadiumId: UUID() ) let stadium = Stadium( id: stadiumId, name: "Stadium", city: "City", state: "ST", latitude: 40.0, longitude: -100.0, capacity: 40000, sport: game.sport ) return RichGame(game: game, homeTeam: homeTeam, awayTeam: awayTeam, stadium: stadium) } private func makeStop( city: String, arrivalDate: Date, departureDate: Date, games: [UUID] ) -> TripStop { TripStop( stopNumber: 1, city: city, state: "ST", arrivalDate: arrivalDate, departureDate: departureDate, games: games ) } // MARK: - Conflict Detection Tests @Test("DayCard with specificStop shows only that stop's games") func dayCard_WithSpecificStop_ShowsOnlyThatStopsGames() { let apr4 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 4))! let apr5 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5))! let denverGameId = UUID() let atlantaGameId = UUID() let denverGameTime = Calendar.current.date(bySettingHour: 20, minute: 10, second: 0, of: apr4)! let atlantaGameTime = Calendar.current.date(bySettingHour: 23, minute: 15, second: 0, of: apr4)! let denverGame = makeGame(id: denverGameId, dateTime: denverGameTime, stadiumId: UUID()) let atlantaGame = makeGame(id: atlantaGameId, dateTime: atlantaGameTime, stadiumId: UUID()) let denverStop = makeStop(city: "Denver", arrivalDate: apr4, departureDate: apr5, games: [denverGameId]) let atlantaStop = makeStop(city: "Atlanta", arrivalDate: apr4, departureDate: apr5, games: [atlantaGameId]) let day = ItineraryDay(dayNumber: 1, date: apr4, stops: [denverStop, atlantaStop], travelSegments: []) let games: [UUID: RichGame] = [ denverGameId: makeRichGame(game: denverGame), atlantaGameId: makeRichGame(game: atlantaGame) ] // Denver card shows only Denver game let denverCard = DayCard(day: day, games: games, specificStop: denverStop) #expect(denverCard.gamesOnThisDay.count == 1) #expect(denverCard.gamesOnThisDay.first?.game.id == denverGameId) #expect(denverCard.primaryCityForDay == "Denver") // Atlanta card shows only Atlanta game let atlantaCard = DayCard(day: day, games: games, specificStop: atlantaStop) #expect(atlantaCard.gamesOnThisDay.count == 1) #expect(atlantaCard.gamesOnThisDay.first?.game.id == atlantaGameId) #expect(atlantaCard.primaryCityForDay == "Atlanta") } @Test("DayCard shows conflict warning when conflictInfo provided") func dayCard_ShowsConflictWarning_WhenConflictInfoProvided() { let apr4 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 4))! let apr5 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5))! let denverGameId = UUID() let denverGameTime = Calendar.current.date(bySettingHour: 20, minute: 10, second: 0, of: apr4)! let denverGame = makeGame(id: denverGameId, dateTime: denverGameTime, stadiumId: UUID()) let denverStop = makeStop(city: "Denver", arrivalDate: apr4, departureDate: apr5, games: [denverGameId]) let atlantaStop = makeStop(city: "Atlanta", arrivalDate: apr4, departureDate: apr5, games: [UUID()]) let day = ItineraryDay(dayNumber: 1, date: apr4, stops: [denverStop, atlantaStop], travelSegments: []) let games: [UUID: RichGame] = [denverGameId: makeRichGame(game: denverGame)] let conflictInfo = DayConflictInfo( hasConflict: true, conflictingStops: [denverStop, atlantaStop], conflictingCities: ["Denver", "Atlanta"] ) let dayCard = DayCard(day: day, games: games, specificStop: denverStop, conflictInfo: conflictInfo) #expect(dayCard.hasConflict == true) #expect(dayCard.otherConflictingCities == ["Atlanta"]) } @Test("DayCard without conflict shows no warning") func dayCard_WithoutConflict_ShowsNoWarning() { let apr4 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 4))! let apr5 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5))! let gameId = UUID() let gameTime = Calendar.current.date(bySettingHour: 19, minute: 0, second: 0, of: apr4)! let game = makeGame(id: gameId, dateTime: gameTime, stadiumId: UUID()) let stop = makeStop(city: "Chicago", arrivalDate: apr4, departureDate: apr5, games: [gameId]) let day = ItineraryDay(dayNumber: 1, date: apr4, stops: [stop], travelSegments: []) let games: [UUID: RichGame] = [gameId: makeRichGame(game: game)] let dayCard = DayCard(day: day, games: games) #expect(dayCard.hasConflict == false) #expect(dayCard.otherConflictingCities.isEmpty) } @Test("DayConflictInfo lists all conflicting cities in warning message") func dayConflictInfo_ListsAllCitiesInWarningMessage() { let apr4 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 4))! let apr5 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5))! let denverStop = makeStop(city: "Denver", arrivalDate: apr4, departureDate: apr5, games: [UUID()]) let atlantaStop = makeStop(city: "Atlanta", arrivalDate: apr4, departureDate: apr5, games: [UUID()]) let chicagoStop = makeStop(city: "Chicago", arrivalDate: apr4, departureDate: apr5, games: [UUID()]) let conflictInfo = DayConflictInfo( hasConflict: true, conflictingStops: [denverStop, atlantaStop, chicagoStop], conflictingCities: ["Denver", "Atlanta", "Chicago"] ) #expect(conflictInfo.hasConflict == true) #expect(conflictInfo.conflictingCities.count == 3) #expect(conflictInfo.warningMessage.contains("Denver")) #expect(conflictInfo.warningMessage.contains("Atlanta")) #expect(conflictInfo.warningMessage.contains("Chicago")) } @Test("otherConflictingCities excludes current city") func otherConflictingCities_ExcludesCurrentCity() { let apr4 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 4))! let apr5 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5))! let denverGameId = UUID() let denverGameTime = Calendar.current.date(bySettingHour: 20, minute: 0, second: 0, of: apr4)! let denverGame = makeGame(id: denverGameId, dateTime: denverGameTime, stadiumId: UUID()) let denverStop = makeStop(city: "Denver", arrivalDate: apr4, departureDate: apr5, games: [denverGameId]) let atlantaStop = makeStop(city: "Atlanta", arrivalDate: apr4, departureDate: apr5, games: [UUID()]) let chicagoStop = makeStop(city: "Chicago", arrivalDate: apr4, departureDate: apr5, games: [UUID()]) let day = ItineraryDay(dayNumber: 1, date: apr4, stops: [denverStop, atlantaStop, chicagoStop], travelSegments: []) let games: [UUID: RichGame] = [denverGameId: makeRichGame(game: denverGame)] let conflictInfo = DayConflictInfo( hasConflict: true, conflictingStops: [denverStop, atlantaStop, chicagoStop], conflictingCities: ["Denver", "Atlanta", "Chicago"] ) let dayCard = DayCard(day: day, games: games, specificStop: denverStop, conflictInfo: conflictInfo) // Should exclude Denver (current city), include Atlanta and Chicago #expect(dayCard.otherConflictingCities.count == 2) #expect(dayCard.otherConflictingCities.contains("Atlanta")) #expect(dayCard.otherConflictingCities.contains("Chicago")) #expect(!dayCard.otherConflictingCities.contains("Denver")) } // MARK: - Basic DayCard Tests @Test("DayCard handles single stop correctly") func dayCard_HandlesSingleStop_Correctly() { let apr4 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 4))! let apr5 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5))! let gameId = UUID() let gameTime = Calendar.current.date(bySettingHour: 19, minute: 0, second: 0, of: apr4)! let game = makeGame(id: gameId, dateTime: gameTime, stadiumId: UUID()) let stop = makeStop(city: "Chicago", arrivalDate: apr4, departureDate: apr5, games: [gameId]) let day = ItineraryDay(dayNumber: 1, date: apr4, stops: [stop], travelSegments: []) let games: [UUID: RichGame] = [gameId: makeRichGame(game: game)] let dayCard = DayCard(day: day, games: games) #expect(dayCard.gamesOnThisDay.count == 1) #expect(dayCard.primaryCityForDay == "Chicago") #expect(dayCard.hasConflict == false) } @Test("DayCard handles no stops gracefully") func dayCard_HandlesNoStops_Gracefully() { let apr4 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 4))! let day = ItineraryDay(dayNumber: 1, date: apr4, stops: [], travelSegments: []) let dayCard = DayCard(day: day, games: [:]) #expect(dayCard.gamesOnThisDay.isEmpty) #expect(dayCard.primaryCityForDay == nil) #expect(dayCard.hasConflict == false) } @Test("DayCard handles stop with no games on the specific day") func dayCard_HandlesStopWithNoGamesOnDay() { let apr4 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 4))! let apr5 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5))! let apr6 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 6))! // Game is on Apr 5, but we're looking at Apr 4 let gameId = UUID() let gameTime = Calendar.current.date(bySettingHour: 19, minute: 0, second: 0, of: apr5)! let game = makeGame(id: gameId, dateTime: gameTime, stadiumId: UUID()) // Stop spans Apr 4-6, but game is on Apr 5 let stop = makeStop(city: "Boston", arrivalDate: apr4, departureDate: apr6, games: [gameId]) // Looking at Apr 4 (arrival day, no game) let day = ItineraryDay(dayNumber: 1, date: apr4, stops: [stop], travelSegments: []) let games: [UUID: RichGame] = [gameId: makeRichGame(game: game)] let dayCard = DayCard(day: day, games: games) // No games should show on Apr 4 even though the stop has a game (it's on Apr 5) #expect(dayCard.gamesOnThisDay.isEmpty, "No games on Apr 4, game is on Apr 5") #expect(dayCard.primaryCityForDay == "Boston", "Still shows the city even without games") } @Test("DayCard handles multiple games at same stop on same day (doubleheader)") func dayCard_HandlesMultipleGamesAtSameStop() { let apr4 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 4))! let apr5 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5))! // Two games in same city on same day (doubleheader) let game1Id = UUID() let game2Id = UUID() let game1Time = Calendar.current.date(bySettingHour: 13, minute: 0, second: 0, of: apr4)! let game2Time = Calendar.current.date(bySettingHour: 19, minute: 0, second: 0, of: apr4)! let game1 = makeGame(id: game1Id, dateTime: game1Time, stadiumId: UUID()) let game2 = makeGame(id: game2Id, dateTime: game2Time, stadiumId: UUID()) let stop = makeStop(city: "New York", arrivalDate: apr4, departureDate: apr5, games: [game1Id, game2Id]) let day = ItineraryDay(dayNumber: 1, date: apr4, stops: [stop], travelSegments: []) let games: [UUID: RichGame] = [ game1Id: makeRichGame(game: game1), game2Id: makeRichGame(game: game2) ] let dayCard = DayCard(day: day, games: games) #expect(dayCard.gamesOnThisDay.count == 2, "Should show both games from same city") #expect(dayCard.hasConflict == false, "Same city doubleheader is not a conflict") } @Test("DayCard selects stop with game when first stop has no game on that day") func dayCard_SelectsStopWithGame_WhenFirstStopHasNoGame() { let apr4 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 4))! let apr5 = Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5))! // First stop has game on different day let firstStopGameId = UUID() let firstStopGameTime = Calendar.current.date(bySettingHour: 19, minute: 0, second: 0, of: apr5)! let firstStopGame = makeGame(id: firstStopGameId, dateTime: firstStopGameTime, stadiumId: UUID()) // Second stop has game on Apr 4 let secondStopGameId = UUID() let secondStopGameTime = Calendar.current.date(bySettingHour: 20, minute: 0, second: 0, of: apr4)! let secondStopGame = makeGame(id: secondStopGameId, dateTime: secondStopGameTime, stadiumId: UUID()) let firstStop = makeStop(city: "Philadelphia", arrivalDate: apr4, departureDate: apr5, games: [firstStopGameId]) let secondStop = makeStop(city: "Baltimore", arrivalDate: apr4, departureDate: apr5, games: [secondStopGameId]) let day = ItineraryDay(dayNumber: 1, date: apr4, stops: [firstStop, secondStop], travelSegments: []) let games: [UUID: RichGame] = [ firstStopGameId: makeRichGame(game: firstStopGame), secondStopGameId: makeRichGame(game: secondStopGame) ] let dayCard = DayCard(day: day, games: games) // Should select Baltimore (has game on Apr 4) not Philadelphia (game on Apr 5) #expect(dayCard.gamesOnThisDay.count == 1) #expect(dayCard.gamesOnThisDay.first?.game.id == secondStopGameId) #expect(dayCard.primaryCityForDay == "Baltimore") } // MARK: - DayConflictInfo Tests @Test("DayConflictInfo with no conflict has empty warning") func dayConflictInfo_NoConflict_EmptyWarning() { let conflictInfo = DayConflictInfo( hasConflict: false, conflictingStops: [], conflictingCities: [] ) #expect(conflictInfo.hasConflict == false) #expect(conflictInfo.warningMessage.isEmpty) } } // MARK: - Duplicate Game ID Regression Tests /// Tests for handling duplicate game IDs without crashing (regression test for fatal error) struct DuplicateGameIdTests { private func makeStadium(sport: Sport = .mlb) -> Stadium { Stadium( id: UUID(), name: "Test Stadium", city: "Test City", state: "TS", latitude: 40.0, longitude: -100.0, capacity: 40000, sport: sport ) } private func makeTeam(sport: Sport = .mlb, stadiumId: UUID) -> Team { Team( id: UUID(), name: "Test Team", abbreviation: "TST", sport: sport, city: "Test City", stadiumId: stadiumId ) } private func makeGame(id: UUID, homeTeamId: UUID, awayTeamId: UUID, stadiumId: UUID, dateTime: Date) -> Game { Game( id: id, homeTeamId: homeTeamId, awayTeamId: awayTeamId, stadiumId: stadiumId, dateTime: dateTime, sport: .mlb, season: "2026" ) } // Note: GameCandidate test removed - type no longer exists after planning engine refactor @Test("Duplicate games are deduplicated at load time") func gamesArray_DeduplicatesById() { // Simulate the deduplication logic used in StubDataProvider let gameId = UUID() 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 games = [game1, game2] // Deduplication logic from StubDataProvider var seenIds = Set() let uniqueGames = games.filter { game in if seenIds.contains(game.id) { return false } seenIds.insert(game.id) return true } #expect(uniqueGames.count == 1) #expect(uniqueGames.first?.dateTime == game1.dateTime, "First occurrence should be kept") } }