import XCTest @testable import Flights /// Unit tests for `EquipmentSwapService`. /// /// These exercise the bundled `aircraft_seats.json` catalog and the public /// `check(scheduledEquipmentIATA:liveEquipmentICAO:)` entry point. The test /// target is hosted by Flights.app, so `Bundle.main` resolves to the host /// bundle and the catalog loads normally. /// /// NOTE: The current catalog is a generic one-size-fits-carrier map. After /// Phase 2 the schema becomes per-carrier-per-IATA; the IATA codes used /// below (`73H`, `7M8`, `73G`, `320`) and ICAO codes (`B738`, `B737`) will /// remain valid lookups, but these tests will need to be revisited then. /// /// Seat values referenced (from `Flights/Resources/aircraft_seats.json` defaults): /// 73G → 137 (B737-700) /// 73H → 172 (B737-800) /// 7M8 → 172 (B737-MAX 8) /// 320 → 150 (A320) /// ICAO B738 → IATA 73H /// ICAO B737 → IATA 73G final class EquipmentSwapServiceTests: XCTestCase { // A fresh service per test — the actor caches the catalog after first // load, but we want each case to be independent of ordering. private func makeService() -> EquipmentSwapService { EquipmentSwapService() } // MARK: - 1. Both nil → nil func test_returnsNil_whenBothScheduledAndLiveAreNil() async { let service = makeService() let result = await service.check( scheduledEquipmentIATA: nil, liveEquipmentICAO: nil ) XCTAssertNil(result, "Expected nil when there is nothing to compare.") } // MARK: - 2. Only live provided → nil (no baseline) func test_returnsNil_whenOnlyLiveICAOProvided() async { let service = makeService() let result = await service.check( scheduledEquipmentIATA: nil, liveEquipmentICAO: "B738" ) XCTAssertNil( result, "Without a scheduled baseline there is no meaningful comparison to surface." ) } // MARK: - 3. Same equipment (live ICAO maps to scheduled IATA) func test_returnsNoneSeverity_whenScheduledAndLiveMatch() async { let service = makeService() // Scheduled 73H (B737-800, 175) vs live B738 → 73H (175). Identical. let result = await service.check( scheduledEquipmentIATA: "73H", liveEquipmentICAO: "B738" ) guard let result else { XCTFail("Expected a non-nil result for a known equipment pair.") return } XCTAssertEqual(result.seatDelta, 0, "Same aircraft should produce a zero seat delta.") XCTAssertEqual(result.severity, .none, "Zero delta must read as .none severity.") XCTAssertEqual(result.scheduledSeats, 172) XCTAssertEqual(result.liveSeats, 172) XCTAssertTrue( result.summary.contains("Same equipment today"), "Summary should reflect the unchanged equipment. Got: \(result.summary)" ) } // MARK: - 4. |delta| in 1...15 → .minor func test_returnsMinorSeverity_whenDeltaIsSmall() async { let service = makeService() // Scheduled 320 (A320, 150) vs live B737 → 73G (137). |delta| = 13. let result = await service.check( scheduledEquipmentIATA: "320", liveEquipmentICAO: "B737" ) guard let result else { XCTFail("Expected a non-nil result for a known equipment pair.") return } XCTAssertEqual(result.scheduledSeats, 150) XCTAssertEqual(result.liveSeats, 137) XCTAssertEqual(result.seatDelta, -13, "Live aircraft has 13 fewer seats than scheduled.") XCTAssertEqual(result.severity, .minor, "A 13-seat change must be classified .minor (1...15).") XCTAssertTrue( result.summary.contains("Smaller bird today"), "Negative delta summary should call out the smaller aircraft. Got: \(result.summary)" ) } // MARK: - 5. |delta| > 15 → .significant func test_returnsSignificantSeverity_whenDeltaIsLarge() async { let service = makeService() // Scheduled 73G (B737-700, 137) vs live B738 → 73H (172). |delta| = 35. let result = await service.check( scheduledEquipmentIATA: "73G", liveEquipmentICAO: "B738" ) guard let result else { XCTFail("Expected a non-nil result for a known equipment pair.") return } XCTAssertEqual(result.scheduledSeats, 137) XCTAssertEqual(result.liveSeats, 172) XCTAssertEqual(result.seatDelta, 35, "Live aircraft has 35 more seats than scheduled.") XCTAssertEqual(result.severity, .significant, "A 35-seat swing exceeds 15 → .significant.") XCTAssertTrue( result.summary.contains("Bigger bird today"), "Positive delta summary should call out the larger aircraft. Got: \(result.summary)" ) } // MARK: - 6. ICAO "B738" maps to IATA "73H" (no-swap path through ICAO mapping) func test_icaoB738_mapsTo_iata73H_asNoSwap() async { let service = makeService() // Scheduled was the 73H; live equipment reports as ICAO B738 — these // are the same airframe family. Catalog mapping should collapse them. let result = await service.check( scheduledEquipmentIATA: "73H", liveEquipmentICAO: "B738" ) guard let result else { XCTFail("Expected a non-nil result; ICAO B738 should map to IATA 73H.") return } XCTAssertEqual( result.liveSeats, result.scheduledSeats, "B738 → 73H mapping must produce equal scheduled/live seat counts." ) XCTAssertEqual(result.seatDelta, 0) XCTAssertEqual(result.severity, .none) XCTAssertEqual( result.liveName, result.scheduledName, "The resolved live aircraft name should match the scheduled name (both 73H)." ) } // MARK: - 7. Unknown live ICAO → liveSeats nil + "live equipment unknown" summary func test_unknownLiveICAO_returnsNilLiveSeats_andUnknownSummary() async { let service = makeService() // "ZZZZ" is not in the ICAO map and is not a valid IATA fallback. let result = await service.check( scheduledEquipmentIATA: "73H", liveEquipmentICAO: "ZZZZ" ) guard let result else { XCTFail("Expected a non-nil result — we still have a scheduled baseline.") return } XCTAssertEqual(result.scheduledSeats, 172, "Scheduled 73H still resolves to 172 seats.") XCTAssertNil(result.liveSeats, "Unknown ICAO must leave liveSeats nil.") XCTAssertNil(result.liveName, "Unknown ICAO must leave liveName nil.") XCTAssertNil(result.seatDelta, "Without a live entry there is no delta to compute.") XCTAssertEqual(result.severity, .none, "Missing live data falls back to .none severity.") XCTAssertTrue( result.summary.contains("live equipment unknown"), "Summary should explicitly say the live equipment is unknown. Got: \(result.summary)" ) } }