//
//  TravelEstimatorTests.swift
//  SportsTimeTests
//
//  TDD specification + property tests for TravelEstimator.
//

import Testing
import CoreLocation
@testable import SportsTime

@Suite("TravelEstimator")
struct TravelEstimatorTests {

    // MARK: - Test Data

    private let nyc = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
    private let boston = CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972)
    private let chicago = CLLocationCoordinate2D(latitude: 41.8827, longitude: -87.6233)
    private let losAngeles = CLLocationCoordinate2D(latitude: 34.0141, longitude: -118.2879)
    private let seattle = CLLocationCoordinate2D(latitude: 47.5914, longitude: -122.3316)

    private let defaultConstraints = DrivingConstraints.default  // 1 driver, 8 hrs/day

    // MARK: - Specification Tests: haversineDistanceMiles

    @Test("haversineDistanceMiles: same point returns zero")
    func haversineDistanceMiles_samePoint_returnsZero() {
        let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: nyc)
        #expect(distance == 0)
    }

    @Test("haversineDistanceMiles: NYC to Boston approximately 190 miles")
    func haversineDistanceMiles_nycToBoston_approximately190() {
        let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: boston)
        // NYC to Boston is approximately 190 miles as the crow flies
        #expect(distance > 180 && distance < 200)
    }

    @Test("haversineDistanceMiles: NYC to LA approximately 2450 miles")
    func haversineDistanceMiles_nycToLA_approximately2450() {
        let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: losAngeles)
        // NYC to LA is approximately 2450 miles as the crow flies
        #expect(distance > 2400 && distance < 2500)
    }

    @Test("haversineDistanceMiles: symmetric - distance(A,B) equals distance(B,A)")
    func haversineDistanceMiles_symmetric() {
        let distanceAB = TravelEstimator.haversineDistanceMiles(from: nyc, to: boston)
        let distanceBA = TravelEstimator.haversineDistanceMiles(from: boston, to: nyc)
        #expect(distanceAB == distanceBA)
    }

    // MARK: - Specification Tests: haversineDistanceMeters

    @Test("haversineDistanceMeters: same point returns zero")
    func haversineDistanceMeters_samePoint_returnsZero() {
        let distance = TravelEstimator.haversineDistanceMeters(from: nyc, to: nyc)
        #expect(distance == 0)
    }

    @Test("haversineDistanceMeters: consistent with miles calculation")
    func haversineDistanceMeters_consistentWithMiles() {
        let meters = TravelEstimator.haversineDistanceMeters(from: nyc, to: boston)
        let miles = TravelEstimator.haversineDistanceMiles(from: nyc, to: boston)

        // Convert meters to miles: 1 mile = 1609.34 meters
        let convertedMiles = meters / 1609.34
        #expect(abs(convertedMiles - miles) < 1.0)  // Within 1 mile tolerance
    }

    // MARK: - Specification Tests: estimateFallbackDistance

    @Test("estimateFallbackDistance: same city returns zero")
    func estimateFallbackDistance_sameCity_returnsZero() {
        let from = makeStop(city: "New York")
        let to = makeStop(city: "New York")

        let distance = TravelEstimator.estimateFallbackDistance(from: from, to: to)
        #expect(distance == 0)
    }

    @Test("estimateFallbackDistance: different cities returns 300 miles")
    func estimateFallbackDistance_differentCities_returns300() {
        let from = makeStop(city: "New York")
        let to = makeStop(city: "Boston")

        let distance = TravelEstimator.estimateFallbackDistance(from: from, to: to)
        #expect(distance == 300)
    }

    // MARK: - Specification Tests: calculateDistanceMiles

    @Test("calculateDistanceMiles: with coordinates uses Haversine times routing factor")
    func calculateDistanceMiles_withCoordinates_usesHaversineTimesRoutingFactor() {
        let from = makeStop(city: "New York", coordinate: nyc)
        let to = makeStop(city: "Boston", coordinate: boston)

        let distance = TravelEstimator.calculateDistanceMiles(from: from, to: to)
        let haversine = TravelEstimator.haversineDistanceMiles(from: nyc, to: boston)

        // Road distance = Haversine * 1.3
        #expect(abs(distance - haversine * 1.3) < 0.1)
    }

    @Test("calculateDistanceMiles: missing coordinates uses fallback")
    func calculateDistanceMiles_missingCoordinates_usesFallback() {
        let from = makeStop(city: "New York", coordinate: nil)
        let to = makeStop(city: "Boston", coordinate: nil)

        let distance = TravelEstimator.calculateDistanceMiles(from: from, to: to)
        #expect(distance == 300)  // Fallback distance
    }

    @Test("calculateDistanceMiles: same city without coordinates returns zero")
    func calculateDistanceMiles_sameCityNoCoords_returnsZero() {
        let from = makeStop(city: "New York", coordinate: nil)
        let to = makeStop(city: "New York", coordinate: nil)

        let distance = TravelEstimator.calculateDistanceMiles(from: from, to: to)
        #expect(distance == 0)
    }

    // MARK: - Specification Tests: estimate(ItineraryStop, ItineraryStop)

    @Test("estimate: valid coordinates returns TravelSegment")
    func estimate_validCoordinates_returnsTravelSegment() {
        let from = makeStop(city: "New York", coordinate: nyc)
        let to = makeStop(city: "Boston", coordinate: boston)

        let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints)

        #expect(segment != nil)
        #expect(segment?.distanceMeters ?? 0 > 0)
        #expect(segment?.durationSeconds ?? 0 > 0)
    }

    @Test("estimate: distance and duration are calculated correctly")
    func estimate_distanceAndDuration_calculatedCorrectly() {
        let from = makeStop(city: "New York", coordinate: nyc)
        let to = makeStop(city: "Boston", coordinate: boston)

        let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints)!

        let expectedMiles = TravelEstimator.calculateDistanceMiles(from: from, to: to)
        let expectedMeters = expectedMiles * 1609.34
        let expectedHours = expectedMiles / 60.0
        let expectedSeconds = expectedHours * 3600

        #expect(abs(segment.distanceMeters - expectedMeters) < 100)  // Within 100m
        #expect(abs(segment.durationSeconds - expectedSeconds) < 60)  // Within 1 minute
    }

    @Test("estimate: exceeding max driving hours returns nil")
    func estimate_exceedingMaxDrivingHours_returnsNil() {
        // NYC to Seattle is ~2850 miles, ~47.5 hours driving
        // With 1 driver at 8 hrs/day, max is 40 hours (5 days)
        let from = makeStop(city: "New York", coordinate: nyc)
        let to = makeStop(city: "Seattle", coordinate: seattle)

        let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints)

        #expect(segment == nil)
    }

    @Test("estimate: within max driving hours with multiple drivers succeeds")
    func estimate_withinMaxWithMultipleDrivers_succeeds() {
        // NYC to Seattle with 2 drivers: max is 80 hours (2*8*5)
        let from = makeStop(city: "New York", coordinate: nyc)
        let to = makeStop(city: "Seattle", coordinate: seattle)
        let twoDrivers = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0)

        let segment = TravelEstimator.estimate(from: from, to: to, constraints: twoDrivers)

        #expect(segment != nil)
    }

    // MARK: - Specification Tests: estimate(LocationInput, LocationInput)

    @Test("estimate LocationInput: missing from coordinate returns nil")
    func estimateLocationInput_missingFromCoordinate_returnsNil() {
        let from = LocationInput(name: "Unknown", coordinate: nil)
        let to = LocationInput(name: "Boston", coordinate: boston)

        let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints)

        #expect(segment == nil)
    }

    @Test("estimate LocationInput: missing to coordinate returns nil")
    func estimateLocationInput_missingToCoordinate_returnsNil() {
        let from = LocationInput(name: "New York", coordinate: nyc)
        let to = LocationInput(name: "Unknown", coordinate: nil)

        let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints)

        #expect(segment == nil)
    }

    @Test("estimate LocationInput: valid coordinates returns TravelSegment")
    func estimateLocationInput_validCoordinates_returnsTravelSegment() {
        let from = LocationInput(name: "New York", coordinate: nyc)
        let to = LocationInput(name: "Boston", coordinate: boston)

        let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints)

        #expect(segment != nil)
        #expect(segment?.distanceMeters ?? 0 > 0)
        #expect(segment?.durationSeconds ?? 0 > 0)
    }

    // MARK: - Specification Tests: calculateTravelDays

    @Test("calculateTravelDays: zero hours returns departure day only")
    func calculateTravelDays_zeroHours_returnsDepartureDay() {
        let departure = Date()
        let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 0)

        #expect(days.count == 1)
    }

    @Test("calculateTravelDays: 1-8 hours returns single day")
    func calculateTravelDays_1to8Hours_returnsSingleDay() {
        let departure = Date()

        for hours in [1.0, 4.0, 7.0, 8.0] {
            let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: hours)
            #expect(days.count == 1, "Expected 1 day for \(hours) hours, got \(days.count)")
        }
    }

    @Test("calculateTravelDays: 8.01-16 hours returns two days")
    func calculateTravelDays_8to16Hours_returnsTwoDays() {
        let departure = Date()

        for hours in [8.01, 12.0, 16.0] {
            let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: hours)
            #expect(days.count == 2, "Expected 2 days for \(hours) hours, got \(days.count)")
        }
    }

    @Test("calculateTravelDays: 16.01-24 hours returns three days")
    func calculateTravelDays_16to24Hours_returnsThreeDays() {
        let departure = Date()

        for hours in [16.01, 20.0, 24.0] {
            let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: hours)
            #expect(days.count == 3, "Expected 3 days for \(hours) hours, got \(days.count)")
        }
    }

    @Test("calculateTravelDays: all dates are start of day")
    func calculateTravelDays_allDatesAreStartOfDay() {
        let calendar = Calendar.current
        // Use a specific time that's not midnight
        var components = calendar.dateComponents([.year, .month, .day], from: Date())
        components.hour = 14
        components.minute = 30
        let departure = calendar.date(from: components)!

        let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 20)

        for day in days {
            let hour = calendar.component(.hour, from: day)
            let minute = calendar.component(.minute, from: day)
            #expect(hour == 0 && minute == 0, "Expected midnight, got \(hour):\(minute)")
        }
    }

    @Test("calculateTravelDays: consecutive days")
    func calculateTravelDays_consecutiveDays() {
        let calendar = Calendar.current
        let departure = Date()
        let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 24)

        #expect(days.count == 3)

        for i in 1..<days.count {
            let diff = calendar.dateComponents([.day], from: days[i-1], to: days[i])
            #expect(diff.day == 1, "Days should be consecutive")
        }
    }

    // MARK: - Property Tests

    @Test("Property: haversine distance is always non-negative")
    func property_haversineDistanceNonNegative() {
        let coordinates = [nyc, boston, chicago, losAngeles, seattle]

        for from in coordinates {
            for to in coordinates {
                let distance = TravelEstimator.haversineDistanceMiles(from: from, to: to)
                #expect(distance >= 0, "Distance should be non-negative")
            }
        }
    }

    @Test("Property: haversine distance is symmetric")
    func property_haversineDistanceSymmetric() {
        let coordinates = [nyc, boston, chicago, losAngeles, seattle]

        for from in coordinates {
            for to in coordinates {
                let distanceAB = TravelEstimator.haversineDistanceMiles(from: from, to: to)
                let distanceBA = TravelEstimator.haversineDistanceMiles(from: to, to: from)
                #expect(distanceAB == distanceBA, "Distance should be symmetric")
            }
        }
    }

    @Test("Property: triangle inequality holds")
    func property_triangleInequality() {
        // For any three points A, B, C: distance(A,C) <= distance(A,B) + distance(B,C)
        let a = nyc
        let b = chicago
        let c = losAngeles

        let ac = TravelEstimator.haversineDistanceMiles(from: a, to: c)
        let ab = TravelEstimator.haversineDistanceMiles(from: a, to: b)
        let bc = TravelEstimator.haversineDistanceMiles(from: b, to: c)

        #expect(ac <= ab + bc + 0.001, "Triangle inequality should hold")
    }

    @Test("Property: road distance >= straight line distance")
    func property_roadDistanceGreaterThanStraightLine() {
        let from = makeStop(city: "New York", coordinate: nyc)
        let to = makeStop(city: "Boston", coordinate: boston)

        let roadDistance = TravelEstimator.calculateDistanceMiles(from: from, to: to)
        let straightLine = TravelEstimator.haversineDistanceMiles(from: nyc, to: boston)

        #expect(roadDistance >= straightLine, "Road distance should be >= straight line")
    }

    @Test("Property: estimate duration proportional to distance")
    func property_durationProportionalToDistance() {
        let from = makeStop(city: "New York", coordinate: nyc)
        let to = makeStop(city: "Boston", coordinate: boston)
        let segment = TravelEstimator.estimate(from: from, to: to, constraints: defaultConstraints)!

        // Duration should be distance / 60 mph
        let miles = segment.distanceMeters / 1609.34
        let expectedHours = miles / 60.0
        let actualHours = segment.durationSeconds / 3600.0

        #expect(abs(actualHours - expectedHours) < 0.1, "Duration should be distance/60mph")
    }

    @Test("Property: more drivers allows longer trips")
    func property_moreDriversAllowsLongerTrips() {
        // NYC to LA is ~2450 miles, ~53 hours driving with routing factor
        let from = makeStop(city: "New York", coordinate: nyc)
        let to = makeStop(city: "Los Angeles", coordinate: losAngeles)

        let oneDriver = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 8.0)
        let twoDrivers = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0)

        let withOne = TravelEstimator.estimate(from: from, to: to, constraints: oneDriver)
        let withTwo = TravelEstimator.estimate(from: from, to: to, constraints: twoDrivers)

        // With more drivers, trips that fail with one driver should succeed
        // (or both succeed/fail, but never one succeeds and more drivers fails)
        if withOne != nil {
            #expect(withTwo != nil, "More drivers should not reduce capability")
        }
    }

    // MARK: - Edge Case Tests

    @Test("Edge: antipodal points (maximum distance)")
    func edge_antipodalPoints() {
        // NYC to a point roughly opposite on Earth
        let antipode = CLLocationCoordinate2D(
            latitude: -nyc.latitude,
            longitude: nyc.longitude + 180
        )

        let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: antipode)
        // Half Earth circumference is about 12,450 miles
        #expect(distance > 12000 && distance < 13000)
    }

    @Test("Edge: very close points")
    func edge_veryClosePoints() {
        let nearby = CLLocationCoordinate2D(
            latitude: nyc.latitude + 0.0001,
            longitude: nyc.longitude + 0.0001
        )

        let distance = TravelEstimator.haversineDistanceMiles(from: nyc, to: nearby)
        #expect(distance < 0.1, "Very close points should have near-zero distance")
    }

    @Test("Edge: crossing prime meridian")
    func edge_crossingPrimeMeridian() {
        let london = CLLocationCoordinate2D(latitude: 51.5074, longitude: -0.1278)
        let paris = CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522)

        let distance = TravelEstimator.haversineDistanceMiles(from: london, to: paris)
        // London to Paris is about 213 miles
        #expect(distance > 200 && distance < 230)
    }

    @Test("Edge: crossing date line")
    func edge_crossingDateLine() {
        let tokyo = CLLocationCoordinate2D(latitude: 35.6762, longitude: 139.6503)
        let honolulu = CLLocationCoordinate2D(latitude: 21.3069, longitude: -157.8583)

        let distance = TravelEstimator.haversineDistanceMiles(from: tokyo, to: honolulu)
        // Tokyo to Honolulu is about 3850 miles
        #expect(distance > 3700 && distance < 4000)
    }

    @Test("Edge: calculateTravelDays with exactly 8 hours")
    func edge_calculateTravelDays_exactly8Hours() {
        let departure = Date()
        let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 8.0)
        #expect(days.count == 1, "Exactly 8 hours should be 1 day")
    }

    @Test("Edge: calculateTravelDays just over 8 hours")
    func edge_calculateTravelDays_justOver8Hours() {
        let departure = Date()
        let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: 8.001)
        #expect(days.count == 2, "Just over 8 hours should be 2 days")
    }

    @Test("Edge: negative driving hours treated as minimum 1 day")
    func edge_negativeDrivingHours() {
        let departure = Date()
        let days = TravelEstimator.calculateTravelDays(departure: departure, drivingHours: -5)
        #expect(days.count >= 1, "Negative hours should still return at least 1 day")
    }

    // MARK: - Helper Methods

    private func makeStop(
        city: String,
        state: String = "XX",
        coordinate: CLLocationCoordinate2D? = nil
    ) -> ItineraryStop {
        ItineraryStop(
            city: city,
            state: state,
            coordinate: coordinate,
            games: [],
            arrivalDate: Date(),
            departureDate: Date(),
            location: LocationInput(name: city, coordinate: coordinate),
            firstGameStart: nil
        )
    }
}
