// // ScenarioBPlannerTests.swift // SportsTimeTests // // Comprehensive tests for ScenarioBPlanner: Selected games scenario. // Tests sliding window logic, anchor game requirements, and route generation. // import Testing import Foundation import CoreLocation @testable import SportsTime @Suite(.serialized) struct ScenarioBPlannerTests { // MARK: - Test Fixtures private func makeStadium( id: UUID = UUID(), name: String, city: String, state: String, latitude: Double, longitude: Double, sport: Sport = .mlb ) -> Stadium { Stadium( id: id, name: name, city: city, state: state, latitude: latitude, longitude: longitude, capacity: 40000, sport: sport ) } private func makeGame( id: UUID = UUID(), stadiumId: UUID, date: Date ) -> Game { Game( id: id, homeTeamId: UUID(), awayTeamId: UUID(), stadiumId: stadiumId, dateTime: date, sport: .mlb, season: "2026" ) } private func date(_ string: String) -> Date { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd HH:mm" formatter.timeZone = TimeZone(identifier: "America/Los_Angeles") return formatter.date(from: string)! } private func makeRequest( games: [Game], stadiums: [UUID: Stadium], mustSeeGameIds: Set, startDate: Date, endDate: Date, tripDuration: Int? = nil, numberOfDrivers: Int = 1 ) -> PlanningRequest { var prefs = TripPreferences( startDate: startDate, endDate: endDate, tripDuration: tripDuration, numberOfDrivers: numberOfDrivers ) prefs.mustSeeGameIds = mustSeeGameIds return PlanningRequest( preferences: prefs, availableGames: games, teams: [:], stadiums: stadiums ) } // Standard test stadiums - close together for feasible routes private var laStadium: Stadium { makeStadium(id: UUID(), name: "Dodger Stadium", city: "Los Angeles", state: "CA", latitude: 34.0739, longitude: -118.2400) } private var sfStadium: Stadium { makeStadium(id: UUID(), name: "Oracle Park", city: "San Francisco", state: "CA", latitude: 37.7786, longitude: -122.3893) } private var sdStadium: Stadium { makeStadium(id: UUID(), name: "Petco Park", city: "San Diego", state: "CA", latitude: 32.7076, longitude: -117.1570) } private var phoenixStadium: Stadium { makeStadium(id: UUID(), name: "Chase Field", city: "Phoenix", state: "AZ", latitude: 33.4455, longitude: -112.0667) } private var denverStadium: Stadium { makeStadium(id: UUID(), name: "Coors Field", city: "Denver", state: "CO", latitude: 39.7559, longitude: -104.9942) } // MARK: - Basic Validation Tests @Test("Empty selected games returns failure") func plan_EmptySelectedGames_ReturnsFailure() { let planner = ScenarioBPlanner() let la = laStadium let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let request = makeRequest( games: [game], stadiums: [la.id: la], mustSeeGameIds: [], // Empty! startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .failure(let failure) = result { #expect(failure.reason == .noValidRoutes) #expect(failure.violations.contains { $0.type == .selectedGames }) } else { Issue.record("Expected failure for empty selected games") } } @Test("Single selected game returns success") func plan_SingleSelectedGame_ReturnsSuccess() { let planner = ScenarioBPlanner() let la = laStadium let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let request = makeRequest( games: [game], stadiums: [la.id: la], mustSeeGameIds: [game.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { #expect(!options.isEmpty) #expect(options.first?.stops.count == 1) } else { Issue.record("Expected success for single selected game") } } @Test("No date range or duration returns failure") func plan_NoDateRangeOrDuration_ReturnsFailure() { let planner = ScenarioBPlanner() let la = laStadium let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) // Use inverted date range to trigger nil dateRange let request = makeRequest( games: [game], stadiums: [la.id: la], mustSeeGameIds: [game.id], startDate: date("2026-06-30 00:00"), endDate: date("2026-06-01 00:00"), // Before start! tripDuration: 0 // Zero duration ) let result = planner.plan(request: request) if case .failure(let failure) = result { #expect(failure.reason == .missingDateRange) } else { // This test may pass if the system handles it differently } } // MARK: - Selected Games Anchor Tests @Test("Selected game appears in all options") func plan_SelectedGame_AppearsInAllOptions() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let selectedGame = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let otherGame = makeGame(stadiumId: sf.id, date: date("2026-06-16 19:00")) let request = makeRequest( games: [selectedGame, otherGame], stadiums: [la.id: la, sf.id: sf], mustSeeGameIds: [selectedGame.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { for option in options { let allGameIds = option.stops.flatMap { $0.games } #expect(allGameIds.contains(selectedGame.id), "Selected game must be in every option") } } else { Issue.record("Expected success") } } @Test("Multiple selected games all appear in options") func plan_MultipleSelectedGames_AllAppearInOptions() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00")) let request = makeRequest( games: [game1, game2], stadiums: [la.id: la, sf.id: sf], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { for option in options { let allGameIds = option.stops.flatMap { $0.games } #expect(allGameIds.contains(game1.id), "First selected game must be present") #expect(allGameIds.contains(game2.id), "Second selected game must be present") } } else { Issue.record("Expected success") } } @Test("Selected games outside date range fail") func plan_SelectedGamesOutsideRange_Fails() { let planner = ScenarioBPlanner() let la = laStadium // Game is in July, date range is June let game = makeGame(stadiumId: la.id, date: date("2026-07-15 19:00")) let request = makeRequest( games: [game], stadiums: [la.id: la], mustSeeGameIds: [game.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .failure = result { // Expected } else { Issue.record("Expected failure when selected games outside date range") } } // MARK: - Sliding Window Tests @Test("Trip duration generates sliding windows") func plan_TripDuration_GeneratesSlidingWindows() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium // Selected games on day 5 and day 8 let game1 = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00")) let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-08 19:00")) // Bonus games on various days let bonus1 = makeGame(stadiumId: la.id, date: date("2026-06-03 19:00")) let bonus2 = makeGame(stadiumId: sf.id, date: date("2026-06-12 19:00")) let request = makeRequest( games: [game1, game2, bonus1, bonus2], stadiums: [la.id: la, sf.id: sf], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-05-01 00:00"), endDate: date("2026-05-01 00:00"), // Trigger sliding window mode tripDuration: 10 ) let result = planner.plan(request: request) // Should succeed - sliding windows cover both selected games if case .success(let options) = result { #expect(!options.isEmpty) } else { // May fail due to date range issues - that's ok } } @Test("Short duration fits selected games") func plan_ShortDuration_FitsSelectedGames() { let planner = ScenarioBPlanner() let la = laStadium // Games 2 days apart let game1 = makeGame(stadiumId: la.id, date: date("2026-06-05 19:00")) let game2 = makeGame(stadiumId: la.id, date: date("2026-06-07 19:00")) let request = makeRequest( games: [game1, game2], stadiums: [la.id: la], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59"), tripDuration: 3 // Just enough to fit ) let result = planner.plan(request: request) if case .success(let options) = result { #expect(!options.isEmpty) } else { Issue.record("Expected success when duration fits games") } } @Test("Duration equal to game span works") func plan_DurationEqualsGameSpan_Works() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium // Games 5 days apart let game1 = makeGame(stadiumId: la.id, date: date("2026-06-01 19:00")) let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-06 19:00")) let request = makeRequest( games: [game1, game2], stadiums: [la.id: la, sf.id: sf], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-10 23:59") ) let result = planner.plan(request: request) if case .success = result { // Expected } else { Issue.record("Expected success when duration equals game span") } } // MARK: - Bonus Games Tests @Test("Bonus games added to route") func plan_BonusGames_AddedToRoute() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let sd = sdStadium let selectedGame = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let bonusGame1 = makeGame(stadiumId: sf.id, date: date("2026-06-16 19:00")) let bonusGame2 = makeGame(stadiumId: sd.id, date: date("2026-06-14 19:00")) let request = makeRequest( games: [selectedGame, bonusGame1, bonusGame2], stadiums: [la.id: la, sf.id: sf, sd.id: sd], mustSeeGameIds: [selectedGame.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { // Should have options with bonus games let maxGames = options.map { $0.stops.flatMap { $0.games }.count }.max() ?? 0 #expect(maxGames > 1, "Should include bonus games in some options") } else { Issue.record("Expected success") } } @Test("Geographic rationale shows selected and bonus counts") func plan_GeographicRationale_ShowsSelectedAndBonusCounts() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let selectedGame = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let bonusGame = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00")) let request = makeRequest( games: [selectedGame, bonusGame], stadiums: [la.id: la, sf.id: sf], mustSeeGameIds: [selectedGame.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { // Find option with both games if let optionWithBoth = options.first(where: { $0.stops.flatMap { $0.games }.count > 1 }) { #expect(optionWithBoth.geographicRationale.contains("selected")) #expect(optionWithBoth.geographicRationale.contains("bonus")) } } else { Issue.record("Expected success") } } // MARK: - Travel Segments Tests @Test("Two stops have one travel segment") func plan_TwoStops_OneTravelSegment() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00")) let request = makeRequest( games: [game1, game2], stadiums: [la.id: la, sf.id: sf], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { for option in options { if option.stops.count == 2 { #expect(option.travelSegments.count == 1) } } } else { Issue.record("Expected success") } } @Test("Single stop has no travel segments") func plan_SingleStop_NoTravelSegments() { let planner = ScenarioBPlanner() let la = laStadium let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let request = makeRequest( games: [game], stadiums: [la.id: la], mustSeeGameIds: [game.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { for option in options { if option.stops.count == 1 { #expect(option.travelSegments.isEmpty) } } } else { Issue.record("Expected success") } } @Test("Travel segment has valid origin and destination") func plan_TravelSegment_ValidOriginDestination() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00")) let request = makeRequest( games: [game1, game2], stadiums: [la.id: la, sf.id: sf], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { for option in options { for segment in option.travelSegments { #expect(!segment.fromLocation.name.isEmpty || segment.fromLocation.coordinate != nil) #expect(!segment.toLocation.name.isEmpty || segment.toLocation.coordinate != nil) } } } else { Issue.record("Expected success") } } // MARK: - Stop Building Tests @Test("Games at same stadium grouped into one stop") func plan_SameStadiumGames_GroupedIntoOneStop() { let planner = ScenarioBPlanner() let la = laStadium let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let game2 = makeGame(stadiumId: la.id, date: date("2026-06-16 19:00")) let request = makeRequest( games: [game1, game2], stadiums: [la.id: la], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { if let option = options.first { #expect(option.stops.count == 1, "Two games at same stadium = one stop") #expect(option.stops.first?.games.count == 2, "Stop should have both games") } } else { Issue.record("Expected success") } } @Test("Stop city matches stadium city") func plan_StopCity_MatchesStadiumCity() { let planner = ScenarioBPlanner() let la = laStadium let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let request = makeRequest( games: [game], stadiums: [la.id: la], mustSeeGameIds: [game.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { if let stop = options.first?.stops.first { #expect(stop.city == "Los Angeles") #expect(stop.state == "CA") } } else { Issue.record("Expected success") } } @Test("Stop coordinate matches stadium") func plan_StopCoordinate_MatchesStadium() { let planner = ScenarioBPlanner() let la = laStadium let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let request = makeRequest( games: [game], stadiums: [la.id: la], mustSeeGameIds: [game.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { if let stop = options.first?.stops.first { #expect(stop.coordinate != nil) #expect(abs(stop.coordinate!.latitude - 34.0739) < 0.01) } } else { Issue.record("Expected success") } } @Test("Stop arrival date is first game date") func plan_StopArrivalDate_IsFirstGameDate() { let planner = ScenarioBPlanner() let la = laStadium let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let game2 = makeGame(stadiumId: la.id, date: date("2026-06-17 19:00")) let request = makeRequest( games: [game1, game2], stadiums: [la.id: la], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { if let stop = options.first?.stops.first { let calendar = Calendar.current let arrivalDay = calendar.component(.day, from: stop.arrivalDate) #expect(arrivalDay == 15) } } else { Issue.record("Expected success") } } @Test("Stop departure date is last game date") func plan_StopDepartureDate_IsLastGameDate() { let planner = ScenarioBPlanner() let la = laStadium let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let game2 = makeGame(stadiumId: la.id, date: date("2026-06-17 19:00")) let request = makeRequest( games: [game1, game2], stadiums: [la.id: la], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { if let stop = options.first?.stops.first { let calendar = Calendar.current let departureDay = calendar.component(.day, from: stop.departureDate) #expect(departureDay == 17) } } else { Issue.record("Expected success") } } // MARK: - Ranking Tests @Test("Options ranked from 1") func plan_OptionsRankedFromOne() { let planner = ScenarioBPlanner() let la = laStadium let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let request = makeRequest( games: [game], stadiums: [la.id: la], mustSeeGameIds: [game.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { #expect(options.first?.rank == 1) for (index, option) in options.enumerated() { #expect(option.rank == index + 1) } } else { Issue.record("Expected success") } } @Test("More games ranked higher") func plan_MoreGames_RankedHigher() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let sd = sdStadium let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-16 19:00")) let game3 = makeGame(stadiumId: sd.id, date: date("2026-06-14 19:00")) let request = makeRequest( games: [game1, game2, game3], stadiums: [la.id: la, sf.id: sf, sd.id: sd], mustSeeGameIds: [game1.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { // Options should be sorted by game count (most first) var prevCount = Int.max for option in options { let count = option.stops.flatMap { $0.games }.count #expect(count <= prevCount, "Should be sorted by game count descending") prevCount = count } } else { Issue.record("Expected success") } } @Test("Max 10 options returned") func plan_Max10OptionsReturned() { let planner = ScenarioBPlanner() let la = laStadium // Create many games in same city to generate many sliding window options var games: [Game] = [] for day in 1...20 { games.append(makeGame( stadiumId: la.id, date: date("2026-06-\(String(format: "%02d", day)) 19:00") )) } // Use tripDuration to enable sliding windows (many possible 5-day windows) // With 20 games and 5-day duration, there will be many valid windows let request = makeRequest( games: games, stadiums: [la.id: la], mustSeeGameIds: [games[10].id], // Select game in middle startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59"), tripDuration: 5 ) let result = planner.plan(request: request) if case .success(let options) = result { #expect(options.count <= 10, "Should return at most 10 options") #expect(options.count > 0, "Should return at least one option") } else { Issue.record("Expected success") } } // MARK: - Driving Constraints Tests @Test("Two drivers allows longer routes") func plan_TwoDrivers_AllowsLongerRoutes() { let planner = ScenarioBPlanner() let la = laStadium let phoenix = phoenixStadium let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let game2 = makeGame(stadiumId: phoenix.id, date: date("2026-06-16 19:00")) let request1 = makeRequest( games: [game1, game2], stadiums: [la.id: la, phoenix.id: phoenix], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59"), numberOfDrivers: 1 ) let request2 = makeRequest( games: [game1, game2], stadiums: [la.id: la, phoenix.id: phoenix], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59"), numberOfDrivers: 2 ) let result1 = planner.plan(request: request1) let result2 = planner.plan(request: request2) // Both should succeed for short LA-Phoenix trip // But 2 drivers gives more flexibility if case .success(let options1) = result1, case .success(let options2) = result2 { #expect(!options1.isEmpty && !options2.isEmpty) } } @Test("Exceeding daily driving limit fails route") func plan_ExceedingDailyLimit_FailsRoute() { let planner = ScenarioBPlanner() // Create far-apart stadiums let nyStadium = makeStadium( name: "Yankee Stadium", city: "New York", state: "NY", latitude: 40.8296, longitude: -73.9262 ) let sfStadium = makeStadium( name: "Oracle Park", city: "San Francisco", state: "CA", latitude: 37.7786, longitude: -122.3893 ) // Games only 1 day apart - impossible to drive let game1 = makeGame(stadiumId: nyStadium.id, date: date("2026-06-15 19:00")) let game2 = makeGame(stadiumId: sfStadium.id, date: date("2026-06-16 19:00")) let request = makeRequest( games: [game1, game2], stadiums: [nyStadium.id: nyStadium, sfStadium.id: sfStadium], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59"), numberOfDrivers: 1 ) let result = planner.plan(request: request) // Should fail - can't drive NY to SF in one day if case .failure = result { // Expected } else { // Also acceptable if it routes but validates later } } // MARK: - Edge Cases @Test("All games in future works") func plan_AllGamesInFuture_Works() { let planner = ScenarioBPlanner() let la = laStadium let futureDate = Calendar.current.date(byAdding: .year, value: 1, to: Date())! let game = makeGame(stadiumId: la.id, date: futureDate) let rangeStart = Calendar.current.date(byAdding: .day, value: -30, to: futureDate)! let rangeEnd = Calendar.current.date(byAdding: .day, value: 30, to: futureDate)! let request = makeRequest( games: [game], stadiums: [la.id: la], mustSeeGameIds: [game.id], startDate: rangeStart, endDate: rangeEnd ) let result = planner.plan(request: request) if case .success(let options) = result { #expect(!options.isEmpty) } else { Issue.record("Expected success for future games") } } @Test("Games in chronological order processed correctly") func plan_ChronologicalGames_ProcessedCorrectly() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let sd = sdStadium let game1 = makeGame(stadiumId: sd.id, date: date("2026-06-14 19:00")) let game2 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let game3 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00")) let request = makeRequest( games: [game1, game2, game3], stadiums: [la.id: la, sf.id: sf, sd.id: sd], mustSeeGameIds: [game1.id, game2.id, game3.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { #expect(!options.isEmpty) // First stop should be SD (earliest game) if let firstStop = options.first?.stops.first { #expect(firstStop.city == "San Diego") } } else { Issue.record("Expected success") } } @Test("Reverse chronological games reordered") func plan_ReverseChronologicalGames_Reordered() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium // Input games in reverse order let game1 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00")) let game2 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let request = makeRequest( games: [game1, game2], // SF first, LA second stadiums: [la.id: la, sf.id: sf], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { #expect(!options.isEmpty) // LA should come before SF in route if let option = options.first, option.stops.count == 2 { #expect(option.stops[0].city == "Los Angeles") #expect(option.stops[1].city == "San Francisco") } } else { Issue.record("Expected success") } } @Test("First game start time set correctly") func plan_FirstGameStartTime_SetCorrectly() { let planner = ScenarioBPlanner() let la = laStadium let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let request = makeRequest( games: [game], stadiums: [la.id: la], mustSeeGameIds: [game.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { if let stop = options.first?.stops.first { #expect(stop.firstGameStart != nil) // Verify it's the same as the game's dateTime #expect(stop.firstGameStart == game.dateTime) } } else { Issue.record("Expected success") } } @Test("Location property has correct name") func plan_LocationProperty_CorrectName() { let planner = ScenarioBPlanner() let la = laStadium let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let request = makeRequest( games: [game], stadiums: [la.id: la], mustSeeGameIds: [game.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { if let stop = options.first?.stops.first { #expect(stop.location.name == "Los Angeles") } } else { Issue.record("Expected success") } } @Test("Empty stadiums dictionary still works") func plan_EmptyStadiums_StillWorks() { let planner = ScenarioBPlanner() let la = laStadium let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let request = makeRequest( games: [game], stadiums: [:], // Empty! mustSeeGameIds: [game.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) // Should still produce some result (with "Unknown" city) if case .success(let options) = result { #expect(!options.isEmpty) #expect(options.first?.stops.first?.city == "Unknown") } } @Test("Total driving hours computed correctly") func plan_TotalDrivingHours_ComputedCorrectly() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00")) let request = makeRequest( games: [game1, game2], stadiums: [la.id: la, sf.id: sf], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { for option in options { if option.stops.count > 1 { #expect(option.totalDrivingHours > 0) // LA to SF is ~380 miles, ~6 hours #expect(option.totalDrivingHours < 20, "LA-SF shouldn't take 20 hours") } } } else { Issue.record("Expected success") } } @Test("Total distance miles computed correctly") func plan_TotalDistanceMiles_ComputedCorrectly() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00")) let request = makeRequest( games: [game1, game2], stadiums: [la.id: la, sf.id: sf], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { for option in options { if option.stops.count > 1 { #expect(option.totalDistanceMiles > 0) // LA to SF is ~380 miles (with 1.3x factor ~500 miles) #expect(option.totalDistanceMiles > 300) #expect(option.totalDistanceMiles < 1000) } } } else { Issue.record("Expected success") } } @Test("Geographic rationale shows cities") func plan_GeographicRationale_ShowsCities() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00")) let request = makeRequest( games: [game1, game2], stadiums: [la.id: la, sf.id: sf], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { if let option = options.first { #expect(option.geographicRationale.contains("Los Angeles") || option.geographicRationale.contains("San Francisco")) } } else { Issue.record("Expected success") } } // MARK: - Complex Scenario Tests @Test("Three city route validates chronologically") func plan_ThreeCityRoute_ValidatesChronologically() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let sd = sdStadium let game1 = makeGame(stadiumId: sd.id, date: date("2026-06-14 19:00")) let game2 = makeGame(stadiumId: la.id, date: date("2026-06-16 19:00")) let game3 = makeGame(stadiumId: sf.id, date: date("2026-06-18 19:00")) let request = makeRequest( games: [game1, game2, game3], stadiums: [la.id: la, sf.id: sf, sd.id: sd], mustSeeGameIds: [game1.id, game2.id, game3.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { #expect(!options.isEmpty) // Route should be SD → LA → SF (chronological) if let option = options.first, option.stops.count == 3 { #expect(option.stops[0].city == "San Diego") #expect(option.stops[1].city == "Los Angeles") #expect(option.stops[2].city == "San Francisco") } } else { Issue.record("Expected success") } } @Test("Partial selection with bonus games") func plan_PartialSelection_WithBonusGames() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let sd = sdStadium let selectedGame = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let bonusGame1 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00")) let bonusGame2 = makeGame(stadiumId: sd.id, date: date("2026-06-13 19:00")) let request = makeRequest( games: [selectedGame, bonusGame1, bonusGame2], stadiums: [la.id: la, sf.id: sf, sd.id: sd], mustSeeGameIds: [selectedGame.id], // Only one selected startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { // All options must have the selected game for option in options { let allGames = option.stops.flatMap { $0.games } #expect(allGames.contains(selectedGame.id)) } // Some options may have bonus games let maxGames = options.map { $0.stops.flatMap { $0.games }.count }.max() ?? 0 #expect(maxGames >= 1) } else { Issue.record("Expected success") } } @Test("Same day games at different stadiums") func plan_SameDayDifferentStadiums_CreatesMultipleStops() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium // Both games on same day but different stadiums - will fail as impossible to attend both let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 14:00")) let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-15 20:00")) let request = makeRequest( games: [game1, game2], stadiums: [la.id: la, sf.id: sf], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) // May fail due to impossibility or generate separate options // Either is acceptable switch result { case .success, .failure: break // Either is acceptable } } @Test("Wide date range with few games") func plan_WideDateRangeFewGames_Succeeds() { let planner = ScenarioBPlanner() let la = laStadium let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) // Very wide date range (3 months) let request = makeRequest( games: [game], stadiums: [la.id: la], mustSeeGameIds: [game.id], startDate: date("2026-04-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { #expect(!options.isEmpty) } else { Issue.record("Expected success") } } @Test("Narrow date range matches exactly") func plan_NarrowDateRange_MatchesExactly() { let planner = ScenarioBPlanner() let la = laStadium let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) // Narrow date range (1 day) let request = makeRequest( games: [game], stadiums: [la.id: la], mustSeeGameIds: [game.id], startDate: date("2026-06-15 00:00"), endDate: date("2026-06-15 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { #expect(!options.isEmpty) } else { Issue.record("Expected success for exact date match") } } @Test("All options have valid structure") func plan_AllOptions_HaveValidStructure() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00")) let request = makeRequest( games: [game1, game2], stadiums: [la.id: la, sf.id: sf], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { for option in options { // Rank is positive #expect(option.rank > 0) // Stops non-empty #expect(!option.stops.isEmpty) // Travel segments = stops - 1 (or 0 for single stop) if option.stops.count > 1 { #expect(option.travelSegments.count == option.stops.count - 1) } // Driving hours is reasonable #expect(option.totalDrivingHours >= 0) // Distance is reasonable #expect(option.totalDistanceMiles >= 0) // Has rationale #expect(!option.geographicRationale.isEmpty) } } else { Issue.record("Expected success") } } @Test("Travel segments are drive mode") func plan_TravelSegments_AreDriveMode() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let game1 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let game2 = makeGame(stadiumId: sf.id, date: date("2026-06-17 19:00")) let request = makeRequest( games: [game1, game2], stadiums: [la.id: la, sf.id: sf], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { for option in options { for segment in option.travelSegments { #expect(segment.travelMode == .drive) } } } else { Issue.record("Expected success") } } @Test("Stops have games array populated") func plan_StopsHaveGamesArray_Populated() { let planner = ScenarioBPlanner() let la = laStadium let game = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let request = makeRequest( games: [game], stadiums: [la.id: la], mustSeeGameIds: [game.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { for option in options { for stop in option.stops { #expect(!stop.games.isEmpty, "Each stop must have at least one game") } } } else { Issue.record("Expected success") } } @Test("Multiple games at single stop sorted by time") func plan_MultipleGamesAtStop_SortedByTime() { let planner = ScenarioBPlanner() let la = laStadium let game1 = makeGame(stadiumId: la.id, date: date("2026-06-16 19:00")) let game2 = makeGame(stadiumId: la.id, date: date("2026-06-15 19:00")) let request = makeRequest( games: [game1, game2], stadiums: [la.id: la], mustSeeGameIds: [game1.id, game2.id], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { if let stop = options.first?.stops.first { // game2 (June 15) should be first, game1 (June 16) second #expect(stop.games.count == 2) #expect(stop.games[0] == game2.id) // Earlier game first #expect(stop.games[1] == game1.id) // Later game second } } else { Issue.record("Expected success") } } @Test("Failure contains constraint violation details") func plan_Failure_ContainsViolationDetails() { let planner = ScenarioBPlanner() // Empty selected games should cause failure with details let request = makeRequest( games: [], stadiums: [:], mustSeeGameIds: [], startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .failure(let failure) = result { #expect(!failure.violations.isEmpty) #expect(failure.violations.first?.severity == .error) #expect(!failure.violations.first!.description.isEmpty) } else { Issue.record("Expected failure") } } @Test("Handles many simultaneous selected games") func plan_ManySelectedGames_HandlesEfficiently() { let planner = ScenarioBPlanner() let la = laStadium // Create 10 selected games at same stadium var games: [Game] = [] var gameIds: Set = [] for day in 1...10 { let game = makeGame( stadiumId: la.id, date: date("2026-06-\(String(format: "%02d", day)) 19:00") ) games.append(game) gameIds.insert(game.id) } let request = makeRequest( games: games, stadiums: [la.id: la], mustSeeGameIds: gameIds, startDate: date("2026-06-01 00:00"), endDate: date("2026-06-30 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { #expect(!options.isEmpty) // Should be one stop with 10 games if let option = options.first { #expect(option.stops.count == 1) #expect(option.stops.first?.games.count == 10) } } else { Issue.record("Expected success") } } // MARK: - Filler Game Conflict Tests (Phase 09-02) @Test("filler game between must-see games is included when feasible") func plan_FillerBetweenAnchors_IncludedWhenFeasible() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium // Create San Jose stadium (between LA and SF) let sjStadium = makeStadium( name: "San Jose Stadium", city: "San Jose", state: "CA", latitude: 37.3382, longitude: -121.8863 ) // Must-see: LA Jan 5 1pm, SF Jan 7 7pm let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 13:00")) let sfGame = makeGame(stadiumId: sf.id, date: date("2026-01-07 19:00")) // Filler: San Jose Jan 6 7pm (between LA and SF, feasible timing) let sjGame = makeGame(stadiumId: sjStadium.id, date: date("2026-01-06 19:00")) let request = makeRequest( games: [laGame, sfGame, sjGame], stadiums: [la.id: la, sf.id: sf, sjStadium.id: sjStadium], mustSeeGameIds: [laGame.id, sfGame.id], startDate: date("2026-01-01 00:00"), endDate: date("2026-01-31 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { // Should have a 3-stop route: LA → SJ → SF let threeStopOption = options.first { $0.stops.count == 3 } #expect(threeStopOption != nil, "Should have route with filler game included") if let option = threeStopOption { let cities = option.stops.map { $0.city } #expect(cities.contains("San Jose"), "Filler city should be included") } } else { Issue.record("Expected success") } } @Test func plan_FillerSameDayAsAnchor_Excluded() { let planner = ScenarioBPlanner() let la = laStadium // Create Anaheim stadium (30 miles from LA) let anaheimStadium = makeStadium( name: "Angel Stadium", city: "Anaheim", state: "CA", latitude: 33.8003, longitude: -117.8827 ) // Must-see: LA Jan 5 7pm let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 19:00")) // Filler: Anaheim Jan 5 7pm (same time, different city - impossible) let anaheimGame = makeGame(stadiumId: anaheimStadium.id, date: date("2026-01-05 19:00")) let request = makeRequest( games: [laGame, anaheimGame], stadiums: [la.id: la, anaheimStadium.id: anaheimStadium], mustSeeGameIds: [laGame.id], startDate: date("2026-01-01 00:00"), endDate: date("2026-01-31 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { // All options must include LA game (anchor) for option in options { let allGameIds = option.stops.flatMap { $0.games } #expect(allGameIds.contains(laGame.id), "Anchor game must be present") // If both games in same option, they cannot be at same time if allGameIds.contains(anaheimGame.id) { Issue.record("Filler game at same time as anchor should be excluded") } } } else { Issue.record("Expected success") } } @Test("filler requiring backtracking is excluded or route reordered") func plan_FillerRequiringBacktrack_ExcludedOrReordered() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let sd = sdStadium // San Diego (south of LA) // Must-see: LA Jan 5 7pm, SF Jan 7 7pm (northbound route) let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 19:00")) let sfGame = makeGame(stadiumId: sf.id, date: date("2026-01-07 19:00")) // Filler: San Diego Jan 6 7pm (south of LA, requires backtrack from northbound route) let sdGame = makeGame(stadiumId: sd.id, date: date("2026-01-06 19:00")) let request = makeRequest( games: [laGame, sfGame, sdGame], stadiums: [la.id: la, sf.id: sf, sd.id: sd], mustSeeGameIds: [laGame.id, sfGame.id], startDate: date("2026-01-01 00:00"), endDate: date("2026-01-31 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { // Check if SD is included in any route for option in options { let cities = option.stops.map { $0.city } if cities.contains("San Diego") { // If SD included, route should be reordered: SD → LA → SF // OR it should be a separate option if cities.count == 3 { // Check it's not inefficient backtracking: LA → SF → SD or LA → SD → SF (backtrack) let cityOrder = cities.joined(separator: "→") #expect( cityOrder == "San Diego→Los Angeles→San Francisco", "If filler included, route should avoid backtracking" ) } } } } else { Issue.record("Expected success") } } @Test("multiple filler options only feasible one included") func plan_MultipleFillers_OnlyFeasibleIncluded() { let planner = ScenarioBPlanner() let la = laStadium let phoenix = phoenixStadium // Create Tucson (between LA and Phoenix) let tucsonStadium = makeStadium( name: "Tucson Stadium", city: "Tucson", state: "AZ", latitude: 32.2226, longitude: -110.9747 ) // Must-see: LA Jan 5 1pm, Phoenix Jan 7 7pm (eastbound) let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 13:00")) let phoenixGame = makeGame(stadiumId: phoenix.id, date: date("2026-01-07 19:00")) // Filler A: SF Jan 6 7pm (300mi north, wrong direction from LA→Phoenix) let sfGame = makeGame(stadiumId: sfStadium.id, date: date("2026-01-06 19:00")) // Filler B: Tucson Jan 6 7pm (100mi from Phoenix, on the way) let tucsonGame = makeGame(stadiumId: tucsonStadium.id, date: date("2026-01-06 19:00")) let request = makeRequest( games: [laGame, phoenixGame, sfGame, tucsonGame], stadiums: [la.id: la, phoenix.id: phoenix, sfStadium.id: sfStadium, tucsonStadium.id: tucsonStadium], mustSeeGameIds: [laGame.id, phoenixGame.id], startDate: date("2026-01-01 00:00"), endDate: date("2026-01-31 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { // Look for routes with 3 stops let threeStopOptions = options.filter { $0.stops.count == 3 } for option in threeStopOptions { let cities = option.stops.map { $0.city } // If Tucson included, SF should not be (wrong direction) if cities.contains("Tucson") { #expect(!cities.contains("San Francisco"), "Should not include SF when Tucson is better option") } // If SF included, Tucson should not be (SF is wrong direction) if cities.contains("San Francisco") { #expect(!cities.contains("Tucson"), "Should not include Tucson when SF chosen (though Tucson is better)") } } } else { Issue.record("Expected success") } } // MARK: - Impossible Geographic Combination Tests (Phase 09-02) @Test func plan_MustSeeGamesTooFarApart_Fails() { let planner = ScenarioBPlanner() // Create NY and LA stadiums (2800 miles, 42 hour drive) let nyStadium = makeStadium( name: "Yankee Stadium", city: "New York", state: "NY", latitude: 40.8296, longitude: -73.9262 ) let laStadium = self.laStadium // Must-see: LA Jan 5 7pm, NY Jan 6 7pm (24 hours available) let laGame = makeGame(stadiumId: laStadium.id, date: date("2026-01-05 19:00")) let nyGame = makeGame(stadiumId: nyStadium.id, date: date("2026-01-06 19:00")) let request = makeRequest( games: [laGame, nyGame], stadiums: [laStadium.id: laStadium, nyStadium.id: nyStadium], mustSeeGameIds: [laGame.id, nyGame.id], startDate: date("2026-01-01 00:00"), endDate: date("2026-01-31 23:59") ) let result = planner.plan(request: request) if case .failure(let failure) = result { // Expected - cannot drive 2800mi in 24hr #expect(failure.reason == .constraintsUnsatisfiable || failure.reason == .drivingExceedsLimit) } else { Issue.record("Expected failure - impossible to drive LA to NY in 24 hours") } } @Test("must-see games in reverse order geographically fail") func plan_MustSeeGamesReverseOrder_Fails() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium // Must-see: SF Jan 5, LA Jan 4 // Problem: SF game is chronologically after LA, but geographically north // This requires backtracking or violates date ordering let sfGame = makeGame(stadiumId: sf.id, date: date("2026-01-05 19:00")) let laGame = makeGame(stadiumId: la.id, date: date("2026-01-04 19:00")) let request = makeRequest( games: [sfGame, laGame], stadiums: [la.id: la, sf.id: sf], mustSeeGameIds: [sfGame.id, laGame.id], startDate: date("2026-01-01 00:00"), endDate: date("2026-01-31 23:59") ) let result = planner.plan(request: request) // This may succeed OR fail depending on routing logic // If it succeeds, verify route respects chronology (LA → SF) if case .success(let options) = result { for option in options { if option.stops.count == 2 { // Route should be chronological: LA (Jan 4) → SF (Jan 5) #expect(option.stops[0].city == "Los Angeles") #expect(option.stops[1].city == "San Francisco") } } } // Failure is also acceptable } @Test("three must-see games forming triangle routes or fails") func plan_ThreeMustSeeTriangle_RoutesOrFails() { let planner = ScenarioBPlanner() let la = laStadium let sf = sfStadium let sd = sdStadium // Triangle: LA Jan 5, SF Jan 6, SD Jan 7 // Inefficient: LA→SF→SD requires backtracking south // Better: SD→LA→SF (but violates chronology) let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 19:00")) let sfGame = makeGame(stadiumId: sf.id, date: date("2026-01-06 19:00")) let sdGame = makeGame(stadiumId: sd.id, date: date("2026-01-07 19:00")) let request = makeRequest( games: [laGame, sfGame, sdGame], stadiums: [la.id: la, sf.id: sf, sd.id: sd], mustSeeGameIds: [laGame.id, sfGame.id, sdGame.id], startDate: date("2026-01-01 00:00"), endDate: date("2026-01-31 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { // Should attempt route in chronological order: LA→SF→SD let threeStopOption = options.first { $0.stops.count == 3 } if let option = threeStopOption { // Verify chronological order is respected #expect(option.stops[0].city == "Los Angeles") #expect(option.stops[1].city == "San Francisco") #expect(option.stops[2].city == "San Diego") } } // Failure is also acceptable if deemed too inefficient } @Test("must-see games exceeding driving constraints fail") func plan_MustSeeExceedingDrivingLimit_Fails() { let planner = ScenarioBPlanner() let la = laStadium let phoenix = phoenixStadium // Must-see: LA Jan 5 1pm, Phoenix Jan 5 7pm (380 miles, 6hr drive) // Constraints: 1 driver, 4hr/day max let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 13:00")) let phoenixGame = makeGame(stadiumId: phoenix.id, date: date("2026-01-05 19:00")) // Create custom request with reduced driving limit var prefs = TripPreferences( startDate: date("2026-01-01 00:00"), endDate: date("2026-01-31 23:59"), tripDuration: nil, numberOfDrivers: 1 ) prefs.mustSeeGameIds = [laGame.id, phoenixGame.id] prefs.maxDrivingHoursPerDriver = 4.0 // Limit to 4 hours let request = PlanningRequest( preferences: prefs, availableGames: [laGame, phoenixGame], teams: [:], stadiums: [la.id: la, phoenix.id: phoenix] ) let result = planner.plan(request: request) if case .failure(let failure) = result { // Expected - 6hr drive exceeds 4hr limit #expect(failure.reason == .drivingExceedsLimit || failure.reason == .constraintsUnsatisfiable) } else { Issue.record("Expected failure - 6hr drive exceeds 4hr daily limit") } } @Test("feasible must-see combination succeeds (sanity)") func plan_FeasibleMustSeeCombination_Succeeds() { let planner = ScenarioBPlanner() let la = laStadium // Create Anaheim (30 miles from LA, easy drive) let anaheimStadium = makeStadium( name: "Angel Stadium", city: "Anaheim", state: "CA", latitude: 33.8003, longitude: -117.8827 ) // Must-see: LA Jan 5 7pm, Anaheim Jan 7 7pm (plenty of time) let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 19:00")) let anaheimGame = makeGame(stadiumId: anaheimStadium.id, date: date("2026-01-07 19:00")) let request = makeRequest( games: [laGame, anaheimGame], stadiums: [la.id: la, anaheimStadium.id: anaheimStadium], mustSeeGameIds: [laGame.id, anaheimGame.id], startDate: date("2026-01-01 00:00"), endDate: date("2026-01-31 23:59") ) let result = planner.plan(request: request) if case .success(let options) = result { #expect(!options.isEmpty, "Feasible must-see combination should succeed") // Verify both games appear in route for option in options { let allGameIds = option.stops.flatMap { $0.games } #expect(allGameIds.contains(laGame.id)) #expect(allGameIds.contains(anaheimGame.id)) } } else { Issue.record("Expected success for feasible must-see combination") } } }