import XCTest
import SwiftData
@testable import Flights

/// Unit tests for `StandbyStatsService`.
///
/// All tests use an in-memory `ModelContainer` so they don't touch the
/// real SwiftData store or CloudKit. We seed `LoggedFlight` rows with
/// varied standby outcomes / carriers / routes / dates, then exercise
/// the public surface (`personalRate`, `recentOutcomes`) and assert on
/// the aggregate result.
@MainActor
final class StandbyStatsServiceTests: XCTestCase {

    private var container: ModelContainer!
    private var context: ModelContext!
    private var service: StandbyStatsService!

    override func setUpWithError() throws {
        try super.setUpWithError()
        let schema = Schema([LoggedFlight.self])
        let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
        container = try ModelContainer(for: schema, configurations: config)
        context = ModelContext(container)
        service = StandbyStatsService()
    }

    override func tearDownWithError() throws {
        service = nil
        context = nil
        container = nil
        try super.tearDownWithError()
    }

    // MARK: - Helpers

    /// Reference epoch we offset from so date ordering is deterministic
    /// regardless of wall-clock time when the test runs.
    private static let epoch = Date(timeIntervalSince1970: 1_700_000_000)

    private func date(_ dayOffset: Int) -> Date {
        Self.epoch.addingTimeInterval(TimeInterval(dayOffset) * 86_400)
    }

    @discardableResult
    private func insert(
        outcome: String?,
        carrierIATA: String? = "WN",
        carrierICAO: String? = "SWA",
        origin: String = "DAL",
        dest: String = "HOU",
        flightDate: Date? = nil
    ) -> LoggedFlight {
        let flight = LoggedFlight(
            flightDate: flightDate ?? date(0),
            carrierICAO: carrierICAO,
            carrierIATA: carrierIATA,
            departureIATA: origin,
            arrivalIATA: dest
        )
        flight.standbyOutcome = outcome
        context.insert(flight)
        return flight
    }

    // MARK: - personalRate

    /// Empty store should return the documented sentinel.
    func test_personalRate_emptyContext_returnsEmpty() {
        let rate = service.personalRate(carrier: nil, origin: nil, dest: nil, context: context)

        XCTAssertEqual(rate.attempts, 0)
        XCTAssertEqual(rate.made, 0)
        XCTAssertEqual(rate.bumped, 0)
        XCTAssertEqual(rate.confirmed, 0)
        XCTAssertEqual(rate.rate, 0)
    }

    /// 5 confirmed + 3 standby-made + 2 standby-bumped — sanity check
    /// the aggregate maths. attempts = made + bumped = 5; rate = 3/5.
    func test_personalRate_mixedOutcomes_returnsExpectedCounts() {
        for _ in 0..<5 { insert(outcome: "confirmed") }
        for _ in 0..<3 { insert(outcome: "standby-made") }
        for _ in 0..<2 { insert(outcome: "standby-bumped") }

        let rate = service.personalRate(carrier: nil, origin: nil, dest: nil, context: context)

        XCTAssertEqual(rate.attempts, 5, "attempts = standby-made + standby-bumped")
        XCTAssertEqual(rate.made, 3)
        XCTAssertEqual(rate.bumped, 2)
        XCTAssertEqual(rate.confirmed, 5)
        XCTAssertEqual(rate.rate, 0.6, accuracy: 0.0001)
    }

    /// Carrier filter must restrict to flights whose IATA *or* ICAO matches
    /// (the service deliberately checks both — caller doesn't know which
    /// code was stored).
    func test_personalRate_carrierFilter_onlyCountsMatchingCarrier() {
        // WN: 2 made, 1 bumped → 3 attempts, rate = 2/3
        insert(outcome: "standby-made",   carrierIATA: "WN", carrierICAO: "SWA")
        insert(outcome: "standby-made",   carrierIATA: "WN", carrierICAO: "SWA")
        insert(outcome: "standby-bumped", carrierIATA: "WN", carrierICAO: "SWA")
        // AA noise that must be excluded by the filter.
        insert(outcome: "standby-made",   carrierIATA: "AA", carrierICAO: "AAL")
        insert(outcome: "standby-bumped", carrierIATA: "AA", carrierICAO: "AAL")
        insert(outcome: "confirmed",      carrierIATA: "AA", carrierICAO: "AAL")

        let rate = service.personalRate(carrier: "WN", origin: nil, dest: nil, context: context)

        XCTAssertEqual(rate.attempts, 3)
        XCTAssertEqual(rate.made, 2)
        XCTAssertEqual(rate.bumped, 1)
        XCTAssertEqual(rate.confirmed, 0)
        XCTAssertEqual(rate.rate, 2.0 / 3.0, accuracy: 0.0001)
    }

    /// Origin filter only counts flights departing the requested airport.
    func test_personalRate_originFilter_onlyCountsMatchingDeparture() {
        insert(outcome: "standby-made",   origin: "DAL", dest: "HOU")
        insert(outcome: "standby-bumped", origin: "DAL", dest: "LAS")
        insert(outcome: "confirmed",      origin: "DAL", dest: "MDW")
        // Other-origin noise — must be excluded.
        insert(outcome: "standby-made",   origin: "HOU", dest: "DAL")
        insert(outcome: "confirmed",      origin: "AUS", dest: "DAL")

        let rate = service.personalRate(carrier: nil, origin: "DAL", dest: nil, context: context)

        XCTAssertEqual(rate.attempts, 2)
        XCTAssertEqual(rate.made, 1)
        XCTAssertEqual(rate.bumped, 1)
        XCTAssertEqual(rate.confirmed, 1)
        XCTAssertEqual(rate.rate, 0.5, accuracy: 0.0001)
    }

    /// Carrier + origin + dest filters combine with AND semantics. Only
    /// flights matching every condition should be counted.
    func test_personalRate_combinedFilters_useAndSemantics() {
        // Target combo: WN, DAL → HOU. 2 made, 1 bumped → rate 2/3.
        insert(outcome: "standby-made",   carrierIATA: "WN", origin: "DAL", dest: "HOU")
        insert(outcome: "standby-made",   carrierIATA: "WN", origin: "DAL", dest: "HOU")
        insert(outcome: "standby-bumped", carrierIATA: "WN", origin: "DAL", dest: "HOU")

        // Same carrier + origin, wrong dest.
        insert(outcome: "standby-made",   carrierIATA: "WN", origin: "DAL", dest: "LAS")
        // Same carrier + dest, wrong origin.
        insert(outcome: "standby-made",   carrierIATA: "WN", origin: "AUS", dest: "HOU")
        // Same route, wrong carrier.
        insert(outcome: "standby-made",   carrierIATA: "AA", carrierICAO: "AAL", origin: "DAL", dest: "HOU")

        let rate = service.personalRate(carrier: "WN", origin: "DAL", dest: "HOU", context: context)

        XCTAssertEqual(rate.attempts, 3)
        XCTAssertEqual(rate.made, 2)
        XCTAssertEqual(rate.bumped, 1)
        XCTAssertEqual(rate.confirmed, 0)
        XCTAssertEqual(rate.rate, 2.0 / 3.0, accuracy: 0.0001)
    }

    // MARK: - recentOutcomes

    /// recentOutcomes returns flights sorted by flightDate desc and
    /// honours the fetch limit. Flights without an outcome are excluded.
    func test_recentOutcomes_returnsMostRecentNByDateDescending() {
        // Insert 7 flights with outcomes across day offsets 0..6.
        // Day 6 is newest. Insert out of order to prove sort is by
        // flightDate (not insertion order).
        let outcomes = ["confirmed", "standby-made", "standby-bumped",
                        "confirmed", "standby-made", "standby-bumped", "confirmed"]
        let insertionOrder = [3, 0, 6, 2, 5, 1, 4]
        for day in insertionOrder {
            insert(outcome: outcomes[day], flightDate: date(day))
        }
        // Plus a flight with no outcome — must NOT appear.
        insert(outcome: nil, flightDate: date(99))

        let recent = service.recentOutcomes(limit: 5, context: context)

        XCTAssertEqual(recent.count, 5)
        let returnedDays = recent.map { $0.flightDate.timeIntervalSince(Self.epoch) / 86_400 }
            .map { Int($0.rounded()) }
        XCTAssertEqual(returnedDays, [6, 5, 4, 3, 2],
                       "Should be the 5 most recent by flightDate desc")
        XCTAssertTrue(recent.allSatisfy { $0.standbyOutcome != nil })
    }
}
