/**
 * The fog dip: the transition that is not a crossfade.
 *
 * Two boards cannot be drawn at once and that is a measured fact rather than an
 * engine limit. Against the caps in `scripts/performance-budgets.json`,
 * California + the Bay Area is 2,640,307 triangles against a cap of 2,600,000,
 * California + the Southland is 1,805,344 against 1,700,000, and the two metros
 * together are 3,694,949 against everything; rendering only the outgoing board's
 * terrain still breaks the California direction at 586,535 against 440,000. And
 * `performance-budget.mjs` samples eight seconds after a three-second warm-up,
 * so a sub-second fade never lands inside the window — a transition whose cost
 * the gate cannot see is the exact failure this repo argues against.
 *
 * So the transition is a collapse of the outgoing board's own aerial fog, a
 * swap, and a lift of the incoming board's. Zero extra triangles, zero extra
 * draw calls, no render target — and it is also the *right* transition, because
 * a true dissolve at a matched pose would put the seam's 4.17x vertical
 * deflation and its up-to-4,025 m registration slide on screen simultaneously
 * and in register.
 *
 * What is testable without a GPU is the arithmetic, and the arithmetic is where
 * the two failures live: a dip that does not actually reach either endpoint, and
 * a dip whose collapsed pair is shared between boards whose scene units differ
 * by a factor of twenty.
 */

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
  collapsedFog,
  dipFog,
  FOG_DIP_IN_SECONDS,
  FOG_DIP_OUT_SECONDS,
  FOG_DIP_REACH_FRACTION,
  type AerialFog,
} from "../engine/atmosphere.ts";

/** A clear-day pair of the shape `Atmosphere.aerial` returns for the Bay Area. */
const CLEAR: AerialFog = { near: 1_150, far: 3_900 };

describe("the collapsed pair is measured against the camera, not the board", () => {
  it("scales with how far back the camera is standing", () => {
    // Fog planes are scene distances and California's unit is twenty times the
    // Bay Area's, so one shared pair would leave one board unfogged and wash the
    // other out completely.
    const wide = collapsedFog(808);
    const close = collapsedFog(41);
    assert.ok(wide.far > close.far);
    assert.equal(wide.far, 808 * FOG_DIP_REACH_FRACTION);
    assert.equal(close.far, 41 * FOG_DIP_REACH_FRACTION);
  });

  it("leaves the near limb of the board faintly readable and everything else gone", () => {
    // The failure this replaced: a far plane at 6% of the board's span put every
    // fragment past it, and the night board photographed as one flat field of
    // #16203a with no coastline in it. `fog_fragment` is linear between the
    // planes, so a far plane at 70% of the stand-off means the fragment nearest
    // the camera — roughly half a board-span nearer than the target — is heavily
    // hazed rather than erased.
    const standoff = 808;
    const fog = collapsedFog(standoff);
    const nearest = standoff - 554 / 2;
    const factor = (nearest - fog.near) / (fog.far - fog.near);
    assert.ok(factor > 0.8, `the near limb is hidden (${factor})`);
    assert.ok(factor < 1, `but it is not erased (${factor})`);
    // Everything at or past what the camera is pointed at is gone outright.
    assert.ok((standoff - fog.near) / (fog.far - fog.near) >= 1);
  });

  it("closes to just in front of the camera, and not onto it", () => {
    const fog = collapsedFog(808);
    assert.ok(fog.near > 0, "a positive near plane is what keeps the whole dip on one curve");
    assert.ok(fog.near < fog.far);
  });

  it("refuses to produce a degenerate pair for a degenerate reach", () => {
    for (const reach of [0, -1]) {
      const fog = collapsedFog(reach);
      assert.ok(fog.far > 0, `reach ${reach} still yields a usable far plane`);
      assert.ok(Number.isFinite(fog.far));
    }
  });
});

describe("a dip reaches both ends and nothing beyond them", () => {
  const hidden = collapsedFog(808);

  it("is exactly the start at zero and exactly the end at one", () => {
    assert.deepEqual(dipFog(CLEAR, hidden, 0), CLEAR);
    assert.deepEqual(dipFog(CLEAR, hidden, 1), hidden);
    // The lift is the same call with the endpoints swapped, which is why there
    // is one function and not two.
    assert.deepEqual(dipFog(hidden, CLEAR, 0), hidden);
    assert.deepEqual(dipFog(hidden, CLEAR, 1), CLEAR);
  });

  it("clamps rather than overshooting a caller's clock", () => {
    assert.deepEqual(dipFog(CLEAR, hidden, 1.4), hidden);
    assert.deepEqual(dipFog(CLEAR, hidden, -0.2), CLEAR);
    assert.deepEqual(dipFog(CLEAR, hidden, Number.NaN), CLEAR);
  });

  it("closes monotonically, and never leaves the board briefly clearer", () => {
    // A fog that opened even slightly partway through the collapse would be a
    // flash of the outgoing board at exactly the moment the swap is meant to be
    // invisible.
    let previous = Number.POSITIVE_INFINITY;
    for (let t = 0; t <= 1.0001; t += 0.02) {
      const far = dipFog(CLEAR, hidden, t).far;
      assert.ok(far <= previous + 1e-9, `far grew at t=${t}: ${far} after ${previous}`);
      previous = far;
    }
  });

  it("fades in ratios rather than in metres", () => {
    // Fog distance is perceived logarithmically. A linear ramp spends the first
    // half of the dip doing almost nothing visible and the second half slamming
    // shut. The ease is symmetric, so halfway through the dip the far plane is
    // exactly the *geometric* mean of the two ends — which is the statement that
    // this is a ratio fade and not a distance fade.
    const mid = dipFog(CLEAR, hidden, 0.5).far;
    assert.ok(
      Math.abs(mid - Math.sqrt(CLEAR.far * hidden.far)) < 1e-9,
      `midpoint ${mid} is not the geometric mean of ${CLEAR.far} and ${hidden.far}`,
    );
    assert.ok(mid < (CLEAR.far + hidden.far) / 2, "and it is below the arithmetic mean");
    assert.ok(mid > hidden.far, "and it has not arrived early either");
  });

  it("keeps the pair coherent at every step", () => {
    // The failure this catches is real and was found by writing it: a linear
    // near plane against a geometric far plane crosses over partway through the
    // dip, and `scene.fog` with `near > far` draws nothing coherent.
    for (let t = 0; t <= 1.0001; t += 0.1) {
      const fog = dipFog(CLEAR, hidden, t);
      assert.ok(Number.isFinite(fog.near), `near is finite at t=${t}`);
      assert.ok(Number.isFinite(fog.far), `far is finite at t=${t}`);
      assert.ok(fog.near >= 0);
      assert.ok(fog.far >= fog.near, "a far plane inside the near plane draws nothing");
    }
  });
});

describe("the two halves are timed differently on purpose", () => {
  it("leaves faster than it arrives", () => {
    // Leaving is an instruction the visitor just gave and wants obeyed; arriving
    // is a picture they are being shown.
    assert.ok(FOG_DIP_OUT_SECONDS < FOG_DIP_IN_SECONDS);
  });

  it("fits inside the pause it is hiding", () => {
    // The whole transition is 0.8 s. The switch it covers was measured at
    // 997-1,711 ms of opaque card, of which 222-604 ms blocked the main thread —
    // so the dip is on the order of the thing it replaces rather than an
    // addition to it.
    assert.ok(FOG_DIP_OUT_SECONDS + FOG_DIP_IN_SECONDS <= 1.0);
  });
});
