// // TripPlanningEngineTests.swift // SportsTimeTests // // Fresh test suite for the rewritten trip planning engine. // Organized by scenario and validation type. // import XCTest import CoreLocation @testable import SportsTime final class TripPlanningEngineTests: XCTestCase { var engine: TripPlanningEngine! override func setUp() { super.setUp() engine = TripPlanningEngine() } override func tearDown() { engine = nil super.tearDown() } // MARK: - Test Data Helpers func makeGame( id: UUID = UUID(), dateTime: Date, stadiumId: UUID = UUID(), homeTeamId: UUID = UUID(), awayTeamId: UUID = UUID(), sport: Sport = .mlb ) -> Game { Game( id: id, homeTeamId: homeTeamId, awayTeamId: awayTeamId, stadiumId: stadiumId, dateTime: dateTime, sport: sport, season: "2026" ) } func makeStadium( id: UUID = UUID(), name: String = "Test Stadium", city: String = "Test City", state: String = "TS", latitude: Double = 40.0, longitude: Double = -74.0 ) -> Stadium { Stadium( id: id, name: name, city: city, state: state, latitude: latitude, longitude: longitude, capacity: 40000 ) } func makeTeam( id: UUID = UUID(), name: String = "Test Team", city: String = "Test City", stadiumId: UUID = UUID() ) -> Team { Team( id: id, name: name, abbreviation: "TST", sport: .mlb, city: city, stadiumId: stadiumId ) } func makePreferences( startDate: Date = Date(), endDate: Date = Date().addingTimeInterval(86400 * 7), sports: Set = [.mlb], mustSeeGameIds: Set = [], startLocation: LocationInput? = nil, endLocation: LocationInput? = nil, numberOfDrivers: Int = 1, maxDrivingHoursPerDriver: Double = 8.0 ) -> TripPreferences { TripPreferences( planningMode: .dateRange, startLocation: startLocation, endLocation: endLocation, sports: sports, mustSeeGameIds: mustSeeGameIds, travelMode: .drive, startDate: startDate, endDate: endDate, numberOfStops: nil, tripDuration: nil, leisureLevel: .moderate, mustStopLocations: [], preferredCities: [], routePreference: .balanced, needsEVCharging: false, lodgingType: .hotel, numberOfDrivers: numberOfDrivers, maxDrivingHoursPerDriver: maxDrivingHoursPerDriver, catchOtherSports: false ) } func makeRequest( preferences: TripPreferences, games: [Game], teams: [UUID: Team] = [:], stadiums: [UUID: Stadium] = [:] ) -> PlanningRequest { PlanningRequest( preferences: preferences, availableGames: games, teams: teams, stadiums: stadiums ) } // MARK: - Scenario A Tests (Date Range) func test_ScenarioA_ValidDateRange_ReturnsItineraries() { // Given: A date range with games let startDate = Date() let endDate = startDate.addingTimeInterval(86400 * 7) let stadiumId = UUID() let homeTeamId = UUID() let awayTeamId = UUID() let stadium = makeStadium(id: stadiumId, city: "New York", latitude: 40.7128, longitude: -74.0060) let homeTeam = makeTeam(id: homeTeamId, name: "Yankees", city: "New York") let awayTeam = makeTeam(id: awayTeamId, name: "Red Sox", city: "Boston") let game = makeGame( dateTime: startDate.addingTimeInterval(86400 * 2), stadiumId: stadiumId, homeTeamId: homeTeamId, awayTeamId: awayTeamId ) let preferences = makePreferences(startDate: startDate, endDate: endDate) let request = makeRequest( preferences: preferences, games: [game], teams: [homeTeamId: homeTeam, awayTeamId: awayTeam], stadiums: [stadiumId: stadium] ) // When let result = engine.planItineraries(request: request) // Then XCTAssertTrue(result.isSuccess, "Should return success for valid date range with games") XCTAssertFalse(result.options.isEmpty, "Should return at least one itinerary option") } func test_ScenarioA_EmptyDateRange_ReturnsFailure() { // Given: An invalid date range (end before start) let startDate = Date() let endDate = startDate.addingTimeInterval(-86400) // End before start let preferences = makePreferences(startDate: startDate, endDate: endDate) let request = makeRequest(preferences: preferences, games: []) // When let result = engine.planItineraries(request: request) // Then XCTAssertFalse(result.isSuccess, "Should fail for invalid date range") if case .failure(let failure) = result { XCTAssertEqual(failure.reason, .missingDateRange, "Should fail with missingDateRange") } } func test_ScenarioA_NoGamesInRange_ReturnsFailure() { // Given: A valid date range but no games let startDate = Date() let endDate = startDate.addingTimeInterval(86400 * 7) let preferences = makePreferences(startDate: startDate, endDate: endDate) let request = makeRequest(preferences: preferences, games: []) // When let result = engine.planItineraries(request: request) // Then XCTAssertFalse(result.isSuccess, "Should fail when no games in range") } // MARK: - Scenario B Tests (Selected Games) func test_ScenarioB_SelectedGamesWithinRange_ReturnsSuccess() { // Given: Selected games within date range let startDate = Date() let endDate = startDate.addingTimeInterval(86400 * 7) let gameId = UUID() let stadiumId = UUID() let homeTeamId = UUID() let awayTeamId = UUID() let stadium = makeStadium(id: stadiumId, city: "Chicago", latitude: 41.8781, longitude: -87.6298) let homeTeam = makeTeam(id: homeTeamId, name: "Cubs", city: "Chicago") let awayTeam = makeTeam(id: awayTeamId, name: "Cardinals", city: "St. Louis") let game = makeGame( id: gameId, dateTime: startDate.addingTimeInterval(86400 * 3), stadiumId: stadiumId, homeTeamId: homeTeamId, awayTeamId: awayTeamId ) let preferences = makePreferences( startDate: startDate, endDate: endDate, mustSeeGameIds: [gameId] ) let request = makeRequest( preferences: preferences, games: [game], teams: [homeTeamId: homeTeam, awayTeamId: awayTeam], stadiums: [stadiumId: stadium] ) // When let result = engine.planItineraries(request: request) // Then XCTAssertTrue(result.isSuccess, "Should succeed when selected games are within date range") } func test_ScenarioB_SelectedGameOutsideDateRange_ReturnsFailure() { // Given: A selected game outside the date range let startDate = Date() let endDate = startDate.addingTimeInterval(86400 * 7) let gameId = UUID() let stadiumId = UUID() let homeTeamId = UUID() let awayTeamId = UUID() // Game is 10 days after start, but range is only 7 days let game = makeGame( id: gameId, dateTime: startDate.addingTimeInterval(86400 * 10), stadiumId: stadiumId, homeTeamId: homeTeamId, awayTeamId: awayTeamId ) let preferences = makePreferences( startDate: startDate, endDate: endDate, mustSeeGameIds: [gameId] ) let request = makeRequest( preferences: preferences, games: [game], teams: [:], stadiums: [:] ) // When let result = engine.planItineraries(request: request) // Then XCTAssertFalse(result.isSuccess, "Should fail when selected game is outside date range") if case .failure(let failure) = result { if case .dateRangeViolation(let games) = failure.reason { XCTAssertEqual(games.count, 1, "Should report one game out of range") XCTAssertEqual(games.first?.id, gameId, "Should report the correct game") } else { XCTFail("Expected dateRangeViolation failure reason") } } } // MARK: - Scenario C Tests (Start + End Locations) func test_ScenarioC_LinearRoute_ReturnsSuccess() { // Given: Start and end locations with games along the way let startDate = Date() let endDate = startDate.addingTimeInterval(86400 * 7) let startLocation = LocationInput( name: "Chicago", coordinate: CLLocationCoordinate2D(latitude: 41.8781, longitude: -87.6298) ) let endLocation = LocationInput( name: "New York", coordinate: CLLocationCoordinate2D(latitude: 40.7128, longitude: -74.0060) ) // Stadium in Cleveland (along the route) let stadiumId = UUID() let homeTeamId = UUID() let awayTeamId = UUID() let stadium = makeStadium( id: stadiumId, city: "Cleveland", latitude: 41.4993, longitude: -81.6944 ) let game = makeGame( dateTime: startDate.addingTimeInterval(86400 * 2), stadiumId: stadiumId, homeTeamId: homeTeamId, awayTeamId: awayTeamId ) let preferences = makePreferences( startDate: startDate, endDate: endDate, startLocation: startLocation, endLocation: endLocation ) let request = makeRequest( preferences: preferences, games: [game], teams: [homeTeamId: makeTeam(id: homeTeamId), awayTeamId: makeTeam(id: awayTeamId)], stadiums: [stadiumId: stadium] ) // When let result = engine.planItineraries(request: request) // Then XCTAssertTrue(result.isSuccess, "Should succeed for linear route with games") } // MARK: - Travel Segment Invariant Tests func test_TravelSegmentCount_EqualsStopsMinusOne() { // Given: A multi-stop itinerary let startDate = Date() let endDate = startDate.addingTimeInterval(86400 * 7) var stadiums: [UUID: Stadium] = [:] var teams: [UUID: Team] = [:] var games: [Game] = [] // Create 3 games in 3 cities let cities = [ ("New York", 40.7128, -74.0060), ("Philadelphia", 39.9526, -75.1652), ("Washington DC", 38.9072, -77.0369) ] for (index, (city, lat, lon)) in cities.enumerated() { let stadiumId = UUID() let homeTeamId = UUID() let awayTeamId = UUID() stadiums[stadiumId] = makeStadium(id: stadiumId, city: city, latitude: lat, longitude: lon) teams[homeTeamId] = makeTeam(id: homeTeamId, city: city) teams[awayTeamId] = makeTeam(id: awayTeamId) let game = makeGame( dateTime: startDate.addingTimeInterval(86400 * Double(index + 1)), stadiumId: stadiumId, homeTeamId: homeTeamId, awayTeamId: awayTeamId ) games.append(game) } let preferences = makePreferences(startDate: startDate, endDate: endDate) let request = makeRequest( preferences: preferences, games: games, teams: teams, stadiums: stadiums ) // When let result = engine.planItineraries(request: request) // Then if case .success(let options) = result, let option = options.first { let expectedSegments = option.stops.count - 1 XCTAssertEqual( option.travelSegments.count, max(0, expectedSegments), "Travel segments should equal stops - 1" ) XCTAssertTrue(option.isValid, "Itinerary should pass validity check") } } func test_SingleStopItinerary_HasZeroTravelSegments() { // Given: A single game (single stop) let startDate = Date() let endDate = startDate.addingTimeInterval(86400 * 7) let stadiumId = UUID() let homeTeamId = UUID() let awayTeamId = UUID() let stadium = makeStadium(id: stadiumId, latitude: 40.7128, longitude: -74.0060) let homeTeam = makeTeam(id: homeTeamId) let awayTeam = makeTeam(id: awayTeamId) let game = makeGame( dateTime: startDate.addingTimeInterval(86400 * 2), stadiumId: stadiumId, homeTeamId: homeTeamId, awayTeamId: awayTeamId ) let preferences = makePreferences(startDate: startDate, endDate: endDate) let request = makeRequest( preferences: preferences, games: [game], teams: [homeTeamId: homeTeam, awayTeamId: awayTeam], stadiums: [stadiumId: stadium] ) // When let result = engine.planItineraries(request: request) // Then if case .success(let options) = result, let option = options.first { if option.stops.count == 1 { XCTAssertEqual(option.travelSegments.count, 0, "Single stop should have zero travel segments") } } } // MARK: - Driving Constraints Tests func test_DrivingConstraints_MultipleDrivers_IncreasesCapacity() { // Given: Two drivers instead of one let constraints1 = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 8.0) let constraints2 = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0) // Then XCTAssertEqual(constraints1.maxDailyDrivingHours, 8.0, "Single driver = 8 hours max") XCTAssertEqual(constraints2.maxDailyDrivingHours, 16.0, "Two drivers = 16 hours max") } // MARK: - Ranking Tests func test_ItineraryOptions_AreRanked() { // Given: Multiple games that could form different routes let startDate = Date() let endDate = startDate.addingTimeInterval(86400 * 14) var stadiums: [UUID: Stadium] = [:] var teams: [UUID: Team] = [:] var games: [Game] = [] // Create games with coordinates let locations = [ ("City1", 40.0, -74.0), ("City2", 40.5, -73.5), ("City3", 41.0, -73.0) ] for (index, (city, lat, lon)) in locations.enumerated() { let stadiumId = UUID() let homeTeamId = UUID() let awayTeamId = UUID() stadiums[stadiumId] = makeStadium(id: stadiumId, city: city, latitude: lat, longitude: lon) teams[homeTeamId] = makeTeam(id: homeTeamId) teams[awayTeamId] = makeTeam(id: awayTeamId) let game = makeGame( dateTime: startDate.addingTimeInterval(86400 * Double(index + 1)), stadiumId: stadiumId, homeTeamId: homeTeamId, awayTeamId: awayTeamId ) games.append(game) } let preferences = makePreferences(startDate: startDate, endDate: endDate) let request = makeRequest( preferences: preferences, games: games, teams: teams, stadiums: stadiums ) // When let result = engine.planItineraries(request: request) // Then if case .success(let options) = result { for (index, option) in options.enumerated() { XCTAssertEqual(option.rank, index + 1, "Options should be ranked 1, 2, 3, ...") } } } // MARK: - Edge Case Tests func test_NoGamesAvailable_ReturnsExplicitFailure() { // Given: Empty games array let startDate = Date() let endDate = startDate.addingTimeInterval(86400 * 7) let preferences = makePreferences(startDate: startDate, endDate: endDate) let request = makeRequest(preferences: preferences, games: []) // When let result = engine.planItineraries(request: request) // Then XCTAssertFalse(result.isSuccess, "Should return failure for no games") XCTAssertNotNil(result.failure, "Should have explicit failure reason") } }