// // MustStopValidationTests.swift // SportsTimeTests // // Tests for must-stop location filtering across all scenario planners. // import Testing import CoreLocation @testable import SportsTime @Suite("Must-Stop Validation") struct MustStopValidationTests { // MARK: - Centralized Must-Stop Filter (TripPlanningEngine) @Test("scenarioA: must stops filter routes to include required cities") func scenarioA_mustStops_routesContainRequiredCities() { let baseDate = TestFixtures.date(year: 2026, month: 6, day: 1, hour: 19, minute: 0) let day2 = TestClock.calendar.date(byAdding: .day, value: 1, to: baseDate)! let day3 = TestClock.calendar.date(byAdding: .day, value: 2, to: baseDate)! let gameNYC = TestFixtures.game(city: "New York", dateTime: baseDate) let gameBOS = TestFixtures.game(city: "Boston", dateTime: day2) let gamePHL = TestFixtures.game(city: "Philadelphia", dateTime: day3) let stadiums = TestFixtures.stadiumMap(for: [gameNYC, gameBOS, gamePHL]) let prefs = TripPreferences( planningMode: .dateRange, sports: [.mlb], startDate: baseDate, endDate: day3, mustStopLocations: [LocationInput(name: "Boston")] ) let request = PlanningRequest( preferences: prefs, availableGames: [gameNYC, gameBOS, gamePHL], teams: [:], stadiums: stadiums ) let engine = TripPlanningEngine() let result = engine.planItineraries(request: request) guard case .success(let options) = result else { Issue.record("Expected .success, got \(result)"); return } for option in options { let cities = option.stops.map { $0.city.lowercased() } #expect(cities.contains("boston"), "Every route must include Boston as a must-stop") } } @Test("must stop impossible city returns failure") func mustStops_impossibleCity_returnsFailure() { let baseDate = TestFixtures.date(year: 2026, month: 6, day: 1, hour: 19, minute: 0) let day2 = TestClock.calendar.date(byAdding: .day, value: 1, to: baseDate)! let gameNYC = TestFixtures.game(city: "New York", dateTime: baseDate) let gameBOS = TestFixtures.game(city: "Boston", dateTime: day2) let stadiums = TestFixtures.stadiumMap(for: [gameNYC, gameBOS]) // Require a city with no games let prefs = TripPreferences( planningMode: .dateRange, sports: [.mlb], startDate: baseDate, endDate: day2, mustStopLocations: [LocationInput(name: "Denver")] ) let request = PlanningRequest( preferences: prefs, availableGames: [gameNYC, gameBOS], teams: [:], stadiums: stadiums ) let engine = TripPlanningEngine() let result = engine.planItineraries(request: request) // Should fail because no route can include Denver #expect(!result.isSuccess) } @Test("scenarioB: must stops enforced via centralized filter") func scenarioB_mustStops_routesContainRequiredCities() { let baseDate = TestFixtures.date(year: 2026, month: 6, day: 1, hour: 19, minute: 0) let day2 = TestClock.calendar.date(byAdding: .day, value: 1, to: baseDate)! let day3 = TestClock.calendar.date(byAdding: .day, value: 2, to: baseDate)! let gameNYC = TestFixtures.game(id: "must_see_1", city: "New York", dateTime: baseDate) let gameBOS = TestFixtures.game(city: "Boston", dateTime: day2) let gamePHL = TestFixtures.game(city: "Philadelphia", dateTime: day3) let stadiums = TestFixtures.stadiumMap(for: [gameNYC, gameBOS, gamePHL]) let prefs = TripPreferences( planningMode: .gameFirst, sports: [.mlb], mustSeeGameIds: [gameNYC.id], startDate: baseDate, endDate: day3, mustStopLocations: [LocationInput(name: "Boston")] ) let request = PlanningRequest( preferences: prefs, availableGames: [gameNYC, gameBOS, gamePHL], teams: [:], stadiums: stadiums ) let engine = TripPlanningEngine() let result = engine.planItineraries(request: request) guard case .success(let options) = result else { Issue.record("Expected .success, got \(result)"); return } for option in options { let cities = option.stops.map { $0.city.lowercased() } #expect(cities.contains("boston"), "Must-stop filter should ensure Boston is included") } } @Test("scenarioD: must stops enforced via centralized filter") func scenarioD_mustStops_routesContainRequiredCities() { let baseDate = TestFixtures.date(year: 2026, month: 6, day: 1, hour: 19, minute: 0) let day2 = TestClock.calendar.date(byAdding: .day, value: 1, to: baseDate)! let day3 = TestClock.calendar.date(byAdding: .day, value: 2, to: baseDate)! let teamId = "team_mlb_new_york" let gameNYC = TestFixtures.game(city: "New York", dateTime: baseDate, homeTeamId: teamId) let gameBOS = TestFixtures.game(city: "Boston", dateTime: day2, homeTeamId: "team_mlb_boston", awayTeamId: teamId) let gamePHL = TestFixtures.game(city: "Philadelphia", dateTime: day3, homeTeamId: "team_mlb_philadelphia", awayTeamId: teamId) let stadiums = TestFixtures.stadiumMap(for: [gameNYC, gameBOS, gamePHL]) let prefs = TripPreferences( planningMode: .followTeam, sports: [.mlb], startDate: baseDate, endDate: day3, mustStopLocations: [LocationInput(name: "Boston")], followTeamId: teamId ) let request = PlanningRequest( preferences: prefs, availableGames: [gameNYC, gameBOS, gamePHL], teams: [teamId: TestFixtures.team(city: "New York")], stadiums: stadiums ) let engine = TripPlanningEngine() let result = engine.planItineraries(request: request) guard case .success(let options) = result else { Issue.record("Expected .success, got \(result)"); return } for option in options { let cities = option.stops.map { $0.city.lowercased() } #expect(cities.contains("boston"), "Must-stop filter should ensure Boston is included in follow-team mode") } } @Test("scenarioE: must stops enforced via centralized filter") func scenarioE_mustStops_routesContainRequiredCities() { let baseDate = TestFixtures.date(year: 2026, month: 6, day: 1, hour: 19, minute: 0) // 2 teams → windowDuration = 4 days. Games must be within 3 days to fit in a single window. let day3 = TestClock.calendar.date(byAdding: .day, value: 3, to: baseDate)! let teamNYC = "team_mlb_new_york" let teamBOS = "team_mlb_boston" let gameNYC = TestFixtures.game(city: "New York", dateTime: baseDate, homeTeamId: teamNYC) let gameBOS = TestFixtures.game(city: "Boston", dateTime: day3, homeTeamId: teamBOS) let stadiums = TestFixtures.stadiumMap(for: [gameNYC, gameBOS]) let prefs = TripPreferences( planningMode: .teamFirst, sports: [.mlb], startDate: baseDate, endDate: day3, mustStopLocations: [LocationInput(name: "Boston")], selectedTeamIds: [teamNYC, teamBOS] ) let request = PlanningRequest( preferences: prefs, availableGames: [gameNYC, gameBOS], teams: [ teamNYC: TestFixtures.team(city: "New York"), teamBOS: TestFixtures.team(city: "Boston"), ], stadiums: stadiums ) let engine = TripPlanningEngine() let result = engine.planItineraries(request: request) guard case .success(let options) = result else { Issue.record("Expected .success, got \(result)"); return } for option in options { let cities = option.stops.map { $0.city.lowercased() } #expect(cities.contains("boston"), "Must-stop filter should ensure Boston is included in team-first mode") } } @Test("scenarioC: must stops enforced via centralized filter") func scenarioC_mustStops_routesContainRequiredCities() { // Route: Chicago → New York (eastward). Detroit is directionally between them. let baseDate = TestClock.now let endDate = TestClock.calendar.date(byAdding: .day, value: 10, to: baseDate)! let chiCoord = TestFixtures.coordinates["Chicago"]! let detCoord = TestFixtures.coordinates["Detroit"]! let nycCoord = TestFixtures.coordinates["New York"]! let chiStadium = TestFixtures.stadium(id: "chi", city: "Chicago") let detStadium = TestFixtures.stadium(id: "det", city: "Detroit") let nycStadium = TestFixtures.stadium(id: "nyc", city: "New York") let gameCHI = TestFixtures.game(city: "Chicago", dateTime: TestClock.addingDays(1), stadiumId: "chi") let gameDET = TestFixtures.game(city: "Detroit", dateTime: TestClock.addingDays(4), stadiumId: "det") let gameNYC = TestFixtures.game(city: "New York", dateTime: TestClock.addingDays(7), stadiumId: "nyc") let prefs = TripPreferences( planningMode: .locations, startLocation: LocationInput(name: "Chicago", coordinate: chiCoord), endLocation: LocationInput(name: "New York", coordinate: nycCoord), sports: [.mlb], startDate: baseDate, endDate: endDate, leisureLevel: .moderate, mustStopLocations: [LocationInput(name: "Detroit")], lodgingType: .hotel, numberOfDrivers: 2 ) let request = PlanningRequest( preferences: prefs, availableGames: [gameCHI, gameDET, gameNYC], teams: [:], stadiums: ["chi": chiStadium, "det": detStadium, "nyc": nycStadium] ) let engine = TripPlanningEngine() let result = engine.planItineraries(request: request) guard case .success(let options) = result else { Issue.record("Expected .success, got \(result)"); return } for option in options { let cities = option.stops.map { $0.city.lowercased() } #expect(cities.contains("detroit"), "Must-stop filter should ensure Detroit is included in Chicago→NYC route") } } }