// // TeamFirstIntegrationTests.swift // SportsTimeTests // // Integration tests for Team-First planning mode. // // These tests verify the end-to-end flow from team selection // to route generation, ensuring all components work together. // import Testing import CoreLocation @testable import SportsTime @Suite("TeamFirst Integration") struct TeamFirstIntegrationTests { // MARK: - Test Data private let planner = ScenarioEPlanner(currentDate: TestClock.now) // MLB stadiums with realistic coordinates private let yankeeStadiumCoord = CLLocationCoordinate2D(latitude: 40.8296, longitude: -73.9262) private let fenwayParkCoord = CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972) private let citizensBankCoord = CLLocationCoordinate2D(latitude: 39.9061, longitude: -75.1665) // MARK: - E3. Full Flow Integration Tests @Test("Integration: 3 MLB teams returns top 10 routes") func integration_3MLBTeams_returnsTop10Routes() { let baseDate = TestClock.now // Create realistic MLB stadiums let yankeeStadium = Stadium( id: "stadium_mlb_yankee_stadium", name: "Yankee Stadium", city: "New York", state: "NY", latitude: yankeeStadiumCoord.latitude, longitude: yankeeStadiumCoord.longitude, capacity: 47309, sport: .mlb ) let fenwayPark = Stadium( id: "stadium_mlb_fenway_park", name: "Fenway Park", city: "Boston", state: "MA", latitude: fenwayParkCoord.latitude, longitude: fenwayParkCoord.longitude, capacity: 37755, sport: .mlb ) let citizensBank = Stadium( id: "stadium_mlb_citizens_bank", name: "Citizens Bank Park", city: "Philadelphia", state: "PA", latitude: citizensBankCoord.latitude, longitude: citizensBankCoord.longitude, capacity: 42792, sport: .mlb ) // Create teams let yankees = Team( id: "team_mlb_nyy", name: "Yankees", abbreviation: "NYY", sport: .mlb, city: "New York", stadiumId: "stadium_mlb_yankee_stadium" ) let redsox = Team( id: "team_mlb_bos", name: "Red Sox", abbreviation: "BOS", sport: .mlb, city: "Boston", stadiumId: "stadium_mlb_fenway_park" ) let phillies = Team( id: "team_mlb_phi", name: "Phillies", abbreviation: "PHI", sport: .mlb, city: "Philadelphia", stadiumId: "stadium_mlb_citizens_bank" ) // Create games within a reasonable 6-day window (3 teams * 2 = 6 days) // For the window algorithm to find valid windows, games must span at least 6 days // Window check: windowEnd <= latestDay + 1, so with 6-day window from day 1, // windowEnd = day 7, so latestDay must be >= day 6 // Day 1: Yankees home // Day 3: Red Sox home // Day 6: Phillies home (spans 6 days, window fits) let calendar = TestClock.calendar let day1 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 1))! let day3 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 3))! let day6 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 6))! let yankeesGame = Game( id: "game_mlb_2026_nyy_opp_0401", homeTeamId: "team_mlb_nyy", awayTeamId: "team_mlb_opp", stadiumId: "stadium_mlb_yankee_stadium", dateTime: day1, sport: .mlb, season: "2026", isPlayoff: false ) let redsoxGame = Game( id: "game_mlb_2026_bos_opp_0403", homeTeamId: "team_mlb_bos", awayTeamId: "team_mlb_opp", stadiumId: "stadium_mlb_fenway_park", dateTime: day3, sport: .mlb, season: "2026", isPlayoff: false ) let philliesGame = Game( id: "game_mlb_2026_phi_opp_0405", homeTeamId: "team_mlb_phi", awayTeamId: "team_mlb_opp", stadiumId: "stadium_mlb_citizens_bank", dateTime: day6, sport: .mlb, season: "2026", isPlayoff: false ) let prefs = TripPreferences( planningMode: .teamFirst, sports: [.mlb], startDate: baseDate, endDate: baseDate.addingTimeInterval(86400 * 30), leisureLevel: .moderate, lodgingType: .hotel, numberOfDrivers: 2, selectedTeamIds: ["team_mlb_nyy", "team_mlb_bos", "team_mlb_phi"] ) let request = PlanningRequest( preferences: prefs, availableGames: [yankeesGame, redsoxGame, philliesGame], teams: [ "team_mlb_nyy": yankees, "team_mlb_bos": redsox, "team_mlb_phi": phillies ], stadiums: [ "stadium_mlb_yankee_stadium": yankeeStadium, "stadium_mlb_fenway_park": fenwayPark, "stadium_mlb_citizens_bank": citizensBank ] ) let result = planner.plan(request: request) guard case .success(let options) = result else { Issue.record("Expected success with 3 MLB teams in drivable region") return } // Verify we get routes (may be less than 10 due to limited game combinations) #expect(!options.isEmpty, "Should return at least one route") #expect(options.count <= 10, "Should return at most 10 routes") } @Test("Integration: each route visits all 3 stadiums") func integration_eachRouteVisitsAll3Stadiums() { let baseDate = TestClock.now let yankeeStadium = makeStadium( id: "yankee-stadium", name: "Yankee Stadium", city: "New York", coordinate: yankeeStadiumCoord ) let fenwayPark = makeStadium( id: "fenway-park", name: "Fenway Park", city: "Boston", coordinate: fenwayParkCoord ) let citizensBank = makeStadium( id: "citizens-bank", name: "Citizens Bank Park", city: "Philadelphia", coordinate: citizensBankCoord ) // Create multiple games per team to ensure routes can be found var games: [Game] = [] // Yankees games for dayOffset in [1, 7, 14] { games.append(Game( id: "yankees-\(dayOffset)", homeTeamId: "yankees", awayTeamId: "opponent", stadiumId: "yankee-stadium", dateTime: baseDate.addingTimeInterval(Double(86400 * dayOffset)), sport: .mlb, season: "2026", isPlayoff: false )) } // Red Sox games for dayOffset in [2, 8, 15] { games.append(Game( id: "redsox-\(dayOffset)", homeTeamId: "redsox", awayTeamId: "opponent", stadiumId: "fenway-park", dateTime: baseDate.addingTimeInterval(Double(86400 * dayOffset)), sport: .mlb, season: "2026", isPlayoff: false )) } // Phillies games for dayOffset in [3, 9, 16] { games.append(Game( id: "phillies-\(dayOffset)", homeTeamId: "phillies", awayTeamId: "opponent", stadiumId: "citizens-bank", dateTime: baseDate.addingTimeInterval(Double(86400 * dayOffset)), sport: .mlb, season: "2026", isPlayoff: false )) } let prefs = TripPreferences( planningMode: .teamFirst, sports: [.mlb], startDate: baseDate, endDate: baseDate.addingTimeInterval(86400 * 30), leisureLevel: .moderate, lodgingType: .hotel, numberOfDrivers: 2, selectedTeamIds: ["yankees", "redsox", "phillies"] ) let request = PlanningRequest( preferences: prefs, availableGames: games, teams: [ "yankees": makeTeam(id: "yankees", name: "Yankees"), "redsox": makeTeam(id: "redsox", name: "Red Sox"), "phillies": makeTeam(id: "phillies", name: "Phillies") ], stadiums: [ "yankee-stadium": yankeeStadium, "fenway-park": fenwayPark, "citizens-bank": citizensBank ] ) let result = planner.plan(request: request) guard case .success(let options) = result else { Issue.record("Expected success") return } // Verify each route visits all 3 stadiums for option in options { let citiesVisited = Set(option.stops.map { $0.city }) #expect(citiesVisited.contains("New York"), "Every route must visit New York") #expect(citiesVisited.contains("Boston"), "Every route must visit Boston") #expect(citiesVisited.contains("Philadelphia"), "Every route must visit Philadelphia") } } @Test("Integration: total duration within 6 days (teams x 2)") func integration_totalDurationWithinLimit() { let baseDate = TestClock.now let yankeeStadium = makeStadium( id: "yankee-stadium", name: "Yankee Stadium", city: "New York", coordinate: yankeeStadiumCoord ) let fenwayPark = makeStadium( id: "fenway-park", name: "Fenway Park", city: "Boston", coordinate: fenwayParkCoord ) let citizensBank = makeStadium( id: "citizens-bank", name: "Citizens Bank Park", city: "Philadelphia", coordinate: citizensBankCoord ) // Create games that fit within a 6-day window // For 3 teams, window = 6 days. Games must span at least 6 days. let calendar = TestClock.calendar let day1 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 1))! let day3 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 3))! let day6 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 6))! let yankeesGame = Game( id: "yankees-home", homeTeamId: "yankees", awayTeamId: "opponent", stadiumId: "yankee-stadium", dateTime: day1, sport: .mlb, season: "2026", isPlayoff: false ) let redsoxGame = Game( id: "redsox-home", homeTeamId: "redsox", awayTeamId: "opponent", stadiumId: "fenway-park", dateTime: day3, sport: .mlb, season: "2026", isPlayoff: false ) let philliesGame = Game( id: "phillies-home", homeTeamId: "phillies", awayTeamId: "opponent", stadiumId: "citizens-bank", dateTime: day6, sport: .mlb, season: "2026", isPlayoff: false ) let prefs = TripPreferences( planningMode: .teamFirst, sports: [.mlb], startDate: baseDate, endDate: baseDate.addingTimeInterval(86400 * 30), leisureLevel: .moderate, lodgingType: .hotel, numberOfDrivers: 2, selectedTeamIds: ["yankees", "redsox", "phillies"] ) // 3 teams * 2 = 6 days max window let maxDays = prefs.teamFirstMaxDays #expect(maxDays == 6, "Window should be 6 days for 3 teams") let request = PlanningRequest( preferences: prefs, availableGames: [yankeesGame, redsoxGame, philliesGame], teams: [ "yankees": makeTeam(id: "yankees", name: "Yankees"), "redsox": makeTeam(id: "redsox", name: "Red Sox"), "phillies": makeTeam(id: "phillies", name: "Phillies") ], stadiums: [ "yankee-stadium": yankeeStadium, "fenway-park": fenwayPark, "citizens-bank": citizensBank ] ) let result = planner.plan(request: request) guard case .success(let options) = result else { Issue.record("Expected success") return } // Verify each route's duration is reasonable for option in options { guard let firstStop = option.stops.first, let lastStop = option.stops.last else { continue } let calendar = TestClock.calendar let tripDays = calendar.dateComponents( [.day], from: calendar.startOfDay(for: firstStop.arrivalDate), to: calendar.startOfDay(for: lastStop.departureDate) ).day ?? 0 // Trip duration should be within the window (allowing +1 for same-day start/end) #expect(tripDays <= maxDays, "Trip duration (\(tripDays) days) should not exceed window (\(maxDays) days)") } } @Test("Integration: factory selects ScenarioEPlanner for teamFirst mode") func integration_factorySelectsScenarioEPlanner() { let baseDate = TestClock.now let prefs = TripPreferences( planningMode: .teamFirst, sports: [.mlb], startDate: baseDate, endDate: baseDate.addingTimeInterval(86400 * 30), leisureLevel: .moderate, lodgingType: .hotel, numberOfDrivers: 2, selectedTeamIds: ["team1", "team2"] ) let request = PlanningRequest( preferences: prefs, availableGames: [], teams: [:], stadiums: [:] ) let scenario = ScenarioPlannerFactory.classify(request) #expect(scenario == .scenarioE, "Should classify as scenarioE for teamFirst mode with 2+ teams") let planner = ScenarioPlannerFactory.planner(for: request) #expect(planner is ScenarioEPlanner, "Should return ScenarioEPlanner for teamFirst mode") } @Test("Integration: factory requires 2+ teams for ScenarioE") func integration_factoryRequires2TeamsForScenarioE() { let baseDate = TestClock.now // With only 1 team, should NOT select ScenarioE var prefs = TripPreferences( planningMode: .teamFirst, sports: [.mlb], startDate: baseDate, endDate: baseDate.addingTimeInterval(86400 * 30), leisureLevel: .moderate, lodgingType: .hotel, numberOfDrivers: 2, selectedTeamIds: ["team1"] // Only 1 team ) var request = PlanningRequest( preferences: prefs, availableGames: [], teams: [:], stadiums: [:] ) var scenario = ScenarioPlannerFactory.classify(request) #expect(scenario != .scenarioE, "Should NOT classify as scenarioE with only 1 team") // With 0 teams, should also NOT select ScenarioE prefs.selectedTeamIds = [] request = PlanningRequest( preferences: prefs, availableGames: [], teams: [:], stadiums: [:] ) scenario = ScenarioPlannerFactory.classify(request) #expect(scenario != .scenarioE, "Should NOT classify as scenarioE with 0 teams") } @Test("Integration: realistic east coast trip with 4 teams") func integration_realisticEastCoastTrip() { let baseDate = TestClock.now // East coast stadiums (NYC, Boston, Philly, Baltimore) let yankeeStadium = makeStadium( id: "yankee-stadium", name: "Yankee Stadium", city: "New York", coordinate: CLLocationCoordinate2D(latitude: 40.8296, longitude: -73.9262) ) let fenwayPark = makeStadium( id: "fenway-park", name: "Fenway Park", city: "Boston", coordinate: CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972) ) let citizensBank = makeStadium( id: "citizens-bank", name: "Citizens Bank Park", city: "Philadelphia", coordinate: CLLocationCoordinate2D(latitude: 39.9061, longitude: -75.1665) ) let camdenYards = makeStadium( id: "camden-yards", name: "Camden Yards", city: "Baltimore", coordinate: CLLocationCoordinate2D(latitude: 39.2838, longitude: -76.6215) ) // Create games spread across 8-day window (4 teams * 2 = 8 days) // For 4 teams, window = 8 days. Games must span at least 8 days. let calendar = TestClock.calendar let day1 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 1))! let day3 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 3))! let day5 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 5))! let day8 = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 8))! let games = [ Game(id: "yankees-1", homeTeamId: "yankees", awayTeamId: "opp", stadiumId: "yankee-stadium", dateTime: day1, sport: .mlb, season: "2026", isPlayoff: false), Game(id: "redsox-1", homeTeamId: "redsox", awayTeamId: "opp", stadiumId: "fenway-park", dateTime: day3, sport: .mlb, season: "2026", isPlayoff: false), Game(id: "phillies-1", homeTeamId: "phillies", awayTeamId: "opp", stadiumId: "citizens-bank", dateTime: day5, sport: .mlb, season: "2026", isPlayoff: false), Game(id: "orioles-1", homeTeamId: "orioles", awayTeamId: "opp", stadiumId: "camden-yards", dateTime: day8, sport: .mlb, season: "2026", isPlayoff: false) ] let prefs = TripPreferences( planningMode: .teamFirst, sports: [.mlb], startDate: baseDate, endDate: baseDate.addingTimeInterval(86400 * 30), leisureLevel: .moderate, lodgingType: .hotel, numberOfDrivers: 2, selectedTeamIds: ["yankees", "redsox", "phillies", "orioles"] ) let request = PlanningRequest( preferences: prefs, availableGames: games, teams: [ "yankees": makeTeam(id: "yankees", name: "Yankees"), "redsox": makeTeam(id: "redsox", name: "Red Sox"), "phillies": makeTeam(id: "phillies", name: "Phillies"), "orioles": makeTeam(id: "orioles", name: "Orioles") ], stadiums: [ "yankee-stadium": yankeeStadium, "fenway-park": fenwayPark, "citizens-bank": citizensBank, "camden-yards": camdenYards ] ) let result = planner.plan(request: request) guard case .success(let options) = result else { Issue.record("Expected success with drivable east coast trip") return } #expect(!options.isEmpty, "Should find routes for east coast trip") // Verify each route visits all 4 cities for option in options { let citiesVisited = Set(option.stops.map { $0.city }) #expect(citiesVisited.count >= 4, "Each route should visit at least 4 cities (one per team)") } } // MARK: - Helper Methods private func makeStadium( id: String, name: String, city: String, coordinate: CLLocationCoordinate2D ) -> Stadium { Stadium( id: id, name: name, city: city, state: "XX", latitude: coordinate.latitude, longitude: coordinate.longitude, capacity: 40000, sport: .mlb ) } private func makeTeam(id: String, name: String) -> Team { Team( id: id, name: name, abbreviation: String(name.prefix(3)).uppercased(), sport: .mlb, city: "Test City", stadiumId: id ) } }