// // ScenarioAPlannerSwiftTests.swift // SportsTimeTests // // Additional tests for ScenarioAPlanner using Swift Testing framework. // Combined with ScenarioAPlannerTests.swift, this provides comprehensive coverage. // import Testing @testable import SportsTime import Foundation import CoreLocation // MARK: - ScenarioAPlanner Swift Tests struct ScenarioAPlannerSwiftTests { // MARK: - Test Data Helpers private func makeStadium( id: UUID = UUID(), city: String, latitude: Double, longitude: Double, sport: Sport = .mlb ) -> Stadium { Stadium( id: id, name: "\(city) Stadium", city: city, state: "ST", latitude: latitude, longitude: longitude, capacity: 40000, sport: sport ) } private func makeGame( id: UUID = UUID(), stadiumId: UUID, dateTime: Date ) -> Game { Game( id: id, homeTeamId: UUID(), awayTeamId: UUID(), stadiumId: stadiumId, dateTime: dateTime, sport: .mlb, season: "2026" ) } private func baseDate() -> Date { Calendar.current.date(from: DateComponents(year: 2026, month: 4, day: 5))! } private func date(daysFrom base: Date, days: Int, hour: Int = 19) -> Date { var date = Calendar.current.date(byAdding: .day, value: days, to: base)! return Calendar.current.date(bySettingHour: hour, minute: 0, second: 0, of: date)! } private func makeDateRange(start: Date, days: Int) -> DateInterval { let end = Calendar.current.date(byAdding: .day, value: days, to: start)! return DateInterval(start: start, end: end) } private func plan( games: [Game], stadiums: [Stadium], dateRange: DateInterval, numberOfDrivers: Int = 1, maxHoursPerDriver: Double = 8.0 ) -> ItineraryResult { let stadiumDict = Dictionary(uniqueKeysWithValues: stadiums.map { ($0.id, $0) }) let preferences = TripPreferences( planningMode: .dateRange, startDate: dateRange.start, endDate: dateRange.end, numberOfDrivers: numberOfDrivers, maxDrivingHoursPerDriver: maxHoursPerDriver ) let request = PlanningRequest( preferences: preferences, availableGames: games, teams: [:], stadiums: stadiumDict ) let planner = ScenarioAPlanner() return planner.plan(request: request) } // MARK: - Failure Case Tests @Test("plan with no date range returns failure") func plan_NoDateRange_ReturnsFailure() { // Create a request without a valid date range let preferences = TripPreferences( planningMode: .dateRange, startDate: baseDate(), endDate: baseDate() // Same date = no range ) let request = PlanningRequest( preferences: preferences, availableGames: [], teams: [:], stadiums: [:] ) let planner = ScenarioAPlanner() let result = planner.plan(request: request) #expect(result.failure?.reason == .missingDateRange) } @Test("plan with games all outside date range returns failure") func plan_AllGamesOutsideRange_ReturnsFailure() { let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903) let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 30)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.failure?.reason == .noGamesInRange) } @Test("plan with end date before start date returns failure") func plan_InvalidDateRange_ReturnsFailure() { let preferences = TripPreferences( planningMode: .dateRange, startDate: baseDate(), endDate: Calendar.current.date(byAdding: .day, value: -5, to: baseDate())! ) let request = PlanningRequest( preferences: preferences, availableGames: [], teams: [:], stadiums: [:] ) let planner = ScenarioAPlanner() let result = planner.plan(request: request) #expect(result.failure != nil) } // MARK: - Success Case Tests @Test("plan returns success with valid single game") func plan_ValidSingleGame_ReturnsSuccess() { let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903) let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.count == 1) #expect(result.options.first?.stops.count == 1) } @Test("plan includes game exactly at range start") func plan_GameAtRangeStart_Included() { let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903) // Game exactly at start of range let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 0, hour: 10)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.stops.count == 1) } @Test("plan includes game exactly at range end") func plan_GameAtRangeEnd_Included() { let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903) // Game at end of range let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 9, hour: 19)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) } // MARK: - Driving Constraints Tests @Test("plan rejects route that exceeds driving limit") func plan_ExceedsDrivingLimit_RoutePruned() { // Create two cities ~2000 miles apart let ny = makeStadium(city: "New York", latitude: 40.7128, longitude: -74.0060) let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) // Games 1 day apart - impossible to drive let games = [ makeGame(stadiumId: ny.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 1)) ] let result = plan( games: games, stadiums: [ny, la], dateRange: makeDateRange(start: baseDate(), days: 10), numberOfDrivers: 1, maxHoursPerDriver: 8.0 ) // Should succeed but not have both games in same route if result.isSuccess { // May have single-game options but not both together #expect(true) } } @Test("plan with two drivers allows longer routes") func plan_TwoDrivers_AllowsLongerRoutes() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let denver = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903) // ~1000 miles, ~17 hours - doable with 2 drivers let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: denver.id, dateTime: date(daysFrom: baseDate(), days: 2)) ] let result = plan( games: games, stadiums: [la, denver], dateRange: makeDateRange(start: baseDate(), days: 10), numberOfDrivers: 2, maxHoursPerDriver: 8.0 ) #expect(result.isSuccess) } // MARK: - Stop Grouping Tests @Test("multiple games at same stadium grouped into one stop") func plan_SameStadiumGames_GroupedIntoOneStop() { let stadium = makeStadium(city: "Chicago", latitude: 41.8781, longitude: -87.6298) let games = [ makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 1)), makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) ] let result = plan( games: [games[0], games[1], games[2]], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.stops.count == 1) #expect(result.options.first?.stops.first?.games.count == 3) } @Test("stop arrival date is first game date") func plan_StopArrivalDate_IsFirstGameDate() { let stadium = makeStadium(city: "Chicago", latitude: 41.8781, longitude: -87.6298) let firstGame = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let secondGame = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 3)) let result = plan( games: [firstGame, secondGame], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let stop = result.options.first?.stops.first let firstGameDate = Calendar.current.startOfDay(for: firstGame.startTime) let stopArrival = Calendar.current.startOfDay(for: stop?.arrivalDate ?? Date.distantPast) #expect(firstGameDate == stopArrival) } @Test("stop departure date is last game date") func plan_StopDepartureDate_IsLastGameDate() { let stadium = makeStadium(city: "Chicago", latitude: 41.8781, longitude: -87.6298) let firstGame = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let secondGame = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 4)) let result = plan( games: [firstGame, secondGame], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let stop = result.options.first?.stops.first let lastGameDate = Calendar.current.startOfDay(for: secondGame.startTime) let stopDeparture = Calendar.current.startOfDay(for: stop?.departureDate ?? Date.distantFuture) #expect(lastGameDate == stopDeparture) } // MARK: - Travel Segment Tests @Test("single stop has zero travel segments") func plan_SingleStop_ZeroTravelSegments() { let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903) let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.travelSegments.isEmpty == true) } @Test("two stops have one travel segment") func plan_TwoStops_OneTravelSegment() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3)) ] let result = plan( games: games, stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let twoStopOption = result.options.first { $0.stops.count == 2 } #expect(twoStopOption?.travelSegments.count == 1) } @Test("travel segment has correct origin and destination") func plan_TravelSegment_CorrectOriginDestination() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3)) ] let result = plan( games: games, stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let twoStopOption = result.options.first { $0.stops.count == 2 } let segment = twoStopOption?.travelSegments.first #expect(segment?.fromLocation.name == "Los Angeles") #expect(segment?.toLocation.name == "San Francisco") } @Test("travel segment distance is reasonable for LA to SF") func plan_TravelSegment_ReasonableDistance() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3)) ] let result = plan( games: games, stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let twoStopOption = result.options.first { $0.stops.count == 2 } let distance = twoStopOption?.totalDistanceMiles ?? 0 // LA to SF is ~380 miles, with routing factor ~500 miles #expect(distance > 400 && distance < 600) } // MARK: - Option Ranking Tests @Test("options are ranked starting from 1") func plan_Options_RankedFromOne() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3)) ] let result = plan( games: games, stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.rank == 1) } @Test("all options have valid isValid property") func plan_Options_AllValid() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3)) ] let result = plan( games: games, stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) for option in result.options { #expect(option.isValid, "All options should pass isValid check") } } @Test("totalGames computed property is correct") func plan_TotalGames_ComputedCorrectly() { let stadium = makeStadium(city: "Chicago", latitude: 41.8781, longitude: -87.6298) let games = [ makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 1)), makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) ] let result = plan( games: games, stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.totalGames == 3) } // MARK: - Edge Cases @Test("games in reverse chronological order still processed correctly") func plan_ReverseChronologicalGames_ProcessedCorrectly() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) // Games added in reverse order let game1 = makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 5)) let game2 = makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 2)) let result = plan( games: [game1, game2], // SF first (later date) stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) // Should be sorted: LA (day 2) then SF (day 5) let twoStopOption = result.options.first { $0.stops.count == 2 } #expect(twoStopOption?.stops[0].city == "Los Angeles") #expect(twoStopOption?.stops[1].city == "San Francisco") } @Test("handles many games efficiently") func plan_ManyGames_HandledEfficiently() { var stadiums: [Stadium] = [] var games: [Game] = [] // Create 15 games along the west coast let cities: [(String, Double, Double)] = [ ("San Diego", 32.7157, -117.1611), ("Los Angeles", 34.0522, -118.2437), ("Bakersfield", 35.3733, -119.0187), ("Fresno", 36.7378, -119.7871), ("San Jose", 37.3382, -121.8863), ("San Francisco", 37.7749, -122.4194), ("Oakland", 37.8044, -122.2712), ("Sacramento", 38.5816, -121.4944), ("Reno", 39.5296, -119.8138), ("Redding", 40.5865, -122.3917), ("Eugene", 44.0521, -123.0868), ("Portland", 45.5152, -122.6784), ("Seattle", 47.6062, -122.3321), ("Tacoma", 47.2529, -122.4443), ("Vancouver", 49.2827, -123.1207) ] for (index, city) in cities.enumerated() { let id = UUID() stadiums.append(makeStadium(id: id, city: city.0, latitude: city.1, longitude: city.2)) games.append(makeGame(stadiumId: id, dateTime: date(daysFrom: baseDate(), days: index))) } let result = plan( games: games, stadiums: stadiums, dateRange: makeDateRange(start: baseDate(), days: 20) ) #expect(result.isSuccess) #expect(result.options.count <= 10) } @Test("empty stadiums dictionary returns failure") func plan_EmptyStadiums_ReturnsSuccess() { let stadiumId = UUID() let game = makeGame(stadiumId: stadiumId, dateTime: date(daysFrom: baseDate(), days: 2)) // Game exists but stadium not in dictionary let result = plan( games: [game], stadiums: [], dateRange: makeDateRange(start: baseDate(), days: 10) ) // Should handle gracefully (may return failure or success with empty) #expect(result.failure != nil || result.options.isEmpty || result.isSuccess) } @Test("stop has correct city from stadium") func plan_StopCity_MatchesStadium() { let stadium = makeStadium(city: "Phoenix", latitude: 33.4484, longitude: -112.0740) let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.stops.first?.city == "Phoenix") } @Test("stop has correct state from stadium") func plan_StopState_MatchesStadium() { let stadium = makeStadium(city: "Phoenix", latitude: 33.4484, longitude: -112.0740) let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.stops.first?.state == "ST") } @Test("stop has coordinate from stadium") func plan_StopCoordinate_MatchesStadium() { let stadium = makeStadium(city: "Phoenix", latitude: 33.4484, longitude: -112.0740) let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let coord = result.options.first?.stops.first?.coordinate #expect(coord != nil) #expect(abs(coord!.latitude - 33.4484) < 0.01) #expect(abs(coord!.longitude - (-112.0740)) < 0.01) } @Test("firstGameStart property is set correctly") func plan_FirstGameStart_SetCorrectly() { let stadium = makeStadium(city: "Denver", latitude: 39.7392, longitude: -104.9903) let gameTime = date(daysFrom: baseDate(), days: 2, hour: 19) let game = makeGame(stadiumId: stadium.id, dateTime: gameTime) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let firstGameStart = result.options.first?.stops.first?.firstGameStart #expect(firstGameStart == gameTime) } @Test("location property has correct name") func plan_LocationProperty_CorrectName() { let stadium = makeStadium(city: "Austin", latitude: 30.2672, longitude: -97.7431) let game = makeGame(stadiumId: stadium.id, dateTime: date(daysFrom: baseDate(), days: 2)) let result = plan( games: [game], stadiums: [stadium], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) #expect(result.options.first?.stops.first?.location.name == "Austin") } @Test("geographicRationale shows game count") func plan_GeographicRationale_ShowsGameCount() { let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3)) ] let result = plan( games: games, stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) let twoStopOption = result.options.first { $0.stops.count == 2 } #expect(twoStopOption?.geographicRationale.contains("2") == true) } @Test("options with same game count sorted by driving hours") func plan_SameGameCount_SortedByDrivingHours() { // Create scenario where multiple routes have same game count let la = makeStadium(city: "Los Angeles", latitude: 34.0522, longitude: -118.2437) let sf = makeStadium(city: "San Francisco", latitude: 37.7749, longitude: -122.4194) let games = [ makeGame(stadiumId: la.id, dateTime: date(daysFrom: baseDate(), days: 0)), makeGame(stadiumId: sf.id, dateTime: date(daysFrom: baseDate(), days: 3)) ] let result = plan( games: games, stadiums: [la, sf], dateRange: makeDateRange(start: baseDate(), days: 10) ) #expect(result.isSuccess) // All options should be valid and sorted for option in result.options { #expect(option.isValid) } } }