/**
 * Two seams that were each finished by a different workstream and could only be
 * closed here, and both of which fail *silently* — the build is green, the
 * types check, every unit test passes, and the visitor sees less than they
 * should.
 *
 * **The airports.** `engine/airports.ts` is a complete, tested kit, and
 * `cities/sf.ts` and `cities/socal.ts` author nine real fields between them. In
 * between sits one call. Until it existed the two detailed boards had *no*
 * airports at all — worse than before the kit landed, because the packs had
 * already dropped the runways-as-roads they used to fake them with. There is no
 * half-wired state that looks right, which is exactly why it is worth a test:
 * the failure is a missing thing, and a missing thing is what a screenshot is
 * worst at.
 *
 * **The card.** `flights.ts` resolves a registration and an ICAO type for every
 * live track, and `AircraftDetailInput` had nowhere to put them, so the two
 * facts that turn a dart into an aeroplane were parsed and thrown away one line
 * before a visitor could read them.
 */

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";

import SAN_FRANCISCO from "../../cities/sf.ts";
import SOCAL from "../../cities/socal.ts";
import { formatAircraftDetail, type AircraftDetailInput } from "../../ui/hud.ts";
import { runwayThresholds } from "../../engine/airports.ts";
import type { Airport, City, LatLng } from "../../engine/types.ts";

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const read = (rel: string) => readFileSync(path.join(ROOT, rel), "utf8");

const BOARDS: readonly (readonly [string, City])[] = [
  ["sf", SAN_FRANCISCO],
  ["socal", SOCAL],
];

// ---- The airport wiring ---------------------------------------------------

test("both detailed boards ship the airports their packs author", () => {
  for (const [id, city] of BOARDS) {
    const airports = city.airports ?? [];
    assert.ok(
      airports.length > 0,
      `${id} declares no airports. The kit is built and the fields are authored; ` +
        `this is the one line in the pack literal that publishes them.`,
    );
    for (const airport of airports) {
      assert.ok(airport.runways.length > 0, `${id}/${airport.id} has no runways`);
    }
  }
});

test("the scene builds them, from the field the packs fill in", () => {
  const scene = read("src/engine/scene.ts");
  assert.ok(
    scene.includes('import { createAirports } from "./airports.ts";'),
    "scene.ts no longer imports the airport kit",
  );
  /*
   * Two assertions rather than one literal line, because the call and the add
   * are no longer the same statement: the group is captured so the merged
   * board's detail LOD can hide it, and matching the old one-liner would fail
   * for a formatting change while still passing for a real regression like a
   * guard replacing `?? []`. The two facts are what the test was ever about.
   */
  assert.ok(
    /createAirports\(world, city\.airports \?\? \[\]\)/.test(scene),
    "scene.ts must build the airports for the city it was handed. `?? []` and not " +
      "a guard, because a board with no airfields is the normal case and must cost " +
      "nothing to express.",
  );
  assert.ok(
    // `ground`, not `scene`, since the relief ramp landed: a runway apron is a
    // statement about the terrain and has to be restated with it, or an
    // airfield floats off the ground the moment the exaggeration changes.
    /(?:scene|ground)\.add\(\s*airportGroup\s*\)/.test(scene) ||
      /(?:scene|ground)\.add\(createAirports\(/.test(scene),
    "scene.ts builds the airports but never adds them to the scene",
  );
});

test("the airport contract stays plain data, and off the package surface", () => {
  const types = read("src/engine/types.ts");
  assert.ok(
    !/from "\.\/airports\.ts"/.test(types),
    "types.ts must not reach for airports.ts even for a type. `src/index.ts` " +
      "reaches City, and barrel.test.ts reads the import graph as source — it " +
      "cannot tell an erased edge from a real one, and airports.ts imports three.js.",
  );
  // Structured-cloneable, because a City is posted to the terrain worker.
  for (const [id, city] of BOARDS) {
    assert.doesNotThrow(
      () => structuredClone(city.airports ?? []),
      `${id}'s airports are not sendable to the terrain worker`,
    );
  }
});

/**
 * The hazard the bridges workstream named: a runway drawn as a `Road` drapes
 * about thirteen metres above the terrain at board scale, while the kit lays a
 * runway flush on it. A board carrying both floats a dark stripe over every
 * runway, and nothing in the type system objects.
 */
test("no pack still draws a runway as a road", () => {
  const metresBetween = (a: LatLng, b: LatLng) => {
    const dLat = (a[0] - b[0]) * 111_320;
    const dLng = (a[1] - b[1]) * 111_320 * Math.cos((a[0] * Math.PI) / 180);
    return Math.hypot(dLat, dLng);
  };
  for (const [id, city] of BOARDS) {
    for (const airport of city.airports ?? []) {
      for (const runway of airport.runways) {
        const { low, high } = runwayThresholds(runway);
        for (const road of city.roads) {
          if (road.path.length !== 2) continue;
          const [a, b] = road.path as [LatLng, LatLng];
          const same =
            (metresBetween(a, low) < 400 && metresBetween(b, high) < 400) ||
            (metresBetween(a, high) < 400 && metresBetween(b, low) < 400);
          assert.ok(
            !same,
            `${id} draws ${airport.id}/${runway.id} as a Road as well as a runway — ` +
              `the ribbon will float over the pavement`,
          );
        }
      }
    }
  }
});

/**
 * `FIELD_LIFT` in airports.ts has to clear `LOD_HEIGHT_TOLERANCE` in terrain.ts,
 * because the terrain LOD collapses a flat patch to a quad that may sit that far
 * *above* the lattice it replaced. It cannot be a second hand-typed number: the
 * first time these two drifted apart, Van Nuys grew a wedge of bare ground
 * through the middle of the field and nothing failed.
 */
test("the field lift is derived from the terrain tolerance, not typed beside it", () => {
  assert.ok(
    /export const LOD_HEIGHT_TOLERANCE/.test(read("src/engine/terrain.ts")),
    "terrain.ts must export the tolerance the airport plate is measured against",
  );
  assert.ok(
    /const FIELD_LIFT = LOD_HEIGHT_TOLERANCE \+ /.test(read("src/engine/airports.ts")),
    "airports.ts must derive FIELD_LIFT from LOD_HEIGHT_TOLERANCE",
  );
});

// ---- The aircraft card ----------------------------------------------------

const LIVE: AircraftDetailInput = {
  id: "a4d8f2",
  callsign: "UAL1234",
  lat: 37.6213,
  lng: -122.379,
  altitude: 1_524,
  heading: 298.6,
  type: "B739",
  registration: "N68834",
  groundSpeedKt: 212,
  verticalRateFpm: -1_408,
  synthetic: false,
  attribution: "ADS-B data © adsb.lol contributors, ODbL",
};

const rowsOf = (input: AircraftDetailInput) =>
  Object.fromEntries(formatAircraftDetail(input).rows.map((r) => [r.label, r.value]));

test("an anonymous visitor reads what the aeroplane IS, first", () => {
  const view = formatAircraftDetail(LIVE);
  assert.equal(view.rows[0]?.label, "Aircraft");
  assert.equal(view.rows[0]?.value, "B739 · N68834");
  // And the identity a receiver actually heard is still on the card.
  assert.equal(view.title, "UAL1234");
  assert.equal(view.subtitle, "Mode S A4D8F2");
});

test("the card carries the whole of what the feed said", () => {
  const rows = rowsOf(LIVE);
  assert.equal(rows["Altitude"], "5,000 ft · 1,524 m");
  assert.equal(rows["Heading"], "299° WNW");
  assert.equal(rows["Ground speed"], "212 kt");
  assert.equal(rows["Climb"], "−1,408 ft/min");
  assert.equal(rows["Position"], "37.621° N · 122.379° W");
});

test("a rate inside the noise band is level flight, not a manoeuvre", () => {
  assert.equal(rowsOf({ ...LIVE, verticalRateFpm: 64 })["Climb"], "Level");
  assert.equal(rowsOf({ ...LIVE, verticalRateFpm: 0 })["Climb"], "Level");
  assert.equal(rowsOf({ ...LIVE, verticalRateFpm: 2_240 })["Climb"], "+2,240 ft/min");
});

test("a zero is a fact and an absence is not", () => {
  // Parked, and saying so.
  assert.equal(rowsOf({ ...LIVE, groundSpeedKt: 0 })["Ground speed"], "0 kt");
  // The simulator, and every server one version behind: the rows vanish rather
  // than printing "unknown", which is what makes this safe to ship un-gated.
  const simulated = rowsOf({
    ...LIVE,
    type: null,
    registration: null,
    groundSpeedKt: null,
    verticalRateFpm: null,
    synthetic: true,
  });
  assert.equal(simulated["Aircraft"], undefined);
  assert.equal(simulated["Ground speed"], undefined);
  assert.equal(simulated["Climb"], undefined);
  assert.equal(simulated["Altitude"], "5,000 ft · 1,524 m");
});

test("one half of an airframe is still worth printing", () => {
  assert.equal(rowsOf({ ...LIVE, registration: null })["Aircraft"], "B739");
  assert.equal(rowsOf({ ...LIVE, type: null })["Aircraft"], "N68834");
  assert.equal(rowsOf({ ...LIVE, type: "  ", registration: "" })["Aircraft"], undefined);
});

test("main.ts hands the card all four, ungated", () => {
  const main = read("src/main.ts");
  const from = main.indexOf("function showAircraftDetail");
  assert.ok(from > 0, "showAircraftDetail has been renamed");
  const body = main.slice(from, main.indexOf("\n}", from));
  for (const field of ["registration", "type", "groundSpeedKt", "verticalRateFpm"]) {
    assert.ok(
      new RegExp(`${field}: resolved\\.${field},`).test(body),
      `showAircraftDetail drops ${field} on the floor`,
    );
  }
  assert.ok(
    !/\baccess\./.test(body),
    "owner decision 2: the flight card is not gated on an account",
  );
});

// A type-level check, in the only place that can make one: every field the card
// reads must exist on the record `flights.ts` resolves.
test("the card's inputs are the detail record's outputs", async () => {
  const { aircraftDetail } = await import("../../engine/flights.ts");
  const detail = aircraftDetail(
    { id: "a4d8f2", lat: 37.62, lng: -122.38, altitude: 1_524, heading: 298.6 },
    { observed: true, icao24: "a4d8f2", type: "B739", registration: "N68834" },
  );
  const card: AircraftDetailInput = {
    id: detail.icao24 ?? detail.id,
    callsign: detail.callsign,
    lat: detail.lat,
    lng: detail.lng,
    altitude: detail.altitudeM,
    heading: detail.headingDeg,
    type: detail.type,
    registration: detail.registration,
    groundSpeedKt: detail.groundSpeedKt,
    verticalRateFpm: detail.verticalRateFpm,
    synthetic: !detail.observed,
    attribution: detail.attribution.join(" · "),
  };
  assert.equal(formatAircraftDetail(card).rows[0]?.value, "B739 · N68834");
});

// ---- One thing the packs must agree about ---------------------------------

test("every authored field sits inside the board that draws it", () => {
  const inside = (city: City, [lat, lng]: LatLng) =>
    lat >= city.bounds.minLat &&
    lat <= city.bounds.maxLat &&
    lng >= city.bounds.minLng &&
    lng <= city.bounds.maxLng;
  for (const [id, city] of BOARDS) {
    for (const airport of (city.airports ?? []) as Airport[]) {
      assert.ok(inside(city, [airport.lat, airport.lng]), `${id}/${airport.id} is off the board`);
      for (const point of airport.field ?? []) {
        assert.ok(inside(city, point), `${id}/${airport.id}'s fence leaves the board`);
      }
    }
  }
});
