Files
Sportstime/SportsTimeTests/Planning/TravelEstimatorTests.swift
Trey t 1bd248c255 test(planning): complete test suite with Phase 11 edge cases
Implement comprehensive test infrastructure and all 124 tests across 11 phases:

- Phase 0: Test infrastructure (fixtures, mocks, helpers)
- Phases 1-10: Core planning engine tests (previously implemented)
- Phase 11: Edge case omnibus (11 new tests)
  - Data edge cases: nil stadiums, malformed dates, invalid coordinates
  - Boundary conditions: driving limits, radius boundaries
  - Time zone cases: cross-timezone games, DST transitions

Reorganize test structure under Planning/ directory with proper organization.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 01:14:40 -06:00

203 lines
8.0 KiB
Swift

//
// 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
)
}
}