import XCTest
@testable import Flights

/// Tests for ``FlightAwareScheduleClient``. The two pure-parser entry
/// points (``parseIdents`` and ``extractTrackpollBlob``) are exercised
/// directly against fixture HTML captured from a live request — this
/// catches FlightAware schema drift the moment it happens (route.rvt or
/// trackpoll layout changes) instead of finding out via empty search
/// results in production.
///
/// Fixtures live next to this file under `Fixtures/`. They're real
/// HTML pages saved verbatim from FlightAware, not synthetic markup,
/// so the tests assert against the actual shapes the parser sees.
final class FlightAwareScheduleClientTests: XCTestCase {

    // MARK: - Fixture loading

    /// Reads a file from the `Fixtures/` directory sibling to this test
    /// source file. Avoids needing the test target's pbxproj to declare
    /// a Resources phase — `#filePath` resolves to the real source path
    /// at test-run time.
    private func loadFixture(_ name: String, file: StaticString = #filePath) throws -> String {
        let here = URL(fileURLWithPath: String(describing: file))
        let url = here.deletingLastPathComponent()
            .appendingPathComponent("Fixtures")
            .appendingPathComponent(name)
        return try String(contentsOf: url, encoding: .utf8)
    }

    // MARK: - parseIdents

    func test_parseIdents_extractsFlightIdent_fromRouteAnalysisPage() throws {
        let html = try loadFixture("DFW_EHAM_route.html")
        let idents = FlightAwareScheduleClient.parseIdents(routeHTML: html)
        XCTAssertFalse(idents.isEmpty,
            "Should find at least one operating ident on DFW->AMS route page.")
        XCTAssertTrue(idents.contains("AAL220"),
            "AAL220 (AA daily 777-200 DFW->AMS) must surface; got \(idents)")
    }

    func test_parseIdents_dedupesRepeatedIdents() throws {
        let html = try loadFixture("DFW_EHAM_route.html")
        let idents = FlightAwareScheduleClient.parseIdents(routeHTML: html)
        XCTAssertEqual(idents.count, Set(idents).count,
            "Returned idents should be deduped; got \(idents)")
    }

    func test_parseIdents_returnsEmpty_whenNoRoutesPresent() {
        let empty = """
        <html><body><table>
        <tr><th>Filed Time</th><th>Ident</th></tr>
        <tr><td>No data</td></tr>
        </table></body></html>
        """
        XCTAssertEqual(
            FlightAwareScheduleClient.parseIdents(routeHTML: empty),
            [],
            "Page with no flight rows should produce an empty list, not crash."
        )
    }

    // MARK: - extractTrackpollBlob

    func test_extractTrackpollBlob_returnsParseableJSON() throws {
        let html = try loadFixture("AAL220_trackpoll.html")
        guard let blob = FlightAwareScheduleClient.extractTrackpollBlob(from: html) else {
            XCTFail("Should extract trackpollBootstrap from AAL220 page")
            return
        }
        XCTAssertTrue(blob.hasPrefix("{") && blob.hasSuffix("}"),
            "Extracted blob should be a JSON object literal")
        // Round-trip through JSONDecoder to confirm shape.
        XCTAssertNoThrow(
            try JSONDecoder().decode(TrackpollBootstrap.self, from: Data(blob.utf8)),
            "Extracted JSON should decode against TrackpollBootstrap schema"
        )
    }

    func test_extractTrackpollBlob_returnsNil_whenMarkerMissing() {
        let html = "<html><body>no script here</body></html>"
        XCTAssertNil(FlightAwareScheduleClient.extractTrackpollBlob(from: html))
    }

    func test_extractTrackpollBlob_isStringContentAware() {
        // A closing brace inside a string literal must NOT terminate the scan.
        let html = #"""
        <script>var trackpollBootstrap = {"a":"} not the end","b":1};</script>
        """#
        let blob = FlightAwareScheduleClient.extractTrackpollBlob(from: html)
        XCTAssertEqual(
            blob,
            #"{"a":"} not the end","b":1}"#,
            "Braces inside JSON strings must not break the brace-balance scan."
        )
    }

    // MARK: - ident decomposition

    func test_identCarrierICAO_stripsTrailingDigits() {
        XCTAssertEqual(FlightAwareScheduleClient.identCarrierICAO("AAL220"), "AAL")
        XCTAssertEqual(FlightAwareScheduleClient.identCarrierICAO("BAW296"), "BAW")
        XCTAssertEqual(FlightAwareScheduleClient.identCarrierICAO("SWA1"), "SWA")
    }

    func test_identFlightNumber_extractsTrailingDigits() {
        XCTAssertEqual(FlightAwareScheduleClient.identFlightNumber("AAL220"), 220)
        XCTAssertEqual(FlightAwareScheduleClient.identFlightNumber("BAW296"), 296)
        XCTAssertEqual(FlightAwareScheduleClient.identFlightNumber("SWA1"), 1)
    }

    func test_airlineIATA_mapsKnownAndReturnsNilForUnknown() {
        XCTAssertEqual(FlightAwareScheduleClient.airlineIATA(forICAO: "AAL"), "AA")
        XCTAssertEqual(FlightAwareScheduleClient.airlineIATA(forICAO: "KLM"), "KL")
        XCTAssertEqual(FlightAwareScheduleClient.airlineIATA(forICAO: "BAW"), "BA")
        XCTAssertNil(FlightAwareScheduleClient.airlineIATA(forICAO: "ZZZ"),
            "Unknown ICAO should return nil so caller can fall back to the raw prefix.")
    }

    // MARK: - End-to-end against fixture

    func test_endToEnd_AAL220_trackpoll_decodesToScheduledLeg() throws {
        let html = try loadFixture("AAL220_trackpoll.html")
        guard let blob = FlightAwareScheduleClient.extractTrackpollBlob(from: html) else {
            XCTFail("missing trackpoll blob")
            return
        }
        let decoded = try JSONDecoder().decode(TrackpollBootstrap.self, from: Data(blob.utf8))

        // The fixture was captured on 2026-06-05; it should contain a DFW->AMS
        // leg with a B772 aircraft. We don't assert exact timestamps because
        // future updates to the fixture (re-capture) will rotate the dates.
        let dfwAmsLegs = decoded.flights.values
            .flatMap { $0.activityLog.flights }
            .filter { $0.origin.iata == "DFW" && $0.destination.iata == "AMS" }
        XCTAssertFalse(dfwAmsLegs.isEmpty,
            "AAL220 fixture should contain at least one DFW->AMS leg")
        XCTAssertTrue(
            dfwAmsLegs.contains { $0.aircraftType == "B772" },
            "AAL220 DFW->AMS legs should be operated by B772 per the captured fixture"
        )
    }
}
