// // ItineraryBuilderTests.swift // SportsTimeTests // // Phase 8: ItineraryBuilder Tests // Builds day-by-day itinerary from route with travel segments. // import Testing import CoreLocation @testable import SportsTime @Suite("ItineraryBuilder Tests") struct ItineraryBuilderTests { // MARK: - Test Constants private let defaultConstraints = DrivingConstraints.default private let calendar = Calendar.current // Known locations for testing private let nyc = CLLocationCoordinate2D(latitude: 40.7128, longitude: -73.9352) private let boston = CLLocationCoordinate2D(latitude: 42.3601, longitude: -71.0589) private let chicago = CLLocationCoordinate2D(latitude: 41.8781, longitude: -87.6298) private let la = CLLocationCoordinate2D(latitude: 34.0522, longitude: -118.2437) // MARK: - 8.1 Single Game Creates Single Day @Test("Single stop creates itinerary with one stop and no travel segments") func test_builder_SingleGame_CreatesSingleDay() { // Arrange let gameId = "game_test_\(UUID().uuidString)" let stop = makeItineraryStop( city: "New York", state: "NY", coordinate: nyc, games: [gameId] ) // Act let result = ItineraryBuilder.build( stops: [stop], constraints: defaultConstraints ) // Assert #expect(result != nil, "Single stop should produce a valid itinerary") if let itinerary = result { #expect(itinerary.stops.count == 1, "Should have exactly 1 stop") #expect(itinerary.travelSegments.isEmpty, "Should have no travel segments") #expect(itinerary.totalDrivingHours == 0, "Should have 0 driving hours") #expect(itinerary.totalDistanceMiles == 0, "Should have 0 distance") } } // MARK: - 8.2 Multi-City Creates Travel Segments Between @Test("Multiple cities creates travel segments between consecutive stops") func test_builder_MultiCity_CreatesTravelSegmentsBetween() { // Arrange let stops = [ makeItineraryStop(city: "Boston", state: "MA", coordinate: boston, games: ["game_boston_\(UUID().uuidString)"]), makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: ["game_nyc_\(UUID().uuidString)"]), makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: ["game_chicago_\(UUID().uuidString)"]) ] // Act let result = ItineraryBuilder.build( stops: stops, constraints: defaultConstraints ) // Assert #expect(result != nil, "Multi-city trip should produce a valid itinerary") if let itinerary = result { #expect(itinerary.stops.count == 3, "Should have 3 stops") #expect(itinerary.travelSegments.count == 2, "Should have 2 travel segments (stops - 1)") // Verify segment 1: Boston -> NYC let segment1 = itinerary.travelSegments[0] #expect(segment1.fromLocation.name == "Boston", "First segment should start from Boston") #expect(segment1.toLocation.name == "New York", "First segment should end at New York") #expect(segment1.travelMode == .drive, "Travel mode should be drive") #expect(segment1.distanceMeters > 0, "Distance should be positive") #expect(segment1.durationSeconds > 0, "Duration should be positive") // Verify segment 2: NYC -> Chicago let segment2 = itinerary.travelSegments[1] #expect(segment2.fromLocation.name == "New York", "Second segment should start from New York") #expect(segment2.toLocation.name == "Chicago", "Second segment should end at Chicago") // Verify totals are accumulated #expect(itinerary.totalDrivingHours > 0, "Total driving hours should be positive") #expect(itinerary.totalDistanceMiles > 0, "Total distance should be positive") } } // MARK: - 8.3 Same City Multiple Games Groups On Same Day @Test("Same city multiple stops have zero distance travel between them") func test_builder_SameCity_MultipleGames_GroupsOnSameDay() { // Arrange - Two stops in the same city (different games, same location) let stops = [ makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: ["game_nyc_1_\(UUID().uuidString)"]), makeItineraryStop(city: "New York", state: "NY", coordinate: nyc, games: ["game_nyc_2_\(UUID().uuidString)"]) ] // Act let result = ItineraryBuilder.build( stops: stops, constraints: defaultConstraints ) // Assert #expect(result != nil, "Same city stops should produce a valid itinerary") if let itinerary = result { #expect(itinerary.stops.count == 2, "Should have 2 stops") #expect(itinerary.travelSegments.count == 1, "Should have 1 travel segment") // Travel within same city should be minimal/zero distance let segment = itinerary.travelSegments[0] #expect(segment.estimatedDistanceMiles < 1.0, "Same city travel should have near-zero distance, got \(segment.estimatedDistanceMiles)") #expect(segment.estimatedDrivingHours < 0.1, "Same city travel should have near-zero duration, got \(segment.estimatedDrivingHours)") // Total driving should be minimal #expect(itinerary.totalDrivingHours < 0.1, "Total driving hours should be near zero for same city") } } // MARK: - 8.4 Travel Days Inserted When Driving Exceeds 8 Hours @Test("Multi-day driving is calculated correctly when exceeding 8 hours per day") func test_builder_TravelDays_InsertedWhenDrivingExceeds8Hours() { // Arrange - Create a trip that requires multi-day driving // Boston to Chicago is ~850 miles haversine, ~1100 with road factor // At 60 mph, that's ~18 hours = 3 travel days (ceil(18/8) = 3) let stops = [ makeItineraryStop(city: "Boston", state: "MA", coordinate: boston, games: ["game_boston_\(UUID().uuidString)"]), makeItineraryStop(city: "Chicago", state: "IL", coordinate: chicago, games: ["game_chicago_\(UUID().uuidString)"]) ] // Use constraints that allow long trips let constraints = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0) // Act let result = ItineraryBuilder.build( stops: stops, constraints: constraints ) // Assert #expect(result != nil, "Long-distance trip should produce a valid itinerary") if let itinerary = result { // Get the travel segment #expect(itinerary.travelSegments.count == 1, "Should have 1 travel segment") let segment = itinerary.travelSegments[0] let drivingHours = segment.estimatedDrivingHours // Verify this is a multi-day drive #expect(drivingHours > 8.0, "Boston to Chicago should require more than 8 hours driving") // Calculate travel days using TravelEstimator let travelDays = TravelEstimator.calculateTravelDays( departure: Date(), drivingHours: drivingHours ) // Should span multiple days (ceil(hours/8)) let expectedDays = Int(ceil(drivingHours / 8.0)) #expect(travelDays.count == expectedDays, "Travel should span \(expectedDays) days for \(drivingHours) hours driving, got \(travelDays.count)") } } // MARK: - 8.5 Arrival Time Before Game Calculated @Test("Segment validator rejects trips where arrival is after game start") func test_builder_ArrivalTimeBeforeGame_Calculated() { // Arrange - Create stops where travel time makes arriving on time impossible let now = Date() let gameStartSoon = now.addingTimeInterval(2 * 3600) // Game starts in 2 hours let fromStop = makeItineraryStop( city: "Boston", state: "MA", coordinate: boston, games: ["game_boston_\(UUID().uuidString)"], departureDate: now ) // NYC game starts in 2 hours, but travel is ~4 hours let toStop = makeItineraryStop( city: "New York", state: "NY", coordinate: nyc, games: ["game_nyc_\(UUID().uuidString)"], firstGameStart: gameStartSoon ) // Use the arrival validator let arrivalValidator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600) // Act let result = ItineraryBuilder.build( stops: [fromStop, toStop], constraints: defaultConstraints, segmentValidator: arrivalValidator ) // Assert - Should return nil because we can't arrive 1 hour before game // Boston to NYC is ~4 hours, game starts in 2 hours, need 1 hour buffer // 4 hours travel > 2 hours - 1 hour buffer = 1 hour available #expect(result == nil, "Should return nil when arrival would be after game start minus buffer") } @Test("Segment validator accepts trips where arrival is before game start") func test_builder_ArrivalTimeBeforeGame_Succeeds() { // Arrange - Create stops where there's plenty of time let now = Date() let gameLater = now.addingTimeInterval(10 * 3600) // Game in 10 hours let fromStop = makeItineraryStop( city: "Boston", state: "MA", coordinate: boston, games: ["game_boston_\(UUID().uuidString)"], departureDate: now ) let toStop = makeItineraryStop( city: "New York", state: "NY", coordinate: nyc, games: ["game_nyc_\(UUID().uuidString)"], firstGameStart: gameLater ) let arrivalValidator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600) // Act let result = ItineraryBuilder.build( stops: [fromStop, toStop], constraints: defaultConstraints, segmentValidator: arrivalValidator ) // Assert - Should succeed, 4 hours travel leaves 5 hours before 10-hour deadline #expect(result != nil, "Should return valid itinerary when arrival is well before game") } // MARK: - 8.6 Empty Route Returns Empty Itinerary @Test("Empty stops array returns empty itinerary") func test_builder_EmptyRoute_ReturnsEmptyItinerary() { // Act let result = ItineraryBuilder.build( stops: [], constraints: defaultConstraints ) // Assert #expect(result != nil, "Empty stops should still return a valid (empty) itinerary") if let itinerary = result { #expect(itinerary.stops.isEmpty, "Should have no stops") #expect(itinerary.travelSegments.isEmpty, "Should have no travel segments") #expect(itinerary.totalDrivingHours == 0, "Should have 0 driving hours") #expect(itinerary.totalDistanceMiles == 0, "Should have 0 distance") } } // MARK: - Helpers private func makeItineraryStop( city: String, state: String, coordinate: CLLocationCoordinate2D? = nil, games: [String] = [], arrivalDate: Date = Date(), departureDate: Date? = nil, firstGameStart: Date? = nil ) -> ItineraryStop { ItineraryStop( city: city, state: state, coordinate: coordinate, games: games, arrivalDate: arrivalDate, departureDate: departureDate ?? calendar.date(byAdding: .day, value: 1, to: arrivalDate)!, location: LocationInput(name: city, coordinate: coordinate), firstGameStart: firstGameStart ) } }