/**
 * The ladder: one descent over three boards, and the rule that hands the camera
 * from one to the next.
 *
 * Two things are being protected here and only one of them is the arithmetic.
 *
 * The first is **containment**. The ladder is a new view over the packs, never a
 * replacement for them: twenty-nine capture guards in `scripts/brand-assets` aim
 * at `#chapters .chapter` by index and assert the button's short label, and every
 * still on lumbridgecorp.com is shot that way. So this file asserts that no rung
 * carries a short label, a number or an id the pack did not, and that the packs'
 * own chapter arrays are untouched — a ladder that could only be built by editing
 * a pack is a ladder that re-shoots the marketing site.
 *
 * The second is **oscillation**. `handover` is shipped pure and tested here
 * before it is ever allowed to fire on a wheel notch, because a camera parked at
 * a threshold without a hysteresis band tears down and rebuilds a board on every
 * event, and that is not a bug you find by reading the function.
 */

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
  activeRung,
  BOARD_REGION,
  buildLadder,
  chapterAltitudeMetres,
  chapterStandoffMetres,
  DEMOTE,
  handover,
  handoverStandoff,
  PROMOTE,
  placesRows,
  REGION_LABELS,
  type HandoverBoard,
} from "../engine/ladder.ts";
import CALIFORNIA from "../cities/california.ts";
import SAN_FRANCISCO from "../cities/sf.ts";
import SOCAL from "../cities/socal.ts";

const PACKS = [
  { id: "california", city: CALIFORNIA },
  { id: "sf", city: SAN_FRANCISCO },
  { id: "socal", city: SOCAL },
];

const LADDER = buildLadder(PACKS);

/** The three boards as `handover` sees them. Metres per unit is `World`'s own arithmetic. */
const BOARDS: HandoverBoard[] = PACKS.map((pack) => ({
  id: pack.id,
  bounds: pack.city.bounds,
  metresPerUnit: 111_320 / pack.city.latScale,
}));

describe("the ladder is built from the packs and changes none of them", () => {
  it("folds the two California doors and keeps every other chapter", () => {
    const authored =
      CALIFORNIA.chapters.length + SAN_FRANCISCO.chapters.length + SOCAL.chapters.length;
    assert.equal(authored, 26, "the three packs author twenty-six chapters between them");
    assert.equal(LADDER.length, 24, "the two California doors fold onto the metro rungs");

    // The doors are gone as rungs of their own and present as an origin on the
    // two rungs they open. They are not camera poses — `main.ts` matches them
    // against `CALIFORNIA_DESTINATIONS` and calls `switchCity` — so a rung for
    // each would be a duplicate of the board it opens.
    assert.equal(LADDER.some((rung) => rung.key === "california:los-angeles"), false);
    assert.equal(LADDER.some((rung) => rung.key === "california:san-francisco"), false);
    const southland = LADDER.find((rung) => rung.key === "socal:all");
    const bay = LADDER.find((rung) => rung.key === "sf:all");
    assert.deepEqual(southland?.doorFrom, { board: "california", id: "los-angeles" });
    assert.deepEqual(bay?.doorFrom, { board: "california", id: "san-francisco" });
  });

  it("carries the packs' own ids, numbers and short labels unchanged", () => {
    for (const pack of PACKS) {
      for (const chapter of pack.city.chapters) {
        const rung = LADDER.find((candidate) => candidate.key === `${pack.id}:${chapter.id}`);
        // The two folded doors are the only chapters without a rung.
        if (rung === undefined) {
          assert.ok(
            chapter.id === "los-angeles" || chapter.id === "san-francisco",
            `${pack.id}:${chapter.id} has no rung and is not a door`,
          );
          continue;
        }
        assert.equal(rung.id, chapter.id);
        assert.equal(rung.number, chapter.number);
        assert.equal(rung.shortLabel, chapter.shortLabel);
        assert.equal(rung.description, chapter.description);
      }
    }
  });

  it("resolves the label collisions in the ladder rather than in a pack", () => {
    // Both metro packs open on a chapter labelled "The Whole Board", and both
    // call a chapter "The Valley". Renaming either in its pack would move a
    // string twenty-nine index-aimed capture guards assert on.
    const labels = LADDER.map((rung) => rung.label);
    assert.equal(new Set(labels).size, labels.length, `duplicate ladder label in ${labels}`);
    assert.equal(SAN_FRANCISCO.chapters[0]?.shortLabel, "Whole Board");
    assert.equal(SOCAL.chapters[0]?.shortLabel, "Whole Board");
    assert.equal(LADDER.find((r) => r.key === "socal:all")?.label, "The Southland");
    assert.equal(LADDER.find((r) => r.key === "sf:all")?.label, "The Bay Area");
  });

  it("puts every rung under a region that has a heading", () => {
    for (const rung of LADDER) {
      assert.equal(rung.region, BOARD_REGION[rung.board]);
      assert.equal(typeof REGION_LABELS[rung.region], "string");
    }
  });
});

describe("stand-off, and not altitude, is the ladder's coordinate", () => {
  it("sorts strictly descending by stand-off", () => {
    for (let i = 1; i < LADDER.length; i++) {
      const above = LADDER[i - 1];
      const below = LADDER[i];
      assert.ok(above && below);
      assert.ok(
        above.standoffM >= below.standoffM,
        `${above.key} (${above.standoffM}) sorts above ${below.key} (${below.standoffM})`,
      );
    }
  });

  it("separates the three boards by stand-off and interleaves them by altitude", () => {
    const on = (board: string) => LADDER.filter((rung) => rung.board === board);
    const california = on("california");
    const socal = on("socal");
    const sf = on("sf");

    // The measured claim the coordinate choice rests on: by stand-off the three
    // boards form three clean bands.
    const lowestCalifornia = Math.min(...california.map((r) => r.standoffM));
    const highestSocal = Math.max(...socal.map((r) => r.standoffM));
    const highestSf = Math.max(...sf.map((r) => r.standoffM));
    assert.ok(lowestCalifornia > highestSocal, "California bottoms out above the Southland");
    assert.ok(highestSocal > highestSf, "the Southland's whole board stands off further than the Bay's");
    assert.ok(Math.round(lowestCalifornia / 1000) === 242, `California bottoms at 242 km, got ${lowestCalifornia}`);
    assert.ok(Math.round(highestSocal / 1000) === 109, `the Southland tops at 108.8 km, got ${highestSocal}`);
    assert.ok(Math.round(highestSf / 1000) === 71, `the Bay tops at 71.2 km, got ${highestSf}`);

    // And by altitude they do not: a SoCal chapter sits inside California's range.
    const californiaAltitudes = california.map((r) => r.altitudeM);
    const lowestCaliforniaAltitude = Math.min(...californiaAltitudes);
    const highestCaliforniaAltitude = Math.max(...californiaAltitudes);
    const interleaved = [...socal, ...sf].filter(
      (rung) =>
        rung.altitudeM > lowestCaliforniaAltitude && rung.altitudeM < highestCaliforniaAltitude,
    );
    assert.ok(
      interleaved.length > 0,
      "an altitude ladder would interleave the boards; that is why stand-off is the coordinate",
    );
  });

  it("derives both numbers from the authored focus block alone", () => {
    const chapter = SAN_FRANCISCO.chapters[0];
    assert.ok(chapter);
    const expectedStandoff =
      Math.hypot(chapter.focus.distance, chapter.focus.height) * (111_320 / SAN_FRANCISCO.latScale);
    assert.equal(chapterStandoffMetres(chapter, SAN_FRANCISCO.latScale), expectedStandoff);
    // The vertical exaggeration divides back out, exactly as
    // `SceneHandle.cameraAltitudeMetres` does through `World.unitsToMetres`.
    assert.equal(
      chapterAltitudeMetres(chapter, SAN_FRANCISCO.latScale, SAN_FRANCISCO.verticalExaggeration),
      (chapter.focus.height * (111_320 / SAN_FRANCISCO.latScale)) /
        SAN_FRANCISCO.verticalExaggeration,
    );
  });
});

describe("the printed list groups what the array interleaves", () => {
  const rows = placesRows(LADDER);

  it("holds exactly the same rungs", () => {
    assert.equal(rows.length, LADDER.length);
    assert.deepEqual(
      [...rows].map((r) => r.key).sort(),
      [...LADDER].map((r) => r.key).sort(),
    );
  });

  it("prints three region runs rather than nine", () => {
    const runs: string[] = [];
    for (const rung of rows) if (runs[runs.length - 1] !== rung.region) runs.push(rung.region);
    assert.deepEqual(runs, ["state", "southland", "bay"]);

    // The thing grouping exists to avoid: a globally sorted list changes region
    // far more often than it has regions.
    const interleavedRuns: string[] = [];
    for (const rung of LADDER) {
      if (interleavedRuns[interleavedRuns.length - 1] !== rung.region) {
        interleavedRuns.push(rung.region);
      }
    }
    assert.ok(
      interleavedRuns.length > 5,
      `a global sort changes region ${interleavedRuns.length} times; grouping is why the list is readable`,
    );
  });

  it("still descends inside every group", () => {
    for (let i = 1; i < rows.length; i++) {
      const above = rows[i - 1];
      const below = rows[i];
      assert.ok(above && below);
      if (above.region !== below.region) continue;
      assert.ok(above.standoffM >= below.standoffM, `${above.key} then ${below.key}`);
    }
  });
});

describe("the lit rung is an altimeter, not a menu", () => {
  it("keeps the rung you flew to lit when the camera is on its pose", () => {
    const soma = LADDER.find((rung) => rung.key === "sf:soma");
    assert.ok(soma);
    assert.equal(activeRung(LADDER, "sf", soma.standoffM, "soma")?.key, "sf:soma");
  });

  it("does not hop between the four Bay rungs that sit inside 12% of each other", () => {
    const eastBay = LADDER.find((rung) => rung.key === "sf:east-bay");
    const sanJose = LADDER.find((rung) => rung.key === "sf:south-bay");
    assert.ok(eastBay && sanJose);
    assert.ok(
      Math.abs(eastBay.standoffM - sanJose.standoffM) / sanJose.standoffM < 0.12,
      "these two are the near-collision the tolerance exists for",
    );
    // Parked on the East Bay's own pose, the East Bay stays lit even though San
    // Jose's number is only two per cent away.
    assert.equal(activeRung(LADDER, "sf", eastBay.standoffM, "east-bay")?.key, "sf:east-bay");
  });

  it("follows the camera once it has left that pose", () => {
    const whole = LADDER.find((rung) => rung.key === "sf:all");
    const missionBay = LADDER.find((rung) => rung.key === "sf:mission-bay");
    assert.ok(whole && missionBay);
    // Still nominally "on" the whole-board chapter, but flown down to street
    // level: the list has to say where the camera is.
    assert.equal(activeRung(LADDER, "sf", missionBay.standoffM, "all")?.key, "sf:mission-bay");
  });

  it("only ever lights a rung on the resident board", () => {
    for (const standoff of [3_000, 30_000, 300_000, 3_000_000]) {
      assert.equal(activeRung(LADDER, "socal", standoff, null)?.board, "socal");
    }
  });

  it("answers null for a board with no rungs", () => {
    assert.equal(activeRung(LADDER, "paris", 10_000, null), null);
  });
});

describe("handover does not oscillate", () => {
  /** Downtown San Francisco: inside the sf rectangle, inside California's. */
  const SF_TARGET = { lat: 37.7749, lng: -122.4194 };
  /** Downtown Los Angeles: inside the socal rectangle, inside California's. */
  const LA_TARGET = { lat: 34.05, lng: -118.24 };
  /** Fresno: inside California and inside neither metro. */
  const FRESNO = { lat: 36.74, lng: -119.78 };

  const query = (over: {
    current: string;
    standoffM: number;
    lat: number;
    lng: number;
    dragging?: boolean;
  }) => handover({ boards: BOARDS, ...over });

  it("promotes below the authored threshold and stays put above it", () => {
    const sf = handoverStandoff("sf");
    assert.equal(sf, 71_200);
    assert.equal(
      query({ current: "california", standoffM: sf * PROMOTE * 0.99, ...SF_TARGET }),
      "sf",
    );
    assert.equal(query({ current: "california", standoffM: sf * PROMOTE * 1.01, ...SF_TARGET }), null);
  });

  it("promotes at most once and demotes at most once over a full sweep", () => {
    const sf = handoverStandoff("sf");
    let current = "california";
    let promotions = 0;
    let demotions = 0;

    const step = (standoffM: number) => {
      const next = query({ current, standoffM, ...SF_TARGET });
      if (next === null) return;
      if (next === "sf") promotions++;
      else demotions++;
      current = next;
    };

    // Down through the threshold in one per cent steps, and back up again.
    for (let f = 1.4; f >= 0.5; f -= 0.01) step(sf * f);
    assert.equal(current, "sf", "the sweep down ends on the finer board");
    for (let f = 0.5; f <= 1.4; f += 0.01) step(sf * f);

    assert.equal(promotions, 1, "one promotion over the whole sweep");
    assert.equal(demotions, 1, "one demotion over the whole sweep");
    assert.equal(current, "california", "the sweep up ends back on the state");
  });

  it("has a dead band between the promote and demote thresholds", () => {
    const sf = handoverStandoff("sf");
    // Inside the band, neither direction fires, from either side of it.
    for (const f of [PROMOTE + 0.01, 1.0, DEMOTE - 0.01]) {
      assert.equal(query({ current: "california", standoffM: sf * f, ...SF_TARGET }), null);
      assert.equal(query({ current: "sf", standoffM: sf * f, ...SF_TARGET }), null);
    }
  });

  it("never fires while a drag is in flight", () => {
    const sf = handoverStandoff("sf");
    assert.equal(
      query({ current: "california", standoffM: sf * 0.2, dragging: true, ...SF_TARGET }),
      null,
    );
    assert.equal(
      query({ current: "sf", standoffM: sf * 10, dragging: true, ...SF_TARGET }),
      null,
    );
  });

  it("refuses to promote where there is no finer board — 97.4% of the state", () => {
    for (const standoffM of [200_000, 60_000, 20_000, 4_000]) {
      assert.equal(query({ current: "california", standoffM, ...FRESNO }), null);
    }
  });

  it("promotes into the Southland over Los Angeles and never into the Bay", () => {
    assert.equal(
      query({ current: "california", standoffM: handoverStandoff("socal") * 0.5, ...LA_TARGET }),
      "socal",
    );
    // The two metro rectangles do not intersect, so the candidate set is never
    // larger than one — the assertion is that the *other* one is not a candidate.
    assert.equal(query({ current: "sf", standoffM: 10_000, ...LA_TARGET }), null);
  });

  it("demotes to California and never straight into the other metro", () => {
    assert.equal(
      query({ current: "sf", standoffM: handoverStandoff("sf") * DEMOTE * 1.01, ...SF_TARGET }),
      "california",
    );
    assert.equal(handoverStandoff("california"), Number.POSITIVE_INFINITY);
    // California hands over to nothing, so no stand-off demotes off it.
    assert.equal(query({ current: "california", standoffM: 5_000_000, ...SF_TARGET }), null);
  });

  it("refuses a nonsense stand-off rather than guessing", () => {
    for (const standoffM of [0, -1, Number.NaN, Number.POSITIVE_INFINITY]) {
      assert.equal(query({ current: "california", standoffM, ...SF_TARGET }), null);
    }
  });
});
