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