// // TravelEstimatorTests.swift // SportsTimeTests // // Phase 1: TravelEstimator Tests // Foundation tests — all planners depend on this. // import Testing import CoreLocation @testable import SportsTime @Suite("TravelEstimator Tests") struct TravelEstimatorTests { // MARK: - Test Constants private let nyc = CLLocationCoordinate2D(latitude: 40.7128, longitude: -73.9352) private let la = CLLocationCoordinate2D(latitude: 34.0522, longitude: -118.2437) private let samePoint = CLLocationCoordinate2D(latitude: 40.7128, longitude: -73.9352) // Antipodal point to NYC (roughly opposite side of Earth) private let antipodal = CLLocationCoordinate2D(latitude: -40.7128, longitude: 106.0648) // MARK: - 1.1 Haversine Known Distance @Test("NYC to LA is approximately 2,451 miles (within 1% tolerance)") func test_haversineDistanceMiles_KnownDistance() { let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: la) let expectedDistance = TestConstants.nycToLAMiles let tolerance = expectedDistance * TestConstants.distanceTolerancePercent #expect(abs(distance - expectedDistance) <= tolerance, "Expected \(expectedDistance) ± \(tolerance) miles, got \(distance)") } // MARK: - 1.2 Same Point Returns Zero @Test("Same point returns zero distance") func test_haversineDistanceMiles_SamePoint_ReturnsZero() { let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: samePoint) #expect(distance == 0.0, "Expected 0.0 miles for same point, got \(distance)") } // MARK: - 1.3 Antipodal Distance @Test("Antipodal points return approximately half Earth's circumference") func test_haversineDistanceMiles_Antipodal_ReturnsHalfEarthCircumference() { let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: antipodal) // Half Earth circumference ≈ 12,450 miles let halfCircumference = TestConstants.earthCircumferenceMiles / 2.0 let tolerance = halfCircumference * 0.05 // 5% tolerance for antipodal #expect(abs(distance - halfCircumference) <= tolerance, "Expected ~\(halfCircumference) miles for antipodal, got \(distance)") } // MARK: - 1.4 Nil Coordinates Returns Nil @Test("Estimate returns nil when coordinates are missing") func test_estimate_NilCoordinates_ReturnsNil() { let fromLocation = LocationInput(name: "Unknown City", coordinate: nil) let toLocation = LocationInput(name: "Another City", coordinate: nyc) let constraints = DrivingConstraints.default let result = TravelEstimator.estimate(from: fromLocation, to: toLocation, constraints: constraints) #expect(result == nil, "Expected nil when from coordinate is missing") // Also test when 'to' is nil let fromWithCoord = LocationInput(name: "NYC", coordinate: nyc) let toWithoutCoord = LocationInput(name: "Unknown", coordinate: nil) let result2 = TravelEstimator.estimate(from: fromWithCoord, to: toWithoutCoord, constraints: constraints) #expect(result2 == nil, "Expected nil when to coordinate is missing") } // MARK: - 1.5 Exceeds Max Daily Hours Returns Nil @Test("Estimate returns nil when trip exceeds maximum allowed driving hours") func test_estimate_ExceedsMaxDailyHours_ReturnsNil() { // NYC to LA is ~2,451 miles // At 60 mph, that's ~40.85 hours of driving // With road routing factor of 1.3, actual route is ~3,186 miles = ~53 hours // Max allowed is 2 days * 8 hours = 16 hours by default // So this should return nil let fromLocation = LocationInput(name: "NYC", coordinate: nyc) let toLocation = LocationInput(name: "LA", coordinate: la) let constraints = DrivingConstraints.default // 8 hours/day, 1 driver = 16 max let result = TravelEstimator.estimate(from: fromLocation, to: toLocation, constraints: constraints) #expect(result == nil, "Expected nil for trip exceeding max daily hours (NYC to LA with 16hr limit)") } // MARK: - 1.6 Valid Trip Returns Segment @Test("Estimate returns valid segment for feasible trip") func test_estimate_ValidTrip_ReturnsSegment() { // Boston to NYC is ~215 miles (within 1 day driving) let boston = CLLocationCoordinate2D(latitude: 42.3601, longitude: -71.0589) let fromLocation = LocationInput(name: "Boston", coordinate: boston) let toLocation = LocationInput(name: "NYC", coordinate: nyc) let constraints = DrivingConstraints.default let result = TravelEstimator.estimate(from: fromLocation, to: toLocation, constraints: constraints) #expect(result != nil, "Expected a travel segment for Boston to NYC") if let segment = result { // Verify travel mode #expect(segment.travelMode == .drive, "Expected drive mode") // Distance should be reasonable (with road routing factor) // Haversine Boston to NYC ≈ 190 miles, with 1.3 factor ≈ 247 miles let expectedDistanceMeters = 190.0 * 1.3 * 1609.344 // miles to meters let tolerance = expectedDistanceMeters * 0.15 // 15% tolerance #expect(abs(segment.distanceMeters - expectedDistanceMeters) <= tolerance, "Distance should be approximately \(expectedDistanceMeters) meters, got \(segment.distanceMeters)") // Duration should be reasonable // ~247 miles at 60 mph ≈ 4.1 hours = 14,760 seconds #expect(segment.durationSeconds > 0, "Duration should be positive") #expect(segment.durationSeconds < 8 * 3600, "Duration should be under 8 hours") } } // MARK: - 1.7 Single Day Drive @Test("4 hours of driving spans 1 day") func test_calculateTravelDays_SingleDayDrive() { let departure = Date() let drivingHours = 4.0 let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: drivingHours) #expect(days.count == 1, "Expected 1 day for 4 hours of driving, got \(days.count)") } // MARK: - 1.8 Multi-Day Drive @Test("20 hours of driving spans 3 days (ceil(20/8))") func test_calculateTravelDays_MultiDayDrive() { let departure = Date() let drivingHours = 20.0 let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: drivingHours) // ceil(20/8) = 3 days #expect(days.count == 3, "Expected 3 days for 20 hours of driving (ceil(20/8)), got \(days.count)") } // MARK: - 1.9 Fallback Distance Same City @Test("Fallback distance returns 0 for same city") func test_estimateFallbackDistance_SameCity_ReturnsZero() { let stop1 = makeItineraryStop(city: "Chicago", state: "IL") let stop2 = makeItineraryStop(city: "Chicago", state: "IL") let distance = TravelEstimator.estimateFallbackDistance(from: stop1, to: stop2) #expect(distance == 0.0, "Expected 0 miles for same city, got \(distance)") } // MARK: - 1.10 Fallback Distance Different City @Test("Fallback distance returns 300 miles for different cities") func test_estimateFallbackDistance_DifferentCity_Returns300() { let stop1 = makeItineraryStop(city: "Chicago", state: "IL") let stop2 = makeItineraryStop(city: "Milwaukee", state: "WI") let distance = TravelEstimator.estimateFallbackDistance(from: stop1, to: stop2) #expect(distance == 300.0, "Expected 300 miles for different cities, got \(distance)") } // MARK: - Helpers private func makeItineraryStop( city: String, state: String, coordinate: CLLocationCoordinate2D? = nil ) -> ItineraryStop { ItineraryStop( city: city, state: state, coordinate: coordinate, games: [], arrivalDate: Date(), departureDate: Date().addingTimeInterval(86400), location: LocationInput(name: city, coordinate: coordinate), firstGameStart: nil ) } }