import XCTest
@testable import Flights

// MARK: - Test Doubles
//
// Phase 3 wired the production `AircraftRotationProvider` protocol in
// `Services/DelayCascadePredictor.swift`, so we just consume it here
// rather than re-declaring it.

/// Stub rotation provider: returns whatever segments the test handed in,
/// regardless of which icao24 / lookback is queried.
actor MockRotationProvider: AircraftRotationProvider {
    private let segments: [AircraftRotationTracker.RotationSegment]

    init(segments: [AircraftRotationTracker.RotationSegment]) {
        self.segments = segments
    }

    func rotation(forICAO24: String, lookbackHours: Int) async -> [AircraftRotationTracker.RotationSegment] {
        return segments
    }
}

final class DelayCascadePredictorTests: XCTestCase {

    // Fixed reference point — every test offsets from here so absolute
    // wall-clock time doesn't matter.
    private let scheduledDeparture = Date(timeIntervalSince1970: 1_750_000_000)
    private let departureICAO = "KJFK"
    private let carrier = "DL"
    private let flightNumber = 1234

    // MARK: - Test 1: missing operating aircraft

    func test_nilOperatingICAO24_returnsNil() async {
        let provider = MockRotationProvider(segments: [
            segment(arrivalICAO: "KJFK", arrivalOffsetMin: 60)
        ])
        let predictor = DelayCascadePredictor(tracker: provider)

        let result = await predictor.predict(
            carrier: carrier,
            flightNumber: flightNumber,
            scheduledDeparture: scheduledDeparture,
            departureICAO: departureICAO,
            operatingICAO24: nil
        )

        XCTAssertNil(result, "No tail assigned → no cascade prediction.")
    }

    func test_emptyOperatingICAO24_returnsNil() async {
        let provider = MockRotationProvider(segments: [
            segment(arrivalICAO: "KJFK", arrivalOffsetMin: 60)
        ])
        let predictor = DelayCascadePredictor(tracker: provider)

        let result = await predictor.predict(
            carrier: carrier,
            flightNumber: flightNumber,
            scheduledDeparture: scheduledDeparture,
            departureICAO: departureICAO,
            operatingICAO24: "   "
        )

        XCTAssertNil(result, "Whitespace-only icao24 → no cascade prediction.")
    }

    // MARK: - Test 2: rotation empty

    func test_emptyRotation_returnsNil() async {
        let provider = MockRotationProvider(segments: [])
        let predictor = DelayCascadePredictor(tracker: provider)

        let result = await predictor.predict(
            carrier: carrier,
            flightNumber: flightNumber,
            scheduledDeparture: scheduledDeparture,
            departureICAO: departureICAO,
            operatingICAO24: "a1b2c3"
        )

        XCTAssertNil(result, "No upstream segments → no cascade prediction.")
    }

    // MARK: - Test 3: wrong arrival station

    func test_lastSegmentArrivedAtDifferentStation_returnsNil() async {
        // Aircraft last landed at KATL but we're operating out of KJFK.
        let provider = MockRotationProvider(segments: [
            segment(arrivalICAO: "KATL", arrivalOffsetMin: 60)
        ])
        let predictor = DelayCascadePredictor(tracker: provider)

        let result = await predictor.predict(
            carrier: carrier,
            flightNumber: flightNumber,
            scheduledDeparture: scheduledDeparture,
            departureICAO: departureICAO,
            operatingICAO24: "a1b2c3"
        )

        XCTAssertNil(result, "Aircraft not yet at departure station → no cascade prediction.")
    }

    func test_lastSegmentArrivalICAOMissing_returnsNil() async {
        let provider = MockRotationProvider(segments: [
            segment(arrivalICAO: nil, arrivalOffsetMin: 60)
        ])
        let predictor = DelayCascadePredictor(tracker: provider)

        let result = await predictor.predict(
            carrier: carrier,
            flightNumber: flightNumber,
            scheduledDeparture: scheduledDeparture,
            departureICAO: departureICAO,
            operatingICAO24: "a1b2c3"
        )

        XCTAssertNil(result, "Unknown arrival airport → no cascade prediction.")
    }

    // MARK: - Test 4: 60-min late upstream, 30 min until scheduled departure → ~75 min cascade

    func test_upstreamLandsLate_cascadesByExpectedAmount() async {
        // Aircraft landed at JFK 30 minutes AFTER scheduled departure
        // (arrivalOffsetMin = +30). Add the 45-minute narrowbody turn and
        // earliest pushback is 75 min past scheduled departure.
        let provider = MockRotationProvider(segments: [
            segment(arrivalICAO: "KJFK", arrivalOffsetMin: 30)
        ])
        let predictor = DelayCascadePredictor(tracker: provider)

        let result = await predictor.predict(
            carrier: carrier,
            flightNumber: flightNumber,
            scheduledDeparture: scheduledDeparture,
            departureICAO: departureICAO,
            operatingICAO24: "a1b2c3"
        )

        guard let prediction = result else {
            XCTFail("Expected a cascade prediction, got nil.")
            return
        }
        XCTAssertEqual(prediction.predictedDelayMin, 75, "30 min late arrival + 45 min turn = 75 min cascade.")
        XCTAssertNotNil(prediction.upstreamSegment, "Prediction must surface the upstream leg used.")
        XCTAssertFalse(prediction.basis.isEmpty, "Basis string must explain the prediction.")
    }

    // MARK: - Test 5: 5 min late → below threshold

    func test_upstreamOnlyMildlyLate_returnsNil() async {
        // Arrival 50 min BEFORE scheduled departure → 5 min after the
        // 45-min turn window. Both the raw lateness AND the propagated
        // minutes are below the 15-min reporting threshold.
        let provider = MockRotationProvider(segments: [
            segment(arrivalICAO: "KJFK", arrivalOffsetMin: -50)
        ])
        let predictor = DelayCascadePredictor(tracker: provider)

        let result = await predictor.predict(
            carrier: carrier,
            flightNumber: flightNumber,
            scheduledDeparture: scheduledDeparture,
            departureICAO: departureICAO,
            operatingICAO24: "a1b2c3"
        )

        XCTAssertNil(result, "Below threshold cascade should not surface.")
    }

    // MARK: - Test 6: exactly 45 min before scheduled departure → turn absorbs

    func test_arrivalExactly45MinBeforeScheduled_returnsNil() async {
        // Aircraft landed 45 min before scheduled departure. Earliest
        // pushback equals scheduled departure → propagated 0 → no cascade.
        let provider = MockRotationProvider(segments: [
            segment(arrivalICAO: "KJFK", arrivalOffsetMin: -45)
        ])
        let predictor = DelayCascadePredictor(tracker: provider)

        let result = await predictor.predict(
            carrier: carrier,
            flightNumber: flightNumber,
            scheduledDeparture: scheduledDeparture,
            departureICAO: departureICAO,
            operatingICAO24: "a1b2c3"
        )

        XCTAssertNil(result, "Turn exactly absorbs upstream lateness → no cascade.")
    }

    // MARK: - Test 7: confidence > 0.5 once propagatedMinutes >= 30

    func test_confidenceCrosses50WhenPropagatedAtLeast30() async {
        // Arrival 15 min AFTER scheduled departure → 60 min propagated.
        // Confidence should comfortably exceed 0.5.
        let provider = MockRotationProvider(segments: [
            segment(arrivalICAO: "KJFK", arrivalOffsetMin: 15)
        ])
        let predictor = DelayCascadePredictor(tracker: provider)

        let result = await predictor.predict(
            carrier: carrier,
            flightNumber: flightNumber,
            scheduledDeparture: scheduledDeparture,
            departureICAO: departureICAO,
            operatingICAO24: "a1b2c3"
        )

        guard let prediction = result else {
            XCTFail("Expected a cascade prediction, got nil.")
            return
        }
        XCTAssertGreaterThanOrEqual(prediction.predictedDelayMin, 30,
                                    "Sanity check on the cascade size we're scoring.")
        XCTAssertGreaterThan(prediction.confidence, 0.5,
                             "Propagated >= 30 min should produce confidence > 0.5.")
        XCTAssertLessThanOrEqual(prediction.confidence, 1.0,
                                 "Confidence should always be a probability.")
    }

    // MARK: - Helpers

    /// Builds a single rotation segment whose arrival time is offset from
    /// `scheduledDeparture` by `arrivalOffsetMin` minutes (positive = late
    /// vs. scheduled, negative = before scheduled).
    private func segment(arrivalICAO: String?,
                         arrivalOffsetMin: Int,
                         departureICAO: String? = "KBOS") -> AircraftRotationTracker.RotationSegment {
        let arrival = scheduledDeparture.addingTimeInterval(Double(arrivalOffsetMin) * 60)
        // Block time of 90 min before arrival — exact value doesn't matter
        // for the predictor, which only consults arrivalTime.
        let departure = arrival.addingTimeInterval(-90 * 60)
        return AircraftRotationTracker.RotationSegment(
            id: "test-seg-\(arrivalOffsetMin)",
            departureICAO: departureICAO,
            arrivalICAO: arrivalICAO,
            departureTime: departure,
            arrivalTime: arrival,
            estimatedDelayMin: nil
        )
    }
}
