/**
 * The aeroplane glyph has a floor AND a ceiling, and the ceiling is the newer half.
 *
 * The floor is a screen-space rule: never smaller than legible, because position
 * and heading are what a reader wants off a map and neither survives half a
 * pixel. It scales by the distance to *that aircraft*, which is the same thing
 * as "how far the camera has zoomed" only when everything in frame is equally
 * far away. On a whole-board pose it is. Beside a landmark it is not — at the
 * Golden Gate the bridge is a couple of units from the camera and the traffic
 * over the Pacific is a couple of thousand, so the floor fired hard on the
 * aeroplane and not at all on the bridge, and an airliner was drawn about two
 * and a half times the length of the main span.
 *
 * These tests pin both ends: that the board still gets a symbol it can read, and
 * that no camera anywhere can produce a state-sized aeroplane again.
 */

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { glyphScale } from "../../engine/flights.ts";

/** The field of view the city scenes actually use, near enough for a ratio. */
const FOV = 50;

describe("glyph scale", () => {
  it("never shrinks an aeroplane below the size it was drawn at", () => {
    for (const d of [0.5, 5, 20, 34]) {
      assert.ok(glyphScale(d, FOV) >= 1, `close camera at ${d} must not shrink the glyph`);
    }
  });

  it("still grows the glyph across the whole-board range, where the floor is the point", () => {
    const near = glyphScale(200, FOV);
    const far = glyphScale(400, FOV);
    assert.ok(near > 1, "a board-distance aeroplane must be enlarged to stay readable");
    assert.ok(far > near, "the floor must keep tracking distance until the ceiling binds");
  });

  it("stops growing at all, however far the aircraft is", () => {
    const capped = glyphScale(2280, FOV);
    assert.equal(
      glyphScale(4000, FOV),
      capped,
      "past the ceiling the scale must be flat, not merely slower",
    );
    assert.equal(glyphScale(1e9, FOV), capped, "and flat all the way out");
  });

  /*
   * Deliberately asserts a REDUCTION and not an absolute size, because the
   * ceiling is a mitigation and calling it a cure in a test name would be the
   * test lying about the product.
   *
   * 2,280 units is the measured Golden Gate case, where the uncapped glyph
   * reached about 81x — roughly two and a half times the bridge's 1,280 m main
   * span at ~94 m to the unit. The ceiling takes that to about one and a half.
   * It is still bigger than the bridge. The complete fix is to clamp against the
   * camera's focus distance rather than the aircraft's, which is a signature
   * change and is written up in `flights.ts`.
   */
  it("cuts the worst case by a third without touching any board distance", () => {
    /** The floor alone, with no ceiling — what the scale used to be. */
    const uncapped = (d: number, fov: number): number => {
      const frustum = 2 * d * Math.tan((fov * Math.PI) / 360);
      return Math.max(1, (0.016 * frustum) / 0.42);
    };

    // 1,160 units is the far end of the orbit over the California corridor — a
    // pose people actually use. It must be untouched at every field of view the
    // scenes run at, because below 0.012 of the frame the wings stop resolving
    // and a ceiling of 26 put it at 0.0123.
    for (const fov of [42, 50, 60]) {
      assert.equal(
        glyphScale(1160, fov),
        uncapped(1160, fov),
        `the ceiling must not bind at 1160 units and ${fov} degrees`,
      );
    }

    // And the chapter-zoom case must actually come down.
    assert.ok(
      glyphScale(2280, 50) < uncapped(2280, 50) * 0.7,
      "the ceiling must remove at least 30% of the worst case",
    );
  });

  it("is monotonic, so an aeroplane never grows as it approaches", () => {
    let previous = 0;
    for (const d of [1, 10, 50, 100, 300, 700, 900, 2000, 5000]) {
      const s = glyphScale(d, FOV);
      assert.ok(s >= previous, `scale fell between distances at ${d}`);
      previous = s;
    }
  });

  it("returns 1 for degenerate inputs rather than emptying the sky", () => {
    for (const [d, fov] of [[0, FOV], [-1, FOV], [10, 0], [10, 180], [NaN, FOV], [10, NaN]]) {
      assert.equal(glyphScale(d as number, fov as number), 1);
    }
  });
});

/*
 * The focus-distance clamp — the complete fix the ceiling above was a mitigation
 * for.
 *
 * Every assertion in the suite above is left exactly as it was, and that is the
 * point of the first test here: the third parameter is optional and omitting it
 * has to reproduce the previous answer to the bit, not to a rounding. A ceiling
 * that moved by a hair under this change would be silent visual drift in the one
 * function whose entire job is to prevent silent visual drift.
 */
describe("glyph scale, clamped against what the camera is looking at", () => {
  it("reproduces the two-argument answer exactly when no focus distance is given", () => {
    for (const fov of [42, 50, 60]) {
      for (const d of [0.5, 5, 34, 200, 400, 1160, 2280, 4000, 1e9]) {
        assert.equal(
          glyphScale(d, fov, undefined),
          glyphScale(d, fov),
          `omitting the focus distance changed the answer at ${d} units and ${fov} degrees`,
        );
      }
    }
  });

  it("ignores a focus distance that is not a usable number", () => {
    for (const bad of [0, -1, NaN, Infinity]) {
      assert.equal(
        glyphScale(2280, 50, bad),
        glyphScale(2280, 50),
        `a focus distance of ${bad} must fall back to the flat ceiling`,
      );
    }
  });

  it("leaves the whole-board pose alone, where the floor is right about everything", () => {
    /*
     * At a standoff the camera's target and the aircraft are the same distance
     * away to within a third, so the clamp must not bind. 1,160 units is the far
     * end of the California orbit; the aircraft on that board run from about 900
     * to about 1,400.
     */
    for (const fov of [42, 50, 60]) {
      for (const aircraft of [900, 1160, 1400]) {
        assert.equal(
          glyphScale(aircraft, fov, 1160),
          glyphScale(aircraft, fov),
          `the clamp bound at a whole-board pose (${aircraft} units, ${fov} degrees)`,
        );
      }
    }
  });

  it("collapses the Golden Gate case, which the flat ceiling only softened", () => {
    // The measured chapter: the bridge fifteen units off the camera, the traffic
    // over the Pacific two thousand two hundred and eighty away.
    const flat = glyphScale(2280, 50);
    const focused = glyphScale(2280, 50, 15);
    assert.ok(focused < flat / 5, `focus clamp barely moved the worst case: ${focused} vs ${flat}`);
    assert.ok(focused >= 1, "and it must never shrink the glyph below its authored size");
  });

  it("is monotonic in the focus distance: looking further away never shrinks the glyph", () => {
    let previous = 0;
    for (const focus of [1, 5, 15, 34, 100, 300, 1160, 5000]) {
      const s = glyphScale(2280, 50, focus);
      assert.ok(s >= previous, `scale fell as the camera focused further out, at ${focus}`);
      previous = s;
    }
    assert.equal(
      previous,
      glyphScale(2280, 50),
      "and at a focus distance past the backstop it must equal the unclamped answer",
    );
  });

  it("never exceeds the absolute backstop, whatever focus distance it is handed", () => {
    for (const focus of [1e6, 1e9]) {
      assert.ok(
        glyphScale(1e9, 50, focus) <= glyphScale(1e9, 50),
        "the focus clamp must be a ceiling on top of the old one, never a lift",
      );
    }
  });
});
