// // ScenarioAPlannerSwiftTests.swift // SportsTimeTests // // Additional tests for ScenarioAPlanner using Swift Testing framework. // Combined with ScenarioAPlannerTests.swift, this provides comprehensive coverage. // import Testing @testable import SportsTime import Foundation import CoreLocation // MARK: - ScenarioAPlanner Swift Tests struct ScenarioAPlannerSwiftTests { // MARK: - Test Data Helpers private func makeStadium( id: UUID = UUID(), city: String, latitude: Double, longitude: Double, sport: Sport = .mlb ) -> Stadium { Stadium( id: id, name: "\(city) Stadium", city: city, state: "ST", latitude: latitude, longitude: longitude, capacity: 40000, sport: sport ) } private func makeGame( id: UUID = UUID(), stadiumId: UUID, dateTime: Date ) -> Game { Game( id: id, homeTeamId: UUID(), awayTeamId: UUID(), stadiumId: stadiumId, dateTime: dateTime, sport: .mlb, season: "2026" ) } private func baseDate() -> Date { Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5))! } private func date(daysFrom base: Date, days: Int, hour: Int = 19) -> Date { var date = Calendar.current.date(byAdding: .day, value: days, to: base)! return Calendar.current.date(bySettingHour: hour, minute: 0, second: 0, of: date)! } private func makeDateRange(start: Date, days: Int) -> DateInterval { let end = Calendar.current.date(byAdding: .day, value: days, to: start)! return DateInterval(start: start, end: end) } private func plan( games: [Game], stadiums: [Stadium], dateRange: DateInterval, numberOfDrivers: Int = 1, maxHoursPerDriver: Double = 8.0 ) -> ItineraryResult { let stadiumDict = Dictionary(uniqueKeysWithValues: stadiums.map { ($0.id, $0) }) let preferences = TripPreferences( planningMode: .dateRange, startDate: dateRange.start, endDate: dateRange.end, numberOfDrivers: numberOfDrivers, maxDrivingHoursPerDriver: maxHoursPerDriver ) let request = PlanningRequest( preferences: preferences, availableGames: games, teams: [:], stadiums: stadiumDict ) let planner = ScenarioAPlanner() return planner.plan(request: request) } // MARK: - Failure Case Tests @Test("plan with no date range returns failure") func plan_NoDateRange_ReturnsFailure() { // Create a request without a valid date range let preferences = TripPreferences( planningMode: .dateRange, startDate: baseDate(), endDate: baseDate() // Same date = no range ) let request = PlanningRequest( preferences: preferences, availableGames: [], teams: [:], stadiums: [:] ) let planner = ScenarioAPlanner() let result = planner.plan(request: request) #expect(result.failure?.reason == .missingDateRange) } @Test("plan with games all outside date range returns failure") func plan_AllGamesOutsideRange_ReturnsFailure() { let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903) let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 30)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.failure?.reason == .noGamesInRange) } @Test("plan with end date before start date returns failure") func plan_InvalidDateRange_ReturnsFailure() { let preferences = TripPreferences( planningMode: .dateRange, startDate: baseDate(), endDate: Calendar.current.date(byAdding: .day, value: -5, to: baseDate())! ) let request = PlanningRequest( preferences: preferences, availableGames: [], teams: [:], stadiums: [:] ) let planner = ScenarioAPlanner() let result = planner.plan(request: request) #expect(result.failure != nil) } // MARK: - Success Case Tests @Test("plan returns success with valid single game") func plan_ValidSingleGame_ReturnsSuccess() { let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903) let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.count == 1) #expect(result.options.first?.stops.count == 1) } @Test("plan includes game exactly at range start") func plan_GameAtRangeStart_Included() { let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903) // Game exactly at start of range let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 0, hour: 10)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.stops.count == 1) } @Test("plan includes game exactly at range end") func plan_GameAtRangeEnd_Included() { let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903) // Game at end of range let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 9, hour: 19)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) } // MARK: - Driving Constraints Tests @Test("plan rejects route that exceeds driving limit") func plan_ExceedsDrivingLimit_RoutePruned() { // Create two cities ~2000 miles apart let ny = makeStadium(city: "New York", latitude: 40.7128, longitude: -74.0060) let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) // Games 1 day apart - impossible to drive let games = [ makeGame(stadiumId: ny.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 1)) ] let result = plan( games: games, stadiums: [ny, la], dateRange: makeDateRange(start: baseDate(), days: 10), numberOfDrivers: 1, maxHoursPerDriver: 8.0 ) // Should succeed but not have both games in same route if result.isSuccess { // May have single-game options but not both together #expect(true) } } @Test("plan with two drivers allows longer routes") func plan_TwoDrivers_AllowsLongerRoutes() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let denver = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903) // ~1000 miles, ~17 hours - doable with 2 drivers let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: denver.id, dateTime: date(daysFrom: baseDate(), days: 2)) ] let result = plan( games: games, stadiums: [la, denver], dateRange: makeDateRange(start: baseDate(), days: 10), numberOfDrivers: 2, maxHoursPerDriver: 8.0 ) #expect(result.isSuccess) } // MARK: - Stop Grouping Tests @Test("multiple games at same stadium grouped into one stop") func plan_SameStadiumGames_GroupedIntoOneStop() { let stadium = makeStadium(city: "Chicago", latitude: 41.8781, longitude: -87.6298) let games = [ makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 1)), makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) ] let result = plan( games: [games[0], games[1], games[2]], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.stops.count == 1) #expect(result.options.first?.stops.first?.games.count == 3) } @Test("stop arrival date is first game date") func plan_StopArrivalDate_IsFirstGameDate() { let stadium = makeStadium(city: "Chicago", latitude: 41.8781, longitude: -87.6298) let firstGame = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let secondGame = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 3)) let result = plan( games: [firstGame, secondGame], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let stop = result.options.first?.stops.first let firstGameDate = Calendar.current.startOfDay(for: firstGame.startTime) let stopArrival = Calendar.current.startOfDay(for: stop?.arrivalDate ?? Date.distantPast) #expect(firstGameDate == stopArrival) } @Test("stop departure date is last game date") func plan_StopDepartureDate_IsLastGameDate() { let stadium = makeStadium(city: "Chicago", latitude: 41.8781, longitude: -87.6298) let firstGame = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let secondGame = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 4)) let result = plan( games: [firstGame, secondGame], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let stop = result.options.first?.stops.first let lastGameDate = Calendar.current.startOfDay(for: secondGame.startTime) let stopDeparture = Calendar.current.startOfDay(for: stop?.departureDate ?? Date.distantFuture) #expect(lastGameDate == stopDeparture) } // MARK: - Travel Segment Tests @Test("single stop has zero travel segments") func plan_SingleStop_ZeroTravelSegments() { let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903) let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.travelSegments.isEmpty == true) } @Test("two stops have one travel segment") func plan_TwoStops_OneTravelSegment() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3)) ] let result = plan( games: games, stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let twoStopOption = result.options.first { $0.stops.count == 2 } #expect(twoStopOption?.travelSegments.count == 1) } @Test("travel segment has correct origin and destination") func plan_TravelSegment_CorrectOriginDestination() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3)) ] let result = plan( games: games, stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let twoStopOption = result.options.first { $0.stops.count == 2 } let segment = twoStopOption?.travelSegments.first #expect(segment?.fromLocation.name == "Los Angeles") #expect(segment?.toLocation.name == "San Francisco") } @Test("travel segment distance is reasonable for LA to SF") func plan_TravelSegment_ReasonableDistance() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3)) ] let result = plan( games: games, stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let twoStopOption = result.options.first { $0.stops.count == 2 } let distance = twoStopOption?.totalDistanceMiles ?? 0 // LA to SF is ~380 miles, with routing factor ~500 miles #expect(distance > 400 && distance < 600) } // MARK: - Option Ranking Tests @Test("options are ranked starting from 1") func plan_Options_RankedFromOne() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3)) ] let result = plan( games: games, stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.rank == 1) } @Test("all options have valid isValid property") func plan_Options_AllValid() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3)) ] let result = plan( games: games, stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) for option in result.options { #expect(option.isValid, "All options should pass isValid check") } } @Test("totalGames computed property is correct") func plan_TotalGames_ComputedCorrectly() { let stadium = makeStadium(city: "Chicago", latitude: 41.8781, longitude: -87.6298) let games = [ makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 1)), makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) ] let result = plan( games: games, stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.totalGames == 3) } // MARK: - Edge Cases @Test("games in reverse chronological order still processed correctly") func plan_ReverseChronologicalGames_ProcessedCorrectly() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) // Games added in reverse order let game1 = makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 5)) let game2 = makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 2)) let result = plan( games: [game1, game2], // SF first (later date) stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) // Should be sorted: LA (day 2) then SF (day 5) let twoStopOption = result.options.first { $0.stops.count == 2 } #expect(twoStopOption?.stops[0].city == "Los Angeles") #expect(twoStopOption?.stops[1].city == "San Francisco") } @Test("handles many games efficiently") func plan_ManyGames_HandledEfficiently() { var stadiums: [Stadium] = [] var games: [Game] = [] // Create 15 games along the west coast let cities: [(String, Double, Double)] = [ ("San Diego", 32.7157, -117.1611), ("Los Angeles", 34.0522, -118.2437), ("Bakersfield", 35.3733, -119.0187), ("Fresno", 36.7378, -119.7871), ("San Jose", 37.3382, -121.8863), ("San Francisco", 37.7749, -122.4194), ("Oakland", 37.8044, -122.2712), ("Sacramento", 38.5816, -121.4944), ("Reno", 39.5296, -119.8138), ("Redding", 40.5865, -122.3917), ("Eugene", 44.0521, -123.0868), ("Portland", 45.5152, -122.6784), ("Seattle", 47.6062, -122.3321), ("Tacoma", 47.2529, -122.4443), ("Vancouver", 49.2827, -123.1207) ] for (index, city) in cities.enumerated() { let id = UUID() stadiums.append(makeStadium(id: id, city: city.0, latitude: city.1, longitude: city.2)) games.append(makeGame(stadiumId: id, dateTime: date(daysFrom: baseDate(), days: index))) } let result = plan( games: games, stadiums: stadiums, dateRange: makeDateRange(start: baseDate(), days: 20) ) #expect(result.isSuccess) #expect(result.options.count <= 10) } @Test("empty stadiums dictionary returns failure") func plan_EmptyStadiums_ReturnsSuccess() { let stadiumId = UUID() let game = makeGame(stadiumId: stadiumId, dateTime: date(daysFrom: baseDate(), days: 2)) // Game exists but stadium not in dictionary let result = plan( games: [game], stadiums: [], dateRange: makeDateRange(start: baseDate(), days: 10) ) // Should handle gracefully (may return failure or success with empty) #expect(result.failure != nil || result.options.isEmpty || result.isSuccess) } @Test("stop has correct city from stadium") func plan_StopCity_MatchesStadium() { let stadium = makeStadium(city: "Phoenix", latitude: 33.4484, longitude: -112.0740) let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.stops.first?.city == "Phoenix") } @Test("stop has correct state from stadium") func plan_StopState_MatchesStadium() { let stadium = makeStadium(city: "Phoenix", latitude: 33.4484, longitude: -112.0740) let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.stops.first?.state == "ST") } @Test("stop has coordinate from stadium") func plan_StopCoordinate_MatchesStadium() { let stadium = makeStadium(city: "Phoenix", latitude: 33.4484, longitude: -112.0740) let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let coord = result.options.first?.stops.first?.coordinate #expect(coord != nil) #expect(abs(coord!.latitude - 33.4484) < 0.01) #expect(abs(coord!.longitude - (-112.0740)) < 0.01) } @Test("firstGameStart property is set correctly") func plan_FirstGameStart_SetCorrectly() { let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903) let gameTime = date(daysFrom: baseDate(), days: 2, hour: 19) let game = makeGame(stadiumId: stadium.id, dateTime: gameTime) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let firstGameStart = result.options.first?.stops.first?.firstGameStart #expect(firstGameStart == gameTime) } @Test("location property has correct name") func plan_LocationProperty_CorrectName() { let stadium = makeStadium(city: "Austin", latitude: 30.2672, longitude: -97.7431) let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.stops.first?.location.name == "Austin") } @Test("geographicRationale shows game count") func plan_GeographicRationale_ShowsGameCount() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3)) ] let result = plan( games: games, stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let twoStopOption = result.options.first { $0.stops.count == 2 } #expect(twoStopOption?.geographicRationale.contains("2") == true) } @Test("options with same game count sorted by driving hours") func plan_SameGameCount_SortedByDrivingHours() { // Create scenario where multiple routes have same game count let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3)) ] let result = plan( games: games, stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) // All options should be valid and sorted for option in result.options { #expect(option.isValid) } } // MARK: - Timezone Boundary Tests @Test("game at range start in different timezone is included") func plan_GameAtRangeStartDifferentTimezone_Included() { // Date range: Jan 5 00:00 PST to Jan 10 23:59 PST let pstCalendar = Calendar.current var pstComponents = DateComponents() pstComponents.year = 2026 pstComponents.month = 1 pstComponents.day = 5 pstComponents.hour = 0 pstComponents.minute = 0 pstComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")! let rangeStart = pstCalendar.date(from: pstComponents)! var endComponents = DateComponents() endComponents.year = 2026 endComponents.month = 1 endComponents.day = 10 endComponents.hour = 23 endComponents.minute = 59 endComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")! let rangeEnd = pstCalendar.date(from: endComponents)! let dateRange = DateInterval(start: rangeStart, end: rangeEnd) // Game: Jan 5 19:00 EST (New York) = Jan 5 16:00 PST let nyStadium = makeStadium(city: "New York", latitude: 40.7128, longitude: -74.0060) var estComponents = DateComponents() estComponents.year = 2026 estComponents.month = 1 estComponents.day = 5 estComponents.hour = 19 estComponents.minute = 0 estComponents.timeZone = TimeZone(identifier: "America/New_York")! let gameTime = pstCalendar.date(from: estComponents)! let game = makeGame(stadiumId: nyStadium.id, dateTime: gameTime) let result = plan( games: [game], stadiums: [nyStadium], dateRange: dateRange ) // Game should be included (within PST range) #expect(result.isSuccess) #expect(result.options.first?.stops.count == 1) } @Test("game just before range start in different timezone is excluded") func plan_GameBeforeRangeStartDifferentTimezone_Excluded() { // Date range: Jan 5 00:00 PST to Jan 10 23:59 PST let pstCalendar = Calendar.current var pstComponents = DateComponents() pstComponents.year = 2026 pstComponents.month = 1 pstComponents.day = 5 pstComponents.hour = 0 pstComponents.minute = 0 pstComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")! let rangeStart = pstCalendar.date(from: pstComponents)! var endComponents = DateComponents() endComponents.year = 2026 endComponents.month = 1 endComponents.day = 10 endComponents.hour = 23 endComponents.minute = 59 endComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")! let rangeEnd = pstCalendar.date(from: endComponents)! let dateRange = DateInterval(start: rangeStart, end: rangeEnd) // Game: Jan 4 22:00 EST (New York) = Jan 4 19:00 PST let nyStadium = makeStadium(city: "New York", latitude: 40.7128, longitude: -74.0060) var estComponents = DateComponents() estComponents.year = 2026 estComponents.month = 1 estComponents.day = 4 estComponents.hour = 22 estComponents.minute = 0 estComponents.timeZone = TimeZone(identifier: "America/New_York")! let gameTime = pstCalendar.date(from: estComponents)! let game = makeGame(stadiumId: nyStadium.id, dateTime: gameTime) let result = plan( games: [game], stadiums: [nyStadium], dateRange: dateRange ) // Game should be excluded (before PST range start) #expect(result.failure?.reason == .noGamesInRange) } @Test("game at range end in different timezone is included") func plan_GameAtRangeEndDifferentTimezone_Included() { // Date range: Jan 5 00:00 PST to Jan 10 23:59 PST let pstCalendar = Calendar.current var pstComponents = DateComponents() pstComponents.year = 2026 pstComponents.month = 1 pstComponents.day = 5 pstComponents.hour = 0 pstComponents.minute = 0 pstComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")! let rangeStart = pstCalendar.date(from: pstComponents)! var endComponents = DateComponents() endComponents.year = 2026 endComponents.month = 1 endComponents.day = 10 endComponents.hour = 23 endComponents.minute = 59 endComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")! let rangeEnd = pstCalendar.date(from: endComponents)! let dateRange = DateInterval(start: rangeStart, end: rangeEnd) // Game: Jan 10 21:00 EST (New York) = Jan 10 18:00 PST let nyStadium = makeStadium(city: "New York", latitude: 40.7128, longitude: -74.0060) var estComponents = DateComponents() estComponents.year = 2026 estComponents.month = 1 estComponents.day = 10 estComponents.hour = 21 estComponents.minute = 0 estComponents.timeZone = TimeZone(identifier: "America/New_York")! let gameTime = pstCalendar.date(from: estComponents)! let game = makeGame(stadiumId: nyStadium.id, dateTime: gameTime) let result = plan( games: [game], stadiums: [nyStadium], dateRange: dateRange ) // Game should be included (within PST range) #expect(result.isSuccess) #expect(result.options.first?.stops.count == 1) } @Test("game just after range end in different timezone is excluded") func plan_GameAfterRangeEndDifferentTimezone_Excluded() { // Date range: Jan 5 00:00 PST to Jan 10 23:59 PST let pstCalendar = Calendar.current var pstComponents = DateComponents() pstComponents.year = 2026 pstComponents.month = 1 pstComponents.day = 5 pstComponents.hour = 0 pstComponents.minute = 0 pstComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")! let rangeStart = pstCalendar.date(from: pstComponents)! var endComponents = DateComponents() endComponents.year = 2026 endComponents.month = 1 endComponents.day = 10 endComponents.hour = 23 endComponents.minute = 59 endComponents.timeZone = TimeZone(identifier: "America/Los_Angeles")! let rangeEnd = pstCalendar.date(from: endComponents)! let dateRange = DateInterval(start: rangeStart, end: rangeEnd) // Game: Jan 11 02:00 EST (New York) = Jan 10 23:00 PST // This is actually WITHIN the range (before 23:59 PST) // Let me adjust: Jan 11 03:00 EST = Jan 11 00:00 PST (after range) let nyStadium = makeStadium(city: "New York", latitude: 40.7128, longitude: -74.0060) var estComponents = DateComponents() estComponents.year = 2026 estComponents.month = 1 estComponents.day = 11 estComponents.hour = 3 estComponents.minute = 0 estComponents.timeZone = TimeZone(identifier: "America/New_York")! let gameTime = pstCalendar.date(from: estComponents)! let game = makeGame(stadiumId: nyStadium.id, dateTime: gameTime) let result = plan( games: [game], stadiums: [nyStadium], dateRange: dateRange ) // Game should be excluded (after PST range end) #expect(result.failure?.reason == .noGamesInRange) } // MARK: - Same-Day Multi-City Conflict Tests @Test("same-day games in close cities are both included in route") func plan_SameDayGamesCloseCities_BothIncluded() { // LA game at 1pm, San Diego game at 7pm (120 miles, ~2hr drive) let laStadium = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sdStadium = makeStadium(city: "San Diego", latitude: 32.7157, longitude: -117.1611) let base = baseDate() let laGame = makeGame(stadiumId: laStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 13)) let sdGame = makeGame(stadiumId: sdStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 19)) let result = plan( games: [laGame, sdGame], stadiums: [laStadium, sdStadium], dateRange: makeDateRange(start: base, days: 10) ) // Should succeed with both games in route (enough time to drive between) #expect(result.isSuccess) let twoStopOption = result.options.first { $0.stops.count == 2 } #expect(twoStopOption != nil, "Should have route with both cities") #expect(twoStopOption?.totalGames == 2) } @Test("same-day games in distant cities only one included per route") func plan_SameDayGamesDistantCities_OnlyOnePerRoute() { // LA game at 1pm, SF game at 7pm (380 miles, ~6hr drive) let laStadium = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sfStadium = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) let base = baseDate() let laGame = makeGame(stadiumId: laStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 13)) let sfGame = makeGame(stadiumId: sfStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 19)) let result = plan( games: [laGame, sfGame], stadiums: [laStadium, sfStadium], dateRange: makeDateRange(start: base, days: 10) ) // Should succeed but each route picks ONE game (cannot attend both same day) #expect(result.isSuccess) for option in result.options { // Each option should have only 1 stop (cannot do both same day) #expect(option.stops.count == 1, "Route should pick only one game - cannot attend both LA and SF same day") } } @Test("same-day games on opposite coasts only one included per route") func plan_SameDayGamesOppositCoasts_OnlyOnePerRoute() { // LA game at 1pm PST, NY game at 7pm EST (2800 miles, impossible same day) let laStadium = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let nyStadium = makeStadium(city: "New York", latitude: 40.7128, longitude: -74.0060) let base = baseDate() let laGame = makeGame(stadiumId: laStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 13)) let nyGame = makeGame(stadiumId: nyStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 19)) let result = plan( games: [laGame, nyGame], stadiums: [laStadium, nyStadium], dateRange: makeDateRange(start: base, days: 10) ) // Should succeed but each route picks ONE game (obviously impossible same day) #expect(result.isSuccess) for option in result.options { #expect(option.stops.count == 1, "Route should pick only one game - cannot attend both coasts same day") } } @Test("three same-day games picks feasible combinations") func plan_ThreeSameDayGames_PicksFeasibleCombinations() { // LA 1pm, Anaheim 4pm (30mi), San Diego 7pm (90mi from Anaheim) // Feasible: LA→Anaheim→SD // Cannot include NY game same day let laStadium = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let anaheimStadium = makeStadium(city: "Anaheim", latitude: 33.8003, longitude: -117.8827) let sdStadium = makeStadium(city: "San Diego", latitude: 32.7157, longitude: -117.1611) let nyStadium = makeStadium(city: "New York", latitude: 40.7128, longitude: -74.0060) let base = baseDate() let laGame = makeGame(stadiumId: laStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 13)) let anaheimGame = makeGame(stadiumId: anaheimStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 16)) let sdGame = makeGame(stadiumId: sdStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 19)) let nyGame = makeGame(stadiumId: nyStadium.id, dateTime: date(daysFrom: base, days: 0, hour: 19)) let result = plan( games: [laGame, anaheimGame, sdGame, nyGame], stadiums: [laStadium, anaheimStadium, sdStadium, nyStadium], dateRange: makeDateRange(start: base, days: 10) ) // Should have options, and best option includes the 3 West Coast games #expect(result.isSuccess) // Should have a 3-stop option (LA→Anaheim→SD) let threeStopOption = result.options.first { $0.stops.count == 3 } #expect(threeStopOption != nil, "Should have route with 3 West Coast stops") #expect(threeStopOption?.totalGames == 3) // No option should include NY with any other game from same day for option in result.options { let cities = option.stops.map { $0.city } if cities.contains("New York") { #expect(option.stops.count == 1, "NY game cannot be combined with West Coast games same day") } } } }