//
//  ItineraryBuilderTests.swift
//  SportsTimeTests
//
//  TDD specification + property tests for ItineraryBuilder.
//

import Testing
import CoreLocation
@testable import SportsTime

@Suite("ItineraryBuilder")
struct ItineraryBuilderTests {

    // MARK: - Test Data

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

    private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
    private let bostonCoord = CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972)
    private let phillyCoord = CLLocationCoordinate2D(latitude: 39.9526, longitude: -75.1652)
    private let chicagoCoord = CLLocationCoordinate2D(latitude: 41.8827, longitude: -87.6233)
    private let seattleCoord = CLLocationCoordinate2D(latitude: 47.5914, longitude: -122.3316)

    private let calendar = Calendar.current

    // MARK: - Specification Tests: build()

    @Test("build: empty stops returns empty itinerary")
    func build_emptyStops_returnsEmptyItinerary() {
        let result = ItineraryBuilder.build(stops: [], constraints: constraints)

        #expect(result != nil)
        #expect(result?.stops.isEmpty == true)
        #expect(result?.travelSegments.isEmpty == true)
        #expect(result?.totalDrivingHours == 0)
        #expect(result?.totalDistanceMiles == 0)
    }

    @Test("build: single stop returns single-stop itinerary")
    func build_singleStop_returnsSingleStopItinerary() {
        let stop = makeStop(city: "New York", coordinate: nycCoord)

        let result = ItineraryBuilder.build(stops: [stop], constraints: constraints)

        #expect(result != nil)
        #expect(result?.stops.count == 1)
        #expect(result?.travelSegments.isEmpty == true)
        #expect(result?.totalDrivingHours == 0)
        #expect(result?.totalDistanceMiles == 0)
    }

    @Test("build: two stops creates one segment")
    func build_twoStops_createsOneSegment() {
        let stop1 = makeStop(city: "New York", coordinate: nycCoord)
        let stop2 = makeStop(city: "Boston", coordinate: bostonCoord)

        let result = ItineraryBuilder.build(stops: [stop1, stop2], constraints: constraints)

        #expect(result != nil)
        #expect(result?.stops.count == 2)
        #expect(result?.travelSegments.count == 1)
        #expect(result?.totalDrivingHours ?? 0 > 0)
        #expect(result?.totalDistanceMiles ?? 0 > 0)
    }

    @Test("build: three stops creates two segments")
    func build_threeStops_createsTwoSegments() {
        let stop1 = makeStop(city: "New York", coordinate: nycCoord)
        let stop2 = makeStop(city: "Philadelphia", coordinate: phillyCoord)
        let stop3 = makeStop(city: "Boston", coordinate: bostonCoord)

        let result = ItineraryBuilder.build(stops: [stop1, stop2, stop3], constraints: constraints)

        #expect(result != nil)
        #expect(result?.stops.count == 3)
        #expect(result?.travelSegments.count == 2)
    }

    @Test("build: totalDrivingHours is sum of segments")
    func build_totalDrivingHours_isSumOfSegments() {
        let stop1 = makeStop(city: "New York", coordinate: nycCoord)
        let stop2 = makeStop(city: "Philadelphia", coordinate: phillyCoord)
        let stop3 = makeStop(city: "Boston", coordinate: bostonCoord)

        let result = ItineraryBuilder.build(stops: [stop1, stop2, stop3], constraints: constraints)!

        let segmentHours = result.travelSegments.reduce(0.0) { $0 + $1.estimatedDrivingHours }
        #expect(abs(result.totalDrivingHours - segmentHours) < 0.01)
    }

    @Test("build: totalDistanceMiles is sum of segments")
    func build_totalDistanceMiles_isSumOfSegments() {
        let stop1 = makeStop(city: "New York", coordinate: nycCoord)
        let stop2 = makeStop(city: "Philadelphia", coordinate: phillyCoord)
        let stop3 = makeStop(city: "Boston", coordinate: bostonCoord)

        let result = ItineraryBuilder.build(stops: [stop1, stop2, stop3], constraints: constraints)!

        let segmentMiles = result.travelSegments.reduce(0.0) { $0 + $1.estimatedDistanceMiles }
        #expect(abs(result.totalDistanceMiles - segmentMiles) < 0.01)
    }

    @Test("build: infeasible segment returns nil")
    func build_infeasibleSegment_returnsNil() {
        // NYC to Seattle is ~2850 miles, ~47 hours driving
        // With 1 driver at 8 hrs/day, max is 40 hours (5 days)
        let stop1 = makeStop(city: "New York", coordinate: nycCoord)
        let stop2 = makeStop(city: "Seattle", coordinate: seattleCoord)

        let result = ItineraryBuilder.build(stops: [stop1, stop2], constraints: constraints)

        #expect(result == nil)
    }

    @Test("build: feasible with more drivers succeeds")
    func build_feasibleWithMoreDrivers_succeeds() {
        // NYC to Seattle with 2 drivers: max is 80 hours (2*8*5)
        let stop1 = makeStop(city: "New York", coordinate: nycCoord)
        let stop2 = makeStop(city: "Seattle", coordinate: seattleCoord)
        let twoDrivers = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 8.0)

        let result = ItineraryBuilder.build(stops: [stop1, stop2], constraints: twoDrivers)

        #expect(result != nil)
    }

    // MARK: - Specification Tests: Custom Validator

    @Test("build: validator returning true allows segment")
    func build_validatorReturnsTrue_allowsSegment() {
        let stop1 = makeStop(city: "New York", coordinate: nycCoord)
        let stop2 = makeStop(city: "Boston", coordinate: bostonCoord)

        let alwaysValid: ItineraryBuilder.SegmentValidator = { _, _, _ in true }

        let result = ItineraryBuilder.build(
            stops: [stop1, stop2],
            constraints: constraints,
            segmentValidator: alwaysValid
        )

        #expect(result != nil)
    }

    @Test("build: validator returning false rejects itinerary")
    func build_validatorReturnsFalse_rejectsItinerary() {
        let stop1 = makeStop(city: "New York", coordinate: nycCoord)
        let stop2 = makeStop(city: "Boston", coordinate: bostonCoord)

        let alwaysInvalid: ItineraryBuilder.SegmentValidator = { _, _, _ in false }

        let result = ItineraryBuilder.build(
            stops: [stop1, stop2],
            constraints: constraints,
            segmentValidator: alwaysInvalid
        )

        #expect(result == nil)
    }

    @Test("build: validator receives correct stops")
    func build_validatorReceivesCorrectStops() {
        let stop1 = makeStop(city: "New York", coordinate: nycCoord)
        let stop2 = makeStop(city: "Boston", coordinate: bostonCoord)

        var capturedFromCity: String?
        var capturedToCity: String?

        let captureValidator: ItineraryBuilder.SegmentValidator = { _, fromStop, toStop in
            capturedFromCity = fromStop.city
            capturedToCity = toStop.city
            return true
        }

        _ = ItineraryBuilder.build(
            stops: [stop1, stop2],
            constraints: constraints,
            segmentValidator: captureValidator
        )

        #expect(capturedFromCity == "New York")
        #expect(capturedToCity == "Boston")
    }

    // MARK: - Specification Tests: arrivalBeforeGameStart Validator

    @Test("arrivalBeforeGameStart: no game start time always passes")
    func arrivalBeforeGameStart_noGameStart_alwaysPasses() {
        let stop1 = makeStop(city: "New York", coordinate: nycCoord)
        let stop2 = makeStop(city: "Boston", coordinate: bostonCoord, firstGameStart: nil)

        let validator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600)

        let result = ItineraryBuilder.build(
            stops: [stop1, stop2],
            constraints: constraints,
            segmentValidator: validator
        )

        #expect(result != nil)
    }

    @Test("arrivalBeforeGameStart: sufficient time passes")
    func arrivalBeforeGameStart_sufficientTime_passes() {
        let now = Date()
        let tomorrow = calendar.date(byAdding: .day, value: 1, to: now)!
        let gameTime = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: tomorrow)!

        let stop1 = makeStop(
            city: "New York",
            coordinate: nycCoord,
            departureDate: now
        )
        let stop2 = makeStop(
            city: "Boston",
            coordinate: bostonCoord,
            firstGameStart: gameTime
        )

        // NYC to Boston is ~4 hours, game is tomorrow at 7pm, plenty of time
        let validator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600)

        let result = ItineraryBuilder.build(
            stops: [stop1, stop2],
            constraints: constraints,
            segmentValidator: validator
        )

        #expect(result != nil)
    }

    @Test("arrivalBeforeGameStart: insufficient time fails")
    func arrivalBeforeGameStart_insufficientTime_fails() {
        let now = Date()
        let gameTime = now.addingTimeInterval(2 * 3600)  // Game in 2 hours

        let stop1 = makeStop(
            city: "New York",
            coordinate: nycCoord,
            departureDate: now
        )
        let stop2 = makeStop(
            city: "Chicago",  // ~13 hours away
            coordinate: chicagoCoord,
            firstGameStart: gameTime
        )

        let validator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600)

        let result = ItineraryBuilder.build(
            stops: [stop1, stop2],
            constraints: constraints,
            segmentValidator: validator
        )

        // Should fail because we can't get to Chicago in 2 hours
        // (assuming the segment is even feasible, which it isn't for 1 driver)
        // Either the segment is infeasible OR the validator rejects it
        #expect(result == nil)
    }

    // MARK: - Property Tests

    @Test("Property: segments count equals stops count minus one")
    func property_segmentsCountEqualsStopsMinusOne() {
        for count in [2, 3, 4, 5] {
            let stops = (0..<count).map { i in
                makeStop(city: "City\(i)", coordinate: nycCoord)  // Same coord for all = always feasible
            }

            if let result = ItineraryBuilder.build(stops: stops, constraints: constraints) {
                #expect(result.travelSegments.count == stops.count - 1,
                       "For \(count) stops, should have \(count - 1) segments")
            }
        }
    }

    @Test("Property: totals are non-negative")
    func property_totalsNonNegative() {
        let stops = [
            makeStop(city: "New York", coordinate: nycCoord),
            makeStop(city: "Philadelphia", coordinate: phillyCoord)
        ]

        if let result = ItineraryBuilder.build(stops: stops, constraints: constraints) {
            #expect(result.totalDrivingHours >= 0)
            #expect(result.totalDistanceMiles >= 0)
        }
    }

    @Test("Property: empty/single stop always succeeds")
    func property_emptyOrSingleStopAlwaysSucceeds() {
        let emptyResult = ItineraryBuilder.build(stops: [], constraints: constraints)
        #expect(emptyResult != nil)

        let singleResult = ItineraryBuilder.build(
            stops: [makeStop(city: "NYC", coordinate: nycCoord)],
            constraints: constraints
        )
        #expect(singleResult != nil)
    }

    // MARK: - Edge Case Tests

    @Test("Edge: stops with nil coordinates use fallback")
    func edge_nilCoordinates_useFallback() {
        let stop1 = makeStop(city: "City1", coordinate: nil)
        let stop2 = makeStop(city: "City2", coordinate: nil)

        let result = ItineraryBuilder.build(stops: [stop1, stop2], constraints: constraints)

        // Should use fallback distance (300 miles)
        #expect(result != nil)
        #expect(result?.totalDistanceMiles ?? 0 > 0)
    }

    @Test("Edge: same city stops have zero distance")
    func edge_sameCityStops_zeroDistance() {
        let stop1 = makeStop(city: "New York", coordinate: nycCoord)
        let stop2 = makeStop(city: "New York", coordinate: nycCoord)  // Same location

        let result = ItineraryBuilder.build(stops: [stop1, stop2], constraints: constraints)

        #expect(result != nil)
        // Same coordinates should result in ~0 distance
        #expect(result?.totalDistanceMiles ?? 1000 < 1)
    }

    @Test("Edge: very long trip is still feasible with multiple drivers")
    func edge_veryLongTrip_feasibleWithMultipleDrivers() {
        // NYC -> Chicago -> Seattle
        let stops = [
            makeStop(city: "New York", coordinate: nycCoord),
            makeStop(city: "Chicago", coordinate: chicagoCoord),
            makeStop(city: "Seattle", coordinate: seattleCoord)
        ]

        // With 3 drivers, max is 120 hours (3*8*5)
        let threeDrivers = DrivingConstraints(numberOfDrivers: 3, maxHoursPerDriverPerDay: 8.0)

        let result = ItineraryBuilder.build(stops: stops, constraints: threeDrivers)

        #expect(result != nil)
    }

    // MARK: - Helper Methods

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