// // TripPlanningEngineTests.swift // SportsTimeTests // // Phase 7: TripPlanningEngine Integration Tests // Main orchestrator — tests all scenarios together. // import Testing import CoreLocation @testable import SportsTime @Suite("TripPlanningEngine Tests", .serialized) struct TripPlanningEngineTests { // MARK: - Test Fixtures private let calendar = Calendar.current /// Creates a fresh engine for each test to avoid parallel execution issues private func makeEngine() -> TripPlanningEngine { TripPlanningEngine() } /// Creates a date with specific year/month/day/hour private func makeDate(year: Int = 2026, month: Int = 6, day: Int, hour: Int = 19) -> Date { var components = DateComponents() components.year = year components.month = month components.day = day components.hour = hour components.minute = 0 return calendar.date(from: components)! } /// Creates a stadium at a known location private func makeStadium( id: String = "stadium_test_\(UUID().uuidString)", city: String, lat: Double, lon: Double, sport: Sport = .mlb ) -> Stadium { Stadium( id: id, name: "\(city) Stadium", city: city, state: "ST", latitude: lat, longitude: lon, capacity: 40000, sport: sport ) } /// Creates a game at a stadium private func makeGame( id: String = "game_test_\(UUID().uuidString)", stadiumId: String, homeTeamId: String = "team_test_\(UUID().uuidString)", awayTeamId: String = "team_test_\(UUID().uuidString)", dateTime: Date, sport: Sport = .mlb ) -> Game { Game( id: id, homeTeamId: homeTeamId, awayTeamId: awayTeamId, stadiumId: stadiumId, dateTime: dateTime, sport: sport, season: "2026" ) } /// Creates a PlanningRequest for Scenario A (date range only) private func makeScenarioARequest( startDate: Date, endDate: Date, games: [Game], stadiums: [String: Stadium], numberOfDrivers: Int = 1, maxDrivingHoursPerDriver: Double = 8.0, allowRepeatCities: Bool = true ) -> PlanningRequest { let preferences = TripPreferences( planningMode: .dateRange, sports: [.mlb], startDate: startDate, endDate: endDate, leisureLevel: .moderate, numberOfDrivers: numberOfDrivers, maxDrivingHoursPerDriver: maxDrivingHoursPerDriver, allowRepeatCities: allowRepeatCities ) return PlanningRequest( preferences: preferences, availableGames: games, teams: [:], stadiums: stadiums ) } /// Creates a PlanningRequest for Scenario B (selected games) private func makeScenarioBRequest( mustSeeGameIds: Set, startDate: Date, endDate: Date, games: [Game], stadiums: [String: Stadium], numberOfDrivers: Int = 1, maxDrivingHoursPerDriver: Double = 8.0, allowRepeatCities: Bool = true ) -> PlanningRequest { let preferences = TripPreferences( planningMode: .gameFirst, sports: [.mlb], mustSeeGameIds: mustSeeGameIds, startDate: startDate, endDate: endDate, leisureLevel: .moderate, numberOfDrivers: numberOfDrivers, maxDrivingHoursPerDriver: maxDrivingHoursPerDriver, allowRepeatCities: allowRepeatCities ) return PlanningRequest( preferences: preferences, availableGames: games, teams: [:], stadiums: stadiums ) } /// Creates a PlanningRequest for Scenario C (start/end locations) private func makeScenarioCRequest( startLocation: LocationInput, endLocation: LocationInput, startDate: Date, endDate: Date, games: [Game], stadiums: [String: Stadium], numberOfDrivers: Int = 1, maxDrivingHoursPerDriver: Double = 8.0 ) -> PlanningRequest { let preferences = TripPreferences( planningMode: .locations, startLocation: startLocation, endLocation: endLocation, sports: [.mlb], startDate: startDate, endDate: endDate, leisureLevel: .moderate, numberOfDrivers: numberOfDrivers, maxDrivingHoursPerDriver: maxDrivingHoursPerDriver ) return PlanningRequest( preferences: preferences, availableGames: games, teams: [:], stadiums: stadiums ) } // MARK: - 7A: Scenario Routing @Test("7.1 - Engine delegates to Scenario A correctly") func test_engine_ScenarioA_DelegatesCorrectly() { // Setup: Date range only request (Scenario A) let chicagoId = "stadium_chicago_\(UUID().uuidString)" let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee] let game1 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19)) let game2 = makeGame(stadiumId: milwaukeeId, dateTime: makeDate(day: 7, hour: 19)) let request = makeScenarioARequest( startDate: makeDate(day: 4, hour: 0), endDate: makeDate(day: 10, hour: 23), games: [game1, game2], stadiums: stadiums ) // Verify this is classified as Scenario A let scenario = ScenarioPlannerFactory.classify(request) #expect(scenario == .scenarioA, "Should be classified as Scenario A") // Execute through engine let result = makeEngine().planItineraries(request: request) // Verify #expect(result.isSuccess, "Engine should successfully delegate to Scenario A planner") #expect(!result.options.isEmpty, "Should return itinerary options") } @Test("7.2 - Engine delegates to Scenario B correctly") func test_engine_ScenarioB_DelegatesCorrectly() { // Setup: Selected games request (Scenario B) let chicagoId = "stadium_chicago_\(UUID().uuidString)" let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee] let game1 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19)) let game2 = makeGame(stadiumId: milwaukeeId, dateTime: makeDate(day: 7, hour: 19)) // User selects specific games let request = makeScenarioBRequest( mustSeeGameIds: [game1.id, game2.id], startDate: makeDate(day: 4, hour: 0), endDate: makeDate(day: 10, hour: 23), games: [game1, game2], stadiums: stadiums ) // Verify this is classified as Scenario B let scenario = ScenarioPlannerFactory.classify(request) #expect(scenario == .scenarioB, "Should be classified as Scenario B when games are selected") // Execute through engine let result = makeEngine().planItineraries(request: request) // Verify #expect(result.isSuccess, "Engine should successfully delegate to Scenario B planner") if result.isSuccess { // All selected games should be in the routes for option in result.options { let gameIds = Set(option.stops.flatMap { $0.games }) #expect(gameIds.contains(game1.id), "Should contain first selected game") #expect(gameIds.contains(game2.id), "Should contain second selected game") } } } @Test("7.3 - Engine delegates to Scenario C correctly") func test_engine_ScenarioC_DelegatesCorrectly() { // Setup: Start/end locations request (Scenario C) let chicagoId = "stadium_chicago_\(UUID().uuidString)" let clevelandId = "stadium_cleveland_\(UUID().uuidString)" let detroitId = "stadium_detroit_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let cleveland = makeStadium(id: clevelandId, city: "Cleveland", lat: 41.4993, lon: -81.6944) let detroit = makeStadium(id: detroitId, city: "Detroit", lat: 42.3314, lon: -83.0458) let stadiums = [chicagoId: chicago, clevelandId: cleveland, detroitId: detroit] let game1 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19)) let game2 = makeGame(stadiumId: detroitId, dateTime: makeDate(day: 7, hour: 19)) let game3 = makeGame(stadiumId: clevelandId, dateTime: makeDate(day: 9, hour: 19)) let startLocation = LocationInput( name: "Chicago", coordinate: CLLocationCoordinate2D(latitude: 41.8781, longitude: -87.6298) ) let endLocation = LocationInput( name: "Cleveland", coordinate: CLLocationCoordinate2D(latitude: 41.4993, longitude: -81.6944) ) let request = makeScenarioCRequest( startLocation: startLocation, endLocation: endLocation, startDate: makeDate(day: 4, hour: 0), endDate: makeDate(day: 12, hour: 23), games: [game1, game2, game3], stadiums: stadiums ) // Verify this is classified as Scenario C let scenario = ScenarioPlannerFactory.classify(request) #expect(scenario == .scenarioC, "Should be classified as Scenario C when locations are specified") // Execute through engine let result = makeEngine().planItineraries(request: request) // Scenario C may succeed or fail depending on directional filtering // The key test is that it correctly identifies and delegates to Scenario C if result.isSuccess { #expect(!result.options.isEmpty, "If success, should have options") } // Failure is also valid (e.g., no directional routes found) } @Test("7.4 - Scenarios are mutually exclusive") func test_engine_ScenariosAreMutuallyExclusive() { // Setup: Create requests that could theoretically match multiple scenarios let chicagoId = "stadium_chicago_\(UUID().uuidString)" let clevelandId = "stadium_cleveland_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let cleveland = makeStadium(id: clevelandId, city: "Cleveland", lat: 41.4993, lon: -81.6944) let stadiums = [chicagoId: chicago, clevelandId: cleveland] let game1 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19)) let game2 = makeGame(stadiumId: clevelandId, dateTime: makeDate(day: 7, hour: 19)) // Request with BOTH selected games AND start/end locations // According to priority: Scenario B (selected games) takes precedence let preferences = TripPreferences( planningMode: .locations, startLocation: LocationInput( name: "Chicago", coordinate: CLLocationCoordinate2D(latitude: 41.8781, longitude: -87.6298) ), endLocation: LocationInput( name: "Cleveland", coordinate: CLLocationCoordinate2D(latitude: 41.4993, longitude: -81.6944) ), sports: [.mlb], mustSeeGameIds: [game1.id], // Has selected games! startDate: makeDate(day: 4, hour: 0), endDate: makeDate(day: 10, hour: 23) ) let request = PlanningRequest( preferences: preferences, availableGames: [game1, game2], teams: [:], stadiums: stadiums ) // Verify: Selected games (Scenario B) takes precedence over locations (Scenario C) let scenario = ScenarioPlannerFactory.classify(request) #expect(scenario == .scenarioB, "Scenario B should take precedence when games are selected") // Scenario A should only be selected when no games selected AND no locations let scenarioARequest = makeScenarioARequest( startDate: makeDate(day: 4, hour: 0), endDate: makeDate(day: 10, hour: 23), games: [game1, game2], stadiums: stadiums ) let scenarioA = ScenarioPlannerFactory.classify(scenarioARequest) #expect(scenarioA == .scenarioA, "Scenario A is default when no games/locations specified") } // MARK: - 7B: Result Structure @Test("7.5 - Result contains travel segments") func test_engine_Result_ContainsTravelSegments() { // Setup: Multi-city trip that requires travel let chicagoId = "stadium_chicago_\(UUID().uuidString)" let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" let detroitId = "stadium_detroit_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) let detroit = makeStadium(id: detroitId, city: "Detroit", lat: 42.3314, lon: -83.0458) let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee, detroitId: detroit] let game1 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19)) let game2 = makeGame(stadiumId: milwaukeeId, dateTime: makeDate(day: 7, hour: 19)) let game3 = makeGame(stadiumId: detroitId, dateTime: makeDate(day: 9, hour: 19)) let request = makeScenarioARequest( startDate: makeDate(day: 4, hour: 0), endDate: makeDate(day: 10, hour: 23), games: [game1, game2, game3], stadiums: stadiums ) // Execute let result = makeEngine().planItineraries(request: request) // Verify #expect(result.isSuccess, "Should succeed with valid multi-city request") for option in result.options { if option.stops.count > 1 { // Travel segments should exist between stops // INVARIANT: travelSegments.count == stops.count - 1 #expect(option.travelSegments.count == option.stops.count - 1, "Should have N-1 travel segments for N stops") // Each segment should have valid data for segment in option.travelSegments { #expect(segment.distanceMeters > 0, "Segment should have positive distance") #expect(segment.durationSeconds > 0, "Segment should have positive duration") } } } } @Test("7.6 - Result contains itinerary days") func test_engine_Result_ContainsItineraryDays() { // Setup: Multi-day trip let chicagoId = "stadium_chicago_\(UUID().uuidString)" let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee] let game1 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19)) let game2 = makeGame(stadiumId: milwaukeeId, dateTime: makeDate(day: 8, hour: 19)) let request = makeScenarioARequest( startDate: makeDate(day: 4, hour: 0), endDate: makeDate(day: 10, hour: 23), games: [game1, game2], stadiums: stadiums ) // Execute let result = makeEngine().planItineraries(request: request) // Verify #expect(result.isSuccess, "Should succeed with valid request") for option in result.options { // Each stop represents a day/location #expect(!option.stops.isEmpty, "Should have at least one stop") // Stops should have arrival/departure dates for stop in option.stops { #expect(stop.arrivalDate <= stop.departureDate, "Arrival should be before or equal to departure") } // Can generate timeline let timeline = option.generateTimeline() #expect(!timeline.isEmpty, "Should generate non-empty timeline") // Timeline should have stops let stopItems = timeline.filter { $0.isStop } #expect(stopItems.count == option.stops.count, "Timeline should contain all stops") } } @Test("7.7 - Result includes warnings when applicable") func test_engine_Result_IncludesWarnings_WhenApplicable() { // Setup: Request that would normally violate repeat cities // but allowRepeatCities=true so it should succeed without warnings let chicagoId = "stadium_chicago_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [chicagoId: chicago] // Two games in the same city on different days let game1 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19)) let game2 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 7, hour: 19)) // Test with allowRepeatCities = true (should succeed) let allowRequest = makeScenarioARequest( startDate: makeDate(day: 4, hour: 0), endDate: makeDate(day: 10, hour: 23), games: [game1, game2], stadiums: stadiums, allowRepeatCities: true ) let allowResult = makeEngine().planItineraries(request: allowRequest) #expect(allowResult.isSuccess, "Should succeed when repeat cities allowed") // Test with allowRepeatCities = false (may fail with repeat city violation) let disallowRequest = makeScenarioARequest( startDate: makeDate(day: 4, hour: 0), endDate: makeDate(day: 10, hour: 23), games: [game1, game2], stadiums: stadiums, allowRepeatCities: false ) let disallowResult = makeEngine().planItineraries(request: disallowRequest) // When repeat cities not allowed and only option is same city, // should fail with repeatCityViolation if !disallowResult.isSuccess { if case .repeatCityViolation = disallowResult.failure?.reason { // Expected - verify the violating cities are listed if case .repeatCityViolation(let cities) = disallowResult.failure?.reason { #expect(cities.contains("Chicago"), "Should identify Chicago as the repeat city") } } } } // MARK: - 7C: Constraint Application @Test("7.8 - Number of drivers affects max daily driving") func test_engine_NumberOfDrivers_AffectsMaxDailyDriving() { // Setup: Long distance trip that requires significant driving // NYC to Chicago is ~790 miles (~13 hours of driving) let nycId = "stadium_nyc_\(UUID().uuidString)" let chicagoId = "stadium_chicago_\(UUID().uuidString)" let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352) let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [nycId: nyc, chicagoId: chicago] // Games on consecutive days - tight schedule let game1 = makeGame(stadiumId: nycId, dateTime: makeDate(day: 5, hour: 14)) let game2 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 6, hour: 20)) // With 1 driver (8 hours/day max), this should be very difficult let singleDriverRequest = makeScenarioARequest( startDate: makeDate(day: 4, hour: 0), endDate: makeDate(day: 8, hour: 23), games: [game1, game2], stadiums: stadiums, numberOfDrivers: 1, maxDrivingHoursPerDriver: 8.0 ) let singleDriverResult = makeEngine().planItineraries(request: singleDriverRequest) // With 2 drivers (16 hours/day max), this should be more feasible let twoDriverRequest = makeScenarioARequest( startDate: makeDate(day: 4, hour: 0), endDate: makeDate(day: 8, hour: 23), games: [game1, game2], stadiums: stadiums, numberOfDrivers: 2, maxDrivingHoursPerDriver: 8.0 ) let twoDriverResult = makeEngine().planItineraries(request: twoDriverRequest) // The driving constraints are calculated as: numberOfDrivers * maxHoursPerDriver let singleDriverConstraints = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 8.0) let twoDriverConstraints = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0) #expect(singleDriverConstraints.maxDailyDrivingHours == 8.0, "Single driver should have 8 hours max daily") #expect(twoDriverConstraints.maxDailyDrivingHours == 16.0, "Two drivers should have 16 hours max daily") // Two drivers should have more routing flexibility // (may or may not produce different results depending on route feasibility) if singleDriverResult.isSuccess && twoDriverResult.isSuccess { // Both succeeded - that's fine } else if !singleDriverResult.isSuccess && twoDriverResult.isSuccess { // Two drivers enabled a route that single driver couldn't - expected } // Either outcome demonstrates the constraint is being applied } @Test("7.9 - Max driving per day is respected") func test_engine_MaxDrivingPerDay_Respected() { // Test that DrivingConstraints correctly calculates max daily driving hours // based on number of drivers and hours per driver // Single driver: 1 × 8 = 8 hours max daily let singleDriver = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 8.0) #expect(singleDriver.maxDailyDrivingHours == 8.0, "Single driver should have 8 hours max daily") // Two drivers: 2 × 8 = 16 hours max daily let twoDrivers = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0) #expect(twoDrivers.maxDailyDrivingHours == 16.0, "Two drivers should have 16 hours max daily") // Three drivers: 3 × 8 = 24 hours max daily let threeDrivers = DrivingConstraints(numberOfDrivers: 3, maxHoursPerDriverPerDay: 8.0) #expect(threeDrivers.maxDailyDrivingHours == 24.0, "Three drivers should have 24 hours max daily") // Custom hours: 2 × 6 = 12 hours max daily let customHours = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 6.0) #expect(customHours.maxDailyDrivingHours == 12.0, "Two drivers with 6 hours each should have 12 hours max daily") // Verify default constraints let defaultConstraints = DrivingConstraints.default #expect(defaultConstraints.numberOfDrivers == 1, "Default should have 1 driver") #expect(defaultConstraints.maxHoursPerDriverPerDay == 8.0, "Default should have 8 hours per driver") #expect(defaultConstraints.maxDailyDrivingHours == 8.0, "Default max daily should be 8 hours") // Verify constraints from preferences are propagated correctly // (The actual engine planning is tested in other tests) } @Test("7.10 - AllowRepeatCities is propagated to DAG") func test_engine_AllowRepeatCities_PropagatedToDAG() { // Setup: Games that would require visiting the same city twice let chicagoId = "stadium_chicago_\(UUID().uuidString)" let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee] // Chicago → Milwaukee → Chicago pattern let game1 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 5, hour: 19)) let game2 = makeGame(stadiumId: milwaukeeId, dateTime: makeDate(day: 7, hour: 19)) let game3 = makeGame(stadiumId: chicagoId, dateTime: makeDate(day: 9, hour: 19)) // Test with allowRepeatCities = true let allowRequest = makeScenarioARequest( startDate: makeDate(day: 4, hour: 0), endDate: makeDate(day: 12, hour: 23), games: [game1, game2, game3], stadiums: stadiums, allowRepeatCities: true ) let allowResult = makeEngine().planItineraries(request: allowRequest) // Test with allowRepeatCities = false let disallowRequest = makeScenarioARequest( startDate: makeDate(day: 4, hour: 0), endDate: makeDate(day: 12, hour: 23), games: [game1, game2, game3], stadiums: stadiums, allowRepeatCities: false ) let disallowResult = makeEngine().planItineraries(request: disallowRequest) // With allowRepeatCities = true, should be able to include all 3 games if allowResult.isSuccess { let hasThreeGameOption = allowResult.options.contains { $0.totalGames == 3 } // May or may not have 3-game option depending on route feasibility // but the option should be available } // With allowRepeatCities = false: // - Either routes with repeat cities are filtered out // - Or if no other option, may fail with repeatCityViolation if disallowResult.isSuccess { // Verify no routes have the same city appearing multiple times for option in disallowResult.options { let cities = option.stops.map { $0.city } let uniqueCities = Set(cities) // Note: Same city can appear if it's the start/end points // The constraint is about not revisiting cities mid-trip } } else if case .repeatCityViolation = disallowResult.failure?.reason { // Expected when the only valid routes require repeat cities } } // MARK: - 7D: Error Handling @Test("7.11 - Impossible constraints returns no result or excludes unreachable anchors") func test_engine_ImpossibleConstraints_ReturnsNoResult() { // Setup: Create an impossible constraint scenario // Games at the same time on same day in cities far apart (can't make both) let nycId = "stadium_nyc_\(UUID().uuidString)" let laId = "stadium_la_\(UUID().uuidString)" // NYC to LA is ~2,800 miles - impossible to drive same day let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352) let la = makeStadium(id: laId, city: "Los Angeles", lat: 34.0522, lon: -118.2437) let stadiums = [nycId: nyc, laId: la] // Games at exact same time on same day - impossible to attend both let game1 = makeGame(stadiumId: nycId, dateTime: makeDate(day: 5, hour: 19)) let game2 = makeGame(stadiumId: laId, dateTime: makeDate(day: 5, hour: 19)) // Request that requires BOTH games (Scenario B with anchors) let request = makeScenarioBRequest( mustSeeGameIds: [game1.id, game2.id], startDate: makeDate(day: 4, hour: 0), endDate: makeDate(day: 6, hour: 23), games: [game1, game2], stadiums: stadiums ) // Execute let result = makeEngine().planItineraries(request: request) // Two valid behaviors for impossible constraints: // 1. Fail with an error (constraintsUnsatisfiable or noValidRoutes) // 2. Succeed but no route contains BOTH anchor games // // The key assertion: no valid route can contain BOTH games if result.isSuccess { // If success, verify no route contains both games for option in result.options { let gameIds = Set(option.stops.flatMap { $0.games }) let hasBoth = gameIds.contains(game1.id) && gameIds.contains(game2.id) #expect(!hasBoth, "No route should contain both games at the same time in distant cities") } } else { // Failure is the expected primary behavior if let failure = result.failure { // Valid failure reasons let validReasons: [PlanningFailure.FailureReason] = [ .constraintsUnsatisfiable, .noValidRoutes ] let reasonIsValid = validReasons.contains { $0 == failure.reason } #expect(reasonIsValid, "Should have appropriate failure reason: \(failure.reason)") } } } @Test("7.12 - Empty input returns error") func test_engine_EmptyInput_ThrowsError() { // Setup: Request with no games let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] let request = makeScenarioARequest( startDate: makeDate(day: 4, hour: 0), endDate: makeDate(day: 10, hour: 23), games: [], // No games! stadiums: stadiums ) // Execute let result = makeEngine().planItineraries(request: request) // Verify: Should fail with noGamesInRange #expect(!result.isSuccess, "Should fail with empty game list") #expect(result.failure?.reason == .noGamesInRange, "Should return noGamesInRange for empty input") } }