import XCTest
@testable import Flights

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

/// Hard-coded schedule provider. Tests configure airport autocomplete
/// results and a list of schedules to return; the mock plays them back.
actor MockScheduleProvider: FlightScheduleProvider {
    private let airportLookups: [String: [Airport]]
    private let schedulesToReturn: [FlightSchedule]
    private let shouldThrowOnSchedules: Bool

    init(airportLookups: [String: [Airport]] = [:],
         schedulesToReturn: [FlightSchedule] = [],
         shouldThrowOnSchedules: Bool = false) {
        self.airportLookups = airportLookups
        self.schedulesToReturn = schedulesToReturn
        self.shouldThrowOnSchedules = shouldThrowOnSchedules
    }

    func searchAirports(term: String) async throws -> [Airport] {
        return airportLookups[term.uppercased()] ?? []
    }

    func allSchedules(
        dep: String,
        des: String,
        onProgress: @Sendable @escaping (Int, Int) -> Void
    ) async throws -> [FlightSchedule] {
        if shouldThrowOnSchedules {
            throw NSError(domain: "MockScheduleProvider", code: -1, userInfo: nil)
        }
        return schedulesToReturn
    }
}

final class SisterFlightServiceTests: XCTestCase {

    // A fixed test date so day-of-week assertions are deterministic.
    // 2026-06-03 is a Wednesday → Calendar weekday = 4.
    private lazy var targetDate: Date = {
        var components = DateComponents()
        components.year = 2026
        components.month = 6
        components.day = 3
        components.hour = 12
        components.minute = 0
        components.timeZone = TimeZone(identifier: "UTC")
        return Calendar(identifier: .gregorian).date(from: components)!
    }()

    private let origin = "JFK"
    private let dest = "LAX"

    // MARK: - Test 1: empty schedules

    func test_emptySchedules_returnsEmptyArray() async {
        let provider = MockScheduleProvider(
            airportLookups: [
                "JFK": [airport(id: "jfk-id", iata: "JFK")],
                "LAX": [airport(id: "lax-id", iata: "LAX")]
            ],
            schedulesToReturn: []
        )
        let service = SisterFlightService(flightService: provider)

        let results = await service.sisterFlights(
            origin: origin,
            dest: dest,
            date: targetDate,
            currentFlight: nil
        )

        XCTAssertTrue(results.isEmpty, "Empty upstream schedule → empty sister-flight list.")
    }

    // MARK: - Test 2: schedules that don't operate on target date are filtered

    func test_schedulesNotOperatingOnTargetDate_areFiltered() async {
        let weekday = Calendar.current.component(.weekday, from: targetDate)
        let otherWeekdays = Set([1, 2, 3, 4, 5, 6, 7]).subtracting([weekday])

        // Two schedules: one runs on the target weekday, one doesn't.
        let operating = schedule(
            airlineIATA: "DL",
            flightNumberRaw: "DL 100",
            departureTime: "09:00",
            arrivalTime: "12:00",
            daysOfWeek: [weekday]
        )
        let nonOperating = schedule(
            airlineIATA: "AA",
            flightNumberRaw: "AA 200",
            departureTime: "10:00",
            arrivalTime: "13:00",
            daysOfWeek: otherWeekdays
        )

        let provider = MockScheduleProvider(
            airportLookups: [
                "JFK": [airport(id: "jfk-id", iata: "JFK")],
                "LAX": [airport(id: "lax-id", iata: "LAX")]
            ],
            schedulesToReturn: [operating, nonOperating]
        )
        let service = SisterFlightService(flightService: provider)

        let results = await service.sisterFlights(
            origin: origin,
            dest: dest,
            date: targetDate,
            currentFlight: nil
        )

        XCTAssertEqual(results.count, 1, "Only the schedule operating on the target weekday should survive.")
        XCTAssertEqual(results.first?.carrier, "DL")
        XCTAssertEqual(results.first?.flightNumber, 100)
    }

    // MARK: - Test 3: currentFlight match marks one entry isYourFlight

    func test_currentFlightMatch_marksIsYourFlight() async {
        let weekday = Calendar.current.component(.weekday, from: targetDate)

        let mine = schedule(
            airlineIATA: "UA",
            flightNumberRaw: "UA 555",
            departureTime: "08:00",
            arrivalTime: "11:00",
            daysOfWeek: [weekday]
        )
        let other = schedule(
            airlineIATA: "UA",
            flightNumberRaw: "UA 777",
            departureTime: "14:00",
            arrivalTime: "17:00",
            daysOfWeek: [weekday]
        )

        let provider = MockScheduleProvider(
            airportLookups: [
                "JFK": [airport(id: "jfk-id", iata: "JFK")],
                "LAX": [airport(id: "lax-id", iata: "LAX")]
            ],
            schedulesToReturn: [mine, other]
        )
        let service = SisterFlightService(flightService: provider)

        let results = await service.sisterFlights(
            origin: origin,
            dest: dest,
            date: targetDate,
            currentFlight: (carrier: "UA", number: 555)
        )

        XCTAssertEqual(results.count, 2)
        let mineResult = results.first { $0.flightNumber == 555 }
        let otherResult = results.first { $0.flightNumber == 777 }
        XCTAssertNotNil(mineResult, "User's flight should be present in results.")
        XCTAssertNotNil(otherResult, "Other sister flight should be present.")
        XCTAssertTrue(mineResult?.isYourFlight == true, "Matching carrier+number → isYourFlight true.")
        XCTAssertTrue(otherResult?.isYourFlight == false, "Non-matching flight should not be flagged.")
    }

    // MARK: - Test 4: sort by predictedLoad ascending (nil last), then by scheduledDeparture

    func test_resultsSortedByLoadAscending_nilLast_thenByDeparture() async {
        let weekday = Calendar.current.component(.weekday, from: targetDate)

        // Four schedules with distinct departure times so we can identify them.
        // Loads injected via predictor below: DL=0.50, AA=0.10, UA=0.10, B6=nil.
        // Expected order:
        //   AA (0.10, 09:00)   — earliest tie-broken
        //   UA (0.10, 11:00)
        //   DL (0.50, 08:00)
        //   B6 (nil,  10:00)
        let dl = schedule(
            airlineIATA: "DL",
            flightNumberRaw: "DL 100",
            departureTime: "08:00",
            arrivalTime: "11:00",
            daysOfWeek: [weekday]
        )
        let aa = schedule(
            airlineIATA: "AA",
            flightNumberRaw: "AA 200",
            departureTime: "09:00",
            arrivalTime: "12:00",
            daysOfWeek: [weekday]
        )
        let b6 = schedule(
            airlineIATA: "B6",
            flightNumberRaw: "B6 300",
            departureTime: "10:00",
            arrivalTime: "13:00",
            daysOfWeek: [weekday]
        )
        let ua = schedule(
            airlineIATA: "UA",
            flightNumberRaw: "UA 400",
            departureTime: "11:00",
            arrivalTime: "14:00",
            daysOfWeek: [weekday]
        )

        let loadTable: [String: Double] = [
            "DL-100": 0.50,
            "AA-200": 0.10,
            "UA-400": 0.10
            // B6-300 omitted → nil load
        ]
        let predictor: @Sendable (String, Int, Date) async -> Double? = { carrier, number, _ in
            return loadTable["\(carrier)-\(number)"]
        }

        let provider = MockScheduleProvider(
            airportLookups: [
                "JFK": [airport(id: "jfk-id", iata: "JFK")],
                "LAX": [airport(id: "lax-id", iata: "LAX")]
            ],
            schedulesToReturn: [dl, aa, b6, ua]
        )
        let service = SisterFlightService(flightService: provider, loadPredictor: predictor)

        let results = await service.sisterFlights(
            origin: origin,
            dest: dest,
            date: targetDate,
            currentFlight: nil
        )

        XCTAssertEqual(results.count, 4)
        XCTAssertEqual(results[0].carrier, "AA",
                       "Lowest load with earliest departure first.")
        XCTAssertEqual(results[1].carrier, "UA",
                       "Same load as AA but departs later — second.")
        XCTAssertEqual(results[2].carrier, "DL",
                       "Higher load than AA/UA — third.")
        XCTAssertEqual(results[3].carrier, "B6",
                       "Nil load is sorted last regardless of time.")
    }

    // MARK: - Test 5: predictedLoad nil when loadPredictor is nil

    func test_loadPredictorNil_predictedLoadAlwaysNil() async {
        let weekday = Calendar.current.component(.weekday, from: targetDate)

        let s = schedule(
            airlineIATA: "DL",
            flightNumberRaw: "DL 100",
            departureTime: "08:00",
            arrivalTime: "11:00",
            daysOfWeek: [weekday]
        )

        let provider = MockScheduleProvider(
            airportLookups: [
                "JFK": [airport(id: "jfk-id", iata: "JFK")],
                "LAX": [airport(id: "lax-id", iata: "LAX")]
            ],
            schedulesToReturn: [s]
        )
        let service = SisterFlightService(flightService: provider, loadPredictor: nil)

        let results = await service.sisterFlights(
            origin: origin,
            dest: dest,
            date: targetDate,
            currentFlight: nil
        )

        XCTAssertEqual(results.count, 1)
        XCTAssertNil(results.first?.predictedLoad,
                     "No predictor wired → predictedLoad must be nil.")
    }

    // MARK: - Helpers

    private func airport(id: String, iata: String) -> Airport {
        Airport(id: id, iata: iata, name: "\(iata) Airport")
    }

    /// Build a FlightSchedule with a synthetic Airline. Date range is
    /// wide enough (2020 → 2030) that any reasonable target date falls
    /// inside it; the only real filter is the daysOfWeek set.
    private func schedule(
        airlineIATA: String,
        flightNumberRaw: String,
        departureTime: String,
        arrivalTime: String,
        daysOfWeek: Set<Int>
    ) -> FlightSchedule {
        let airline = Airline(
            id: "airline-\(airlineIATA)",
            name: airlineIATA,
            iata: airlineIATA,
            logoFilename: "\(airlineIATA).png"
        )

        var utc = Calendar(identifier: .gregorian)
        utc.timeZone = TimeZone(identifier: "UTC")!
        let from = utc.date(from: DateComponents(year: 2020, month: 1, day: 1))!
        let to = utc.date(from: DateComponents(year: 2030, month: 12, day: 31))!

        return FlightSchedule(
            airline: airline,
            flightNumber: flightNumberRaw,
            aircraft: "738",
            aircraftId: "",
            departureTime: departureTime,
            arrivalTime: arrivalTime,
            dateFrom: from,
            dateTo: to,
            daysOfWeek: daysOfWeek,
            cabinClasses: .economy
        )
    }
}
