// // ScenarioDPlannerTests.swift // SportsTimeTests // // Phase 5: ScenarioDPlanner Tests // Scenario D: User selects a team to follow, planner builds route from their schedule. // import Testing import CoreLocation @testable import SportsTime @Suite("ScenarioDPlanner Tests", .serialized) struct ScenarioDPlannerTests { // MARK: - Test Fixtures private let calendar = Calendar.current private let planner = ScenarioDPlanner() /// 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 team private func makeTeam( id: String = "team_test_\(UUID().uuidString)", name: String, stadiumId: String, sport: Sport = .mlb ) -> Team { Team( id: id, name: name, abbreviation: String(name.prefix(3).uppercased()), sport: sport, city: name, stadiumId: stadiumId, logoURL: nil, primaryColor: "#FF0000", secondaryColor: "#FFFFFF" ) } /// Creates a game at a stadium private func makeGame( id: String = "game_test_\(UUID().uuidString)", stadiumId: String, homeTeamId: String, awayTeamId: String, 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 D (follow team mode) private func makePlanningRequest( startDate: Date, endDate: Date, followTeamId: String?, allGames: [Game], stadiums: [String: Stadium], teams: [String: Team] = [:], selectedRegions: Set = [], allowRepeatCities: Bool = true, useHomeLocation: Bool = false, startLocation: LocationInput? = nil, numberOfDrivers: Int = 1, maxDrivingHoursPerDriver: Double = 8.0 ) -> PlanningRequest { let preferences = TripPreferences( planningMode: .followTeam, startLocation: startLocation, sports: [.mlb], startDate: startDate, endDate: endDate, leisureLevel: .moderate, numberOfDrivers: numberOfDrivers, maxDrivingHoursPerDriver: maxDrivingHoursPerDriver, allowRepeatCities: allowRepeatCities, selectedRegions: selectedRegions, followTeamId: followTeamId, useHomeLocation: useHomeLocation ) return PlanningRequest( preferences: preferences, availableGames: allGames, teams: teams, stadiums: stadiums ) } // MARK: - D.1: Valid Inputs @Test("D.1.1 - Single team with home games returns trip with those games") func test_followTeam_HomeGames_ReturnsTrip() { // Setup: Team with 2 home games let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] let teamId = "team_test_\(UUID().uuidString)" let opponentId = "team_opponent_\(UUID().uuidString)" let team = makeTeam(id: teamId, name: "Chicago Cubs", stadiumId: stadiumId) let game1 = makeGame( stadiumId: stadiumId, homeTeamId: teamId, awayTeamId: opponentId, dateTime: makeDate(day: 5, hour: 19) ) let game2 = makeGame( stadiumId: stadiumId, homeTeamId: teamId, awayTeamId: opponentId, dateTime: makeDate(day: 7, hour: 19) ) let request = makePlanningRequest( startDate: makeDate(day: 1, hour: 0), endDate: makeDate(day: 10, hour: 23), followTeamId: teamId, allGames: [game1, game2], stadiums: stadiums, teams: [teamId: team] ) // Execute let result = planner.plan(request: request) // Verify #expect(result.isSuccess, "Should succeed with team home games") #expect(!result.options.isEmpty, "Should return at least one option") if let firstOption = result.options.first { #expect(firstOption.totalGames >= 2, "Should include both home games") let cities = firstOption.stops.map { $0.city } #expect(cities.contains("Chicago"), "Should visit team's home city") } } @Test("D.1.2 - Team with away games includes those games") func test_followTeam_AwayGames_IncludesAwayGames() { // Setup: Team with one home game and one away game (2 cities for simpler route) let homeStadiumId = "stadium_home_\(UUID().uuidString)" let awayStadiumId = "stadium_away_\(UUID().uuidString)" let homeStadium = makeStadium(id: homeStadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let awayStadium = makeStadium(id: awayStadiumId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) let stadiums = [ homeStadiumId: homeStadium, awayStadiumId: awayStadium ] let teamId = "team_test_\(UUID().uuidString)" let opponentId = "team_opponent_\(UUID().uuidString)" // Home game let homeGame = makeGame( stadiumId: homeStadiumId, homeTeamId: teamId, awayTeamId: opponentId, dateTime: makeDate(day: 5, hour: 19) ) // Away game (team is awayTeamId) let awayGame = makeGame( stadiumId: awayStadiumId, homeTeamId: opponentId, awayTeamId: teamId, dateTime: makeDate(day: 8, hour: 19) ) let request = makePlanningRequest( startDate: makeDate(day: 1, hour: 0), endDate: makeDate(day: 15, hour: 23), followTeamId: teamId, allGames: [homeGame, awayGame], stadiums: stadiums ) // Execute let result = planner.plan(request: request) // Verify #expect(result.isSuccess, "Should succeed with home and away games") #expect(!result.options.isEmpty, "Should return at least one option") if let firstOption = result.options.first { // Should include both games #expect(firstOption.totalGames >= 2, "Should include both team games (home and away)") let cities = firstOption.stops.map { $0.city } #expect(cities.contains("Chicago"), "Should visit home city") #expect(cities.contains("Milwaukee"), "Should visit away city") } } @Test("D.1.3 - Team games filtered by selected regions") func test_followTeam_RegionFilter_FiltersGames() { // Setup: Team with games in multiple regions let eastStadiumId = "stadium_east_\(UUID().uuidString)" let centralStadiumId = "stadium_central_\(UUID().uuidString)" // East region (> -85 longitude) let eastStadium = makeStadium(id: eastStadiumId, city: "New York", lat: 40.7128, lon: -73.9352) // Central region (-110 to -85 longitude) let centralStadium = makeStadium(id: centralStadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [eastStadiumId: eastStadium, centralStadiumId: centralStadium] let teamId = "team_test_\(UUID().uuidString)" let opponentId = "team_opponent_\(UUID().uuidString)" let eastGame = makeGame( stadiumId: eastStadiumId, homeTeamId: opponentId, awayTeamId: teamId, dateTime: makeDate(day: 5, hour: 19) ) let centralGame = makeGame( stadiumId: centralStadiumId, homeTeamId: teamId, awayTeamId: opponentId, dateTime: makeDate(day: 7, hour: 19) ) // Only select East region let request = makePlanningRequest( startDate: makeDate(day: 1, hour: 0), endDate: makeDate(day: 15, hour: 23), followTeamId: teamId, allGames: [eastGame, centralGame], stadiums: stadiums, selectedRegions: [.east] ) // Execute let result = planner.plan(request: request) // Verify #expect(result.isSuccess, "Should succeed with regional filter") #expect(!result.options.isEmpty, "Should return at least one option") if let firstOption = result.options.first { let cities = firstOption.stops.map { $0.city } #expect(cities.contains("New York"), "Should include East region game") #expect(!cities.contains("Chicago"), "Should exclude Central region game") } } // MARK: - D.2: Edge Cases @Test("D.2.1 - No team selected returns missingTeamSelection failure") func test_followTeam_NoTeamSelected_ReturnsMissingTeamSelection() { // Setup: No team ID let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] let game = makeGame( stadiumId: stadiumId, homeTeamId: "team_test_\(UUID().uuidString)", awayTeamId: "team_opponent_\(UUID().uuidString)", dateTime: makeDate(day: 5, hour: 19) ) let request = makePlanningRequest( startDate: makeDate(day: 1, hour: 0), endDate: makeDate(day: 10, hour: 23), followTeamId: nil, // No team selected allGames: [game], stadiums: stadiums ) // Execute let result = planner.plan(request: request) // Verify #expect(!result.isSuccess, "Should fail when no team selected") #expect(result.failure?.reason == .missingTeamSelection, "Should return missingTeamSelection error") } @Test("D.2.2 - Team with no games in date range returns noGamesInRange failure") func test_followTeam_NoGamesInRange_ReturnsNoGamesFailure() { // Setup: Team's games are outside date range let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] let teamId = "team_test_\(UUID().uuidString)" // Game is in July, but we search June let game = makeGame( stadiumId: stadiumId, homeTeamId: teamId, awayTeamId: "team_opponent_\(UUID().uuidString)", dateTime: makeDate(month: 7, day: 15, hour: 19) ) let request = makePlanningRequest( startDate: makeDate(month: 6, day: 1, hour: 0), endDate: makeDate(month: 6, day: 30, hour: 23), followTeamId: teamId, allGames: [game], stadiums: stadiums ) // Execute let result = planner.plan(request: request) // Verify #expect(!result.isSuccess, "Should fail when no games in date range") #expect(result.failure?.reason == .noGamesInRange, "Should return noGamesInRange error") } @Test("D.2.3 - Team not involved in any games returns noGamesInRange failure") func test_followTeam_TeamNotInGames_ReturnsNoGamesFailure() { // Setup: Games exist but team isn't playing let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] let teamId = "team_test_\(UUID().uuidString)" let otherTeam1 = "team_other1_\(UUID().uuidString)" let otherTeam2 = "team_other2_\(UUID().uuidString)" // Game between other teams let game = makeGame( stadiumId: stadiumId, homeTeamId: otherTeam1, awayTeamId: otherTeam2, dateTime: makeDate(day: 5, hour: 19) ) let request = makePlanningRequest( startDate: makeDate(day: 1, hour: 0), endDate: makeDate(day: 10, hour: 23), followTeamId: teamId, allGames: [game], stadiums: stadiums ) // Execute let result = planner.plan(request: request) // Verify #expect(!result.isSuccess, "Should fail when team has no games") #expect(result.failure?.reason == .noGamesInRange, "Should return noGamesInRange error") } @Test("D.2.4 - Repeat city filter removes duplicate city visits") func test_followTeam_RepeatCityFilter_RemovesDuplicates() { // Setup: Team has multiple games at same stadium let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] let teamId = "team_test_\(UUID().uuidString)" let opponentId = "team_opponent_\(UUID().uuidString)" let game1 = makeGame( stadiumId: stadiumId, homeTeamId: teamId, awayTeamId: opponentId, dateTime: makeDate(day: 5, hour: 19) ) let game2 = makeGame( stadiumId: stadiumId, homeTeamId: teamId, awayTeamId: opponentId, dateTime: makeDate(day: 7, hour: 19) ) let game3 = makeGame( stadiumId: stadiumId, homeTeamId: teamId, awayTeamId: opponentId, dateTime: makeDate(day: 9, hour: 19) ) let request = makePlanningRequest( startDate: makeDate(day: 1, hour: 0), endDate: makeDate(day: 15, hour: 23), followTeamId: teamId, allGames: [game1, game2, game3], stadiums: stadiums, allowRepeatCities: false // Don't allow repeat cities ) // Execute let result = planner.plan(request: request) // Verify #expect(result.isSuccess, "Should succeed with repeat city filter") #expect(!result.options.isEmpty, "Should return at least one option") if let firstOption = result.options.first { // With allowRepeatCities=false, should only have 1 game // (the first game in Chicago) #expect(firstOption.totalGames == 1, "Should only include first game per city when repeat cities not allowed") } } @Test("D.2.5 - Missing date range returns missingDateRange failure") func test_followTeam_MissingDateRange_ReturnsMissingDateRangeFailure() { // Setup: Invalid date range (end before start) let stadiumId = "stadium_test_\(UUID().uuidString)" let stadium = makeStadium(id: stadiumId, city: "Chicago", lat: 41.8781, lon: -87.6298) let stadiums = [stadiumId: stadium] let teamId = "team_test_\(UUID().uuidString)" let game = makeGame( stadiumId: stadiumId, homeTeamId: teamId, awayTeamId: "team_opponent_\(UUID().uuidString)", dateTime: makeDate(day: 5, hour: 19) ) // End date before start date makes dateRange nil let request = makePlanningRequest( startDate: makeDate(day: 15, hour: 0), endDate: makeDate(day: 1, hour: 23), // Before start followTeamId: teamId, allGames: [game], stadiums: stadiums ) // Execute let result = planner.plan(request: request) // Verify #expect(!result.isSuccess, "Should fail with invalid date range") #expect(result.failure?.reason == .missingDateRange, "Should return missingDateRange error") } // MARK: - D.3: Route Verification @Test("D.3.1 - Route connects team games chronologically") func test_followTeam_RouteIsChronological() { // Setup: Team with games in 2 nearby cities chronologically let chicagoId = "stadium_chicago_\(UUID().uuidString)" let milwaukeeId = "stadium_milwaukee_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let milwaukee = makeStadium(id: milwaukeeId, city: "Milwaukee", lat: 43.0389, lon: -87.9065) let stadiums = [chicagoId: chicago, milwaukeeId: milwaukee] let teamId = "team_test_\(UUID().uuidString)" let opponentId = "team_opponent_\(UUID().uuidString)" // Games in chronological order: Chicago → Milwaukee let game1 = makeGame( stadiumId: chicagoId, homeTeamId: teamId, awayTeamId: opponentId, dateTime: makeDate(day: 5, hour: 19) ) let game2 = makeGame( stadiumId: milwaukeeId, homeTeamId: opponentId, awayTeamId: teamId, dateTime: makeDate(day: 8, hour: 19) ) let request = makePlanningRequest( startDate: makeDate(day: 1, hour: 0), endDate: makeDate(day: 15, hour: 23), followTeamId: teamId, allGames: [game1, game2], stadiums: stadiums ) // Execute let result = planner.plan(request: request) // Verify #expect(result.isSuccess, "Should succeed with team games") #expect(!result.options.isEmpty, "Should return at least one option") if let firstOption = result.options.first { #expect(firstOption.totalGames >= 2, "Should include both team games") // Verify stops are in chronological order let stopDates = firstOption.stops.map { $0.arrivalDate } let sortedDates = stopDates.sorted() #expect(stopDates == sortedDates, "Stops should be in chronological order") } } @Test("D.3.2 - Travel segments connect stops correctly") func test_followTeam_TravelSegmentsConnectStops() { // Setup: Team with 2 games in different cities let nycId = "stadium_nyc_\(UUID().uuidString)" let bostonId = "stadium_boston_\(UUID().uuidString)" let nyc = makeStadium(id: nycId, city: "New York", lat: 40.7128, lon: -73.9352) let boston = makeStadium(id: bostonId, city: "Boston", lat: 42.3601, lon: -71.0589) let stadiums = [nycId: nyc, bostonId: boston] let teamId = "team_test_\(UUID().uuidString)" let opponentId = "team_opponent_\(UUID().uuidString)" let game1 = makeGame( stadiumId: nycId, homeTeamId: teamId, awayTeamId: opponentId, dateTime: makeDate(day: 5, hour: 19) ) let game2 = makeGame( stadiumId: bostonId, homeTeamId: opponentId, awayTeamId: teamId, dateTime: makeDate(day: 8, hour: 19) ) let request = makePlanningRequest( startDate: makeDate(day: 1, hour: 0), endDate: makeDate(day: 15, hour: 23), followTeamId: teamId, allGames: [game1, game2], stadiums: stadiums ) // Execute let result = planner.plan(request: request) // Verify #expect(result.isSuccess, "Should succeed with team games") #expect(!result.options.isEmpty, "Should return at least one option") if let firstOption = result.options.first { #expect(firstOption.stops.count >= 2, "Should have at least 2 stops") // Should have travel segment between stops if firstOption.stops.count > 1 { #expect(firstOption.travelSegments.count == firstOption.stops.count - 1, "Should have travel segments connecting stops") // Verify travel segment has reasonable distance if let segment = firstOption.travelSegments.first { #expect(segment.distanceMiles > 0, "Travel segment should have distance") #expect(segment.durationHours > 0, "Travel segment should have duration") } } } } // MARK: - D.4: Multi-City Cross-Country Routes @Test("D.4.1 - Three-city route with adequate driving time succeeds (Astros scenario)") func test_followTeam_ThreeCityRoute_WithAdequateTime_Succeeds() { // Setup: Simulates Houston → Chicago → Anaheim (Astros July 20-29 scenario) // Houston to Chicago: ~1000 miles, Chicago to Anaheim: ~2000 miles // With 4+ days between each leg, both should be feasible let houstonId = "stadium_houston_\(UUID().uuidString)" let chicagoId = "stadium_chicago_\(UUID().uuidString)" let anaheimId = "stadium_anaheim_\(UUID().uuidString)" let houston = makeStadium(id: houstonId, city: "Houston", lat: 29.7604, lon: -95.3698) let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let anaheim = makeStadium(id: anaheimId, city: "Anaheim", lat: 33.8003, lon: -117.8827) let stadiums = [houstonId: houston, chicagoId: chicago, anaheimId: anaheim] let teamId = "team_test_\(UUID().uuidString)" let opponent1 = "team_opponent1_\(UUID().uuidString)" let opponent2 = "team_opponent2_\(UUID().uuidString)" let opponent3 = "team_opponent3_\(UUID().uuidString)" // Houston home games: July 20-22 let houstonGame = makeGame( stadiumId: houstonId, homeTeamId: teamId, awayTeamId: opponent1, dateTime: makeDate(month: 7, day: 20, hour: 19) ) // Chicago away games: July 24-26 (4 days after Houston) let chicagoGame = makeGame( stadiumId: chicagoId, homeTeamId: opponent2, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 24, hour: 19) ) // Anaheim away games: July 29 (5 days after Chicago) let anaheimGame = makeGame( stadiumId: anaheimId, homeTeamId: opponent3, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 29, hour: 19) ) let request = makePlanningRequest( startDate: makeDate(month: 7, day: 18, hour: 0), endDate: makeDate(month: 7, day: 31, hour: 23), followTeamId: teamId, allGames: [houstonGame, chicagoGame, anaheimGame], stadiums: stadiums, allowRepeatCities: false ) // Execute let result = planner.plan(request: request) // Verify #expect(result.isSuccess, "Should succeed with 3-city cross-country route") #expect(!result.options.isEmpty, "Should return at least one option") // Find the 3-city route let threeCityOption = result.options.first { option in option.stops.count == 3 } #expect(threeCityOption != nil, "Should include a 3-city route option") if let option = threeCityOption { let cities = option.stops.map { $0.city } #expect(cities.contains("Houston"), "Route should include Houston") #expect(cities.contains("Chicago"), "Route should include Chicago") #expect(cities.contains("Anaheim"), "Route should include Anaheim") // Verify travel segments exist #expect(option.travelSegments.count == 2, "Should have 2 travel segments for 3 stops") } } @Test("D.4.2 - Three-city route with insufficient driving time fails to include all cities") func test_followTeam_ThreeCityRoute_InsufficientTime_ExcludesUnreachableCities() { // Setup: Same cities but games too close together // Chicago to Anaheim needs ~37 hours driving, but only 1 day between games let houstonId = "stadium_houston_\(UUID().uuidString)" let chicagoId = "stadium_chicago_\(UUID().uuidString)" let anaheimId = "stadium_anaheim_\(UUID().uuidString)" let houston = makeStadium(id: houstonId, city: "Houston", lat: 29.7604, lon: -95.3698) let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let anaheim = makeStadium(id: anaheimId, city: "Anaheim", lat: 33.8003, lon: -117.8827) let stadiums = [houstonId: houston, chicagoId: chicago, anaheimId: anaheim] let teamId = "team_test_\(UUID().uuidString)" let opponent1 = "team_opponent1_\(UUID().uuidString)" let opponent2 = "team_opponent2_\(UUID().uuidString)" let opponent3 = "team_opponent3_\(UUID().uuidString)" // Houston: July 20 let houstonGame = makeGame( stadiumId: houstonId, homeTeamId: teamId, awayTeamId: opponent1, dateTime: makeDate(month: 7, day: 20, hour: 19) ) // Chicago: July 21 (only 1 day after Houston - ~20 hrs driving, needs 16 hrs max) // This is borderline but might work let chicagoGame = makeGame( stadiumId: chicagoId, homeTeamId: opponent2, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 22, hour: 19) // 2 days = 16 hrs max, needs ~20 hrs ) // Anaheim: July 23 (only 1 day after Chicago - ~37 hrs driving, needs 8 hrs max) // This should definitely fail let anaheimGame = makeGame( stadiumId: anaheimId, homeTeamId: opponent3, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 23, hour: 19) // 1 day after Chicago = impossible ) let request = makePlanningRequest( startDate: makeDate(month: 7, day: 18, hour: 0), endDate: makeDate(month: 7, day: 25, hour: 23), followTeamId: teamId, allGames: [houstonGame, chicagoGame, anaheimGame], stadiums: stadiums, allowRepeatCities: false ) // Execute let result = planner.plan(request: request) // Verify - should succeed but without a 3-city route #expect(result.isSuccess, "Should still succeed with partial routes") // Should NOT have a 3-city route due to time constraints let threeCityOption = result.options.first { option in option.stops.count == 3 && Set(option.stops.map { $0.city }) == Set(["Houston", "Chicago", "Anaheim"]) } #expect(threeCityOption == nil, "Should NOT include Houston→Chicago→Anaheim route when timing is impossible") } @Test("D.4.3 - Router picks optimal game in city to make route feasible") func test_followTeam_PicksOptimalGamePerCity_ForRouteFeasibility() { // Setup: Team has 3 games in each city (series) // With allowRepeatCities=false, router should pick games that make the route work let chicagoId = "stadium_chicago_\(UUID().uuidString)" let anaheimId = "stadium_anaheim_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let anaheim = makeStadium(id: anaheimId, city: "Anaheim", lat: 33.8003, lon: -117.8827) let stadiums = [chicagoId: chicago, anaheimId: anaheim] let teamId = "team_test_\(UUID().uuidString)" let opponent1 = "team_opponent1_\(UUID().uuidString)" let opponent2 = "team_opponent2_\(UUID().uuidString)" // Chicago series: July 24, 25, 26 let chicagoGame1 = makeGame( stadiumId: chicagoId, homeTeamId: opponent1, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 24, hour: 19) ) let chicagoGame2 = makeGame( stadiumId: chicagoId, homeTeamId: opponent1, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 25, hour: 19) ) let chicagoGame3 = makeGame( stadiumId: chicagoId, homeTeamId: opponent1, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 26, hour: 19) ) // Anaheim series: July 27, 28, 29 // Chicago July 24 → Anaheim July 29 = 5 days = feasible (~37 hrs driving, 40 hrs available) // Chicago July 26 → Anaheim July 27 = 1 day = NOT feasible (~37 hrs driving, 8 hrs available) let anaheimGame1 = makeGame( stadiumId: anaheimId, homeTeamId: opponent2, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 27, hour: 19) ) let anaheimGame2 = makeGame( stadiumId: anaheimId, homeTeamId: opponent2, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 28, hour: 19) ) let anaheimGame3 = makeGame( stadiumId: anaheimId, homeTeamId: opponent2, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 29, hour: 19) ) let request = makePlanningRequest( startDate: makeDate(month: 7, day: 22, hour: 0), endDate: makeDate(month: 7, day: 31, hour: 23), followTeamId: teamId, allGames: [chicagoGame1, chicagoGame2, chicagoGame3, anaheimGame1, anaheimGame2, anaheimGame3], stadiums: stadiums, allowRepeatCities: false ) // Execute let result = planner.plan(request: request) // Verify #expect(result.isSuccess, "Should succeed") // Should have a 2-city route (Chicago → Anaheim) let twoCityOption = result.options.first { option in option.stops.count == 2 && Set(option.stops.map { $0.city }) == Set(["Chicago", "Anaheim"]) } #expect(twoCityOption != nil, "Should include a Chicago→Anaheim route") } @Test("D.4.4 - Five-day driving segment at limit succeeds") func test_followTeam_FiveDaySegment_AtLimit_Succeeds() { // Setup: ~38 hours of driving with exactly 5 days between games // 5 days × 8 hours = 40 hours max, which should pass let seattleId = "stadium_seattle_\(UUID().uuidString)" let miamiId = "stadium_denver_\(UUID().uuidString)" // Seattle to Miami: ~3,300 miles straight line × 1.3 = ~4,300 miles // At 60 mph = ~72 hours - this is too far even for 5 days // Let's use a more reasonable pair: Seattle to Denver (~1,300 miles × 1.3 = ~1,700 miles = ~28 hrs) let seattle = makeStadium(id: seattleId, city: "Seattle", lat: 47.6062, lon: -122.3321) let denver = makeStadium(id: miamiId, city: "Denver", lat: 39.7392, lon: -104.9903) let stadiums = [seattleId: seattle, miamiId: denver] let teamId = "team_test_\(UUID().uuidString)" let opponent1 = "team_opponent1_\(UUID().uuidString)" let opponent2 = "team_opponent2_\(UUID().uuidString)" let seattleGame = makeGame( stadiumId: seattleId, homeTeamId: opponent1, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 20, hour: 19) ) // 4 days later = 32 hours max, ~28 hrs needed = should work let denverGame = makeGame( stadiumId: miamiId, homeTeamId: opponent2, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 24, hour: 19) ) let request = makePlanningRequest( startDate: makeDate(month: 7, day: 18, hour: 0), endDate: makeDate(month: 7, day: 26, hour: 23), followTeamId: teamId, allGames: [seattleGame, denverGame], stadiums: stadiums, allowRepeatCities: false ) // Execute let result = planner.plan(request: request) // Verify #expect(result.isSuccess, "Should succeed with long-distance segment") let twoCityOption = result.options.first { option in option.stops.count == 2 } #expect(twoCityOption != nil, "Should include 2-city route") if let option = twoCityOption { let cities = Set(option.stops.map { $0.city }) #expect(cities.contains("Seattle"), "Should include Seattle") #expect(cities.contains("Denver"), "Should include Denver") } } @Test("D.4.5 - Segment exceeding 5-day driving limit is rejected") func test_followTeam_SegmentExceedingFiveDayLimit_IsRejected() { // Setup: Distance that would take > 40 hours to drive // Seattle to Miami: ~3,300 miles straight line × 1.3 = ~4,300 miles // At 60 mph = ~72 hours - exceeds 40 hour (5 day) limit let seattleId = "stadium_seattle_\(UUID().uuidString)" let miamiId = "stadium_miami_\(UUID().uuidString)" let seattle = makeStadium(id: seattleId, city: "Seattle", lat: 47.6062, lon: -122.3321) let miami = makeStadium(id: miamiId, city: "Miami", lat: 25.7617, lon: -80.1918) let stadiums = [seattleId: seattle, miamiId: miami] let teamId = "team_test_\(UUID().uuidString)" let opponent1 = "team_opponent1_\(UUID().uuidString)" let opponent2 = "team_opponent2_\(UUID().uuidString)" let seattleGame = makeGame( stadiumId: seattleId, homeTeamId: opponent1, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 20, hour: 19) ) // Even with 5 days, Seattle to Miami is impossible by car let miamiGame = makeGame( stadiumId: miamiId, homeTeamId: opponent2, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 25, hour: 19) ) let request = makePlanningRequest( startDate: makeDate(month: 7, day: 18, hour: 0), endDate: makeDate(month: 7, day: 27, hour: 23), followTeamId: teamId, allGames: [seattleGame, miamiGame], stadiums: stadiums, allowRepeatCities: false ) // Execute let result = planner.plan(request: request) // Verify - should succeed but without a 2-city route #expect(result.isSuccess, "Should succeed with individual city options") // Should NOT have a Seattle→Miami route (too far) let twoCityOption = result.options.first { option in option.stops.count == 2 && Set(option.stops.map { $0.city }) == Set(["Seattle", "Miami"]) } #expect(twoCityOption == nil, "Should NOT include Seattle→Miami route (exceeds 5-day driving limit)") // Should have individual city options let singleCityOptions = result.options.filter { $0.stops.count == 1 } #expect(singleCityOptions.count >= 2, "Should have individual city options") } @Test("D.4.6 - Multiple drivers increases available driving time") func test_followTeam_MultipleDrivers_IncreasesAvailableTime() { // Setup: Same Chicago→Anaheim route but with 2 drivers // With 2 drivers × 8 hours = 16 hours/day // Chicago to Anaheim in 3 days = 48 hours available (vs 24 hours with 1 driver) let chicagoId = "stadium_chicago_\(UUID().uuidString)" let anaheimId = "stadium_anaheim_\(UUID().uuidString)" let chicago = makeStadium(id: chicagoId, city: "Chicago", lat: 41.8781, lon: -87.6298) let anaheim = makeStadium(id: anaheimId, city: "Anaheim", lat: 33.8003, lon: -117.8827) let stadiums = [chicagoId: chicago, anaheimId: anaheim] let teamId = "team_test_\(UUID().uuidString)" let opponent1 = "team_opponent1_\(UUID().uuidString)" let opponent2 = "team_opponent2_\(UUID().uuidString)" let chicagoGame = makeGame( stadiumId: chicagoId, homeTeamId: opponent1, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 24, hour: 19) ) // Only 3 days between games - with 1 driver (24 hrs max) this fails // With 2 drivers (48 hrs max) and 37.5 hrs needed, this should pass let anaheimGame = makeGame( stadiumId: anaheimId, homeTeamId: opponent2, awayTeamId: teamId, dateTime: makeDate(month: 7, day: 27, hour: 19) ) let request = makePlanningRequest( startDate: makeDate(month: 7, day: 22, hour: 0), endDate: makeDate(month: 7, day: 29, hour: 23), followTeamId: teamId, allGames: [chicagoGame, anaheimGame], stadiums: stadiums, allowRepeatCities: false, numberOfDrivers: 2, // Two drivers! maxDrivingHoursPerDriver: 8.0 ) // Execute let result = planner.plan(request: request) // Verify #expect(result.isSuccess, "Should succeed with 2 drivers") // Note: The TravelEstimator uses a fixed 5-day limit (40 hours with 1 driver at 8 hrs/day) // With 2 drivers, the limit is 5 × 16 = 80 hours // So 37.5 hours for Chicago→Anaheim should definitely work let twoCityOption = result.options.first { option in option.stops.count == 2 } // This test verifies the constraint system respects numberOfDrivers #expect(twoCityOption != nil || result.options.count > 0, "Should have route options with multiple drivers") } }