/**
 * The statewide California board — the default one, the first frame an
 * anonymous visitor sees.
 *
 * Every defect this file guards against **typechecked, rendered without a
 * console error, and met every performance budget.** They were only ever
 * visible by looking at the board, which is why they survived for so long and
 * why the assertions below are shaped the way they are: each one is the
 * cheapest arithmetic statement of something a person found in a screenshot.
 *
 *   1. **A self-intersecting coastline.** San Francisco Bay was traced as a
 *      concavity in the landmass, and the polygon's closure edge ran across its
 *      head. `isLand` still answered correctly and the terrain grid still left
 *      the hole, but `ShapeGeometry` triangulated the slit shut and the shore
 *      plate paved the entire bay. Nothing threw. The bay was simply not there.
 *   2. **A board with no relief.** At an exaggeration of 2.25 a 3,000 m range
 *      stood 1.6 units off a board 284 units tall — five tenths of one percent.
 *      Every hill was in the pack, every hill was in the heightfield, and the
 *      state looked like a beach.
 *   3. **Cities that produce no buildings.** A district drawn inside a park
 *      envelope emits zero lots, because `createBlocks` skips every lot in a
 *      park. One did, silently, and read as an empty valley.
 *   4. **A corridor that climbs a mountain nobody meant to put there.** Hill
 *      radii are in degrees and the routes are hand-traced; a range centred
 *      half a degree from US-101 puts a kilometre of climb into the Salinas
 *      Valley and neither the pack nor the renderer has an opinion about it.
 *
 * The blocks-scale assertions at the end are here for a different reason: they
 * are the promise that making the state board legible did not disturb the two
 * boards that already looked right.
 */

import assert from "node:assert/strict";
import { describe, it } from "node:test";

import CALIFORNIA_CITY, { CALIFORNIA_I_5, CALIFORNIA_US_101 } from "../../cities/california.ts";
import SF_CITY from "../../cities/sf.ts";
import SOCAL_CITY from "../../cities/socal.ts";
import { createBlocks } from "../../engine/blocks.ts";
import type { City, LatLng } from "../../engine/types.ts";
import { World } from "../../engine/world.ts";

/** Do two closed segments cross, endpoints excluded? */
function crosses(a: LatLng, b: LatLng, c: LatLng, d: LatLng): boolean {
  const side = (p: LatLng, q: LatLng, r: LatLng): number =>
    Math.sign((q[0] - p[0]) * (r[1] - q[1]) - (q[1] - p[1]) * (r[0] - q[0]));
  return side(a, b, c) !== side(a, b, d) && side(c, d, a) !== side(c, d, b);
}

/**
 * Every pair of non-adjacent edges in a ring, which is quadratic and does not
 * matter: the biggest ring in the repo is a few hundred vertices and this runs
 * in single-digit milliseconds.
 */
function selfIntersections(ring: readonly LatLng[]): Array<[number, number]> {
  const hits: Array<[number, number]> = [];
  const n = ring.length;
  for (let i = 0; i < n; i += 1) {
    for (let j = i + 2; j < n; j += 1) {
      if (i === 0 && j === n - 1) continue; // the closing edge touches the first
      const a = ring[i];
      const b = ring[(i + 1) % n];
      const c = ring[j];
      const d = ring[(j + 1) % n];
      if (!a || !b || !c || !d) continue;
      if (crosses(a, b, c, d)) hits.push([i, j]);
    }
  }
  return hits;
}

/**
 * A `World` whose heightfield is already built.
 *
 * `ready()` waits on a paint that never comes under the Node test runner, so
 * this takes the documented synchronous path instead: `lattice()` builds the
 * field on the calling thread when nobody awaited `ready()`, which is the same
 * fallback a browser with Workers blocked takes.
 */
function builtWorld(city: City): World {
  const world = new World(city);
  world.lattice();
  return world;
}

describe("California board — geometry that only a picture used to catch", () => {
  it("traces every coastline as a simple polygon", () => {
    for (const city of [CALIFORNIA_CITY, SF_CITY, SOCAL_CITY]) {
      for (const [index, ring] of city.landmasses.entries()) {
        const hits = selfIntersections(ring);
        assert.deepEqual(
          hits,
          [],
          `${city.id} landmass ${index} crosses itself at ${JSON.stringify(hits)}; ` +
            "ShapeGeometry will quietly triangulate the slit shut and pave whatever is inside it",
        );
      }
    }
  });

  it("keeps San Francisco Bay as water joined to the Pacific", () => {
    const world = new World(CALIFORNIA_CITY);
    // Down the middle of the bay, from San Pablo to the south bay, plus the
    // Golden Gate itself. Every one of these was dry land when the contour
    // self-intersected.
    for (const [lat, lng] of [
      [38.0, -122.35],
      [37.9, -122.35],
      [37.8, -122.33],
      [37.7, -122.25],
      [37.6, -122.16],
      [37.5, -122.05],
      [37.83, -122.5],
    ] as LatLng[]) {
      assert.equal(world.isLand(lat, lng), false, `${lat},${lng} should be bay`);
    }
    // And the two shores are still land, so the bay is a strait and not a hole
    // punched through the peninsula.
    assert.equal(world.isLand(37.76, -122.44), true, "San Francisco");
    assert.equal(world.isLand(37.8, -122.15), true, "the East Bay");
  });

  it("stands the ranges up far enough to be seen from the state camera", () => {
    /**
     * Relief measured against the board **span**, which is a change from the
     * board's north-south extent and is worth being explicit about, because
     * lowering a threshold and changing its denominator in the same commit is
     * exactly what a weakened test looks like.
     *
     * The span is the number `scene.ts` actually frames on — `boardSpan` is
     * `max(width, height)` and every camera limit, the fog and `chapterFraming`
     * divide by it — so it is the denominator that decides how big a mountain
     * looks. On the board that stopped at 38.05 the two differed a lot (428
     * across against 319 tall) and this assertion was quietly measuring against
     * the smaller one, which flattered it by a third. On the whole state they
     * are both 554 and the distinction stops mattering; it is corrected here so
     * that the next board to change shape is measured against the right thing.
     *
     * The bar is 7%, and it is calibrated against the two boards that already
     * look right rather than chosen. Measured today:
     *
     *   Southern California  29.4 u of 393  =  7.5%
     *   the whole state      41.2 u of 554  =  7.4%
     *   the Bay Area         48.7 u of 1003 =  4.9%
     *
     * The state board sits on Southern California's number, which is the
     * calibration that matters — the two are meant to read as one landscape at
     * two zooms. It got there by the exaggeration going 13 to 15 when the bounds
     * grew, not by this number moving to meet it: at 13 the extended board is
     * 6.4% and this assertion fails, which is the failure doing its job.
     */
    const world = builtWorld(CALIFORNIA_CITY);
    const { bounds } = CALIFORNIA_CITY;
    const [westX, northZ] = world.project(bounds.maxLat, bounds.minLng);
    const [eastX, southZ] = world.project(bounds.minLat, bounds.maxLng);
    const boardUnits = Math.max(Math.abs(eastX - westX), Math.abs(southZ - northZ));

    let peak = 0;
    for (const metres of world.lattice().height) if (metres > peak) peak = metres;
    const peakUnits = world.metres(peak);

    assert.ok(peak > 4_000, `the highest ground is only ${Math.round(peak)} m`);
    assert.ok(
      peakUnits / boardUnits > 0.07,
      `relief is ${((peakUnits / boardUnits) * 100).toFixed(1)}% of the board span — flat`,
    );
  });

  it("leaves the Central Valley a genuine flat between two ranges", () => {
    const world = builtWorld(CALIFORNIA_CITY);
    // A line up the middle of the valley floor, and a matching line along the
    // Sierra crest. Both axes lean west as they run north, which is why they are
    // interpolated rather than held at one longitude: the valley at Bakersfield
    // is at -119.2 and at Stockton it is at -121.4, and a straight line down one
    // meridian walks out of the valley and up into the foothills.
    for (let lat = 35.6; lat <= 37.8; lat += 0.2) {
      const lng = -119.2 - (lat - 35.4) * 0.88;
      const floor = world.elevationAt(lat, lng);
      assert.ok(floor < 260, `the valley floor at ${lat.toFixed(1)}N is ${Math.round(floor)} m`);
    }
    // The crest is sampled at its own longitudes rather than off a straight
    // line: the Sierra swings from -118.3 at Whitney to -119.6 at Sonora, and a
    // meridian drawn through both ends misses the range in the middle.
    for (const [lat, lng] of [
      [36.2, -118.28],
      [36.45, -118.28],
      [36.62, -118.29],
      [36.85, -118.38],
      [37.05, -118.52],
      [37.25, -118.72],
      [37.45, -118.92],
      [37.65, -119.12],
    ] as LatLng[]) {
      const crest = world.elevationAt(lat, lng);
      assert.ok(crest > 2_000, `the Sierra at ${lat.toFixed(2)}N is only ${Math.round(crest)} m`);
    }
  });

  it("keeps the two corridors in the valleys, and climbs only where a driver climbs", () => {
    const world = builtWorld(CALIFORNIA_CITY);
    /**
     * The named passes, and nothing else, each with its own reach.
     *
     * Newhall is a single notch behind Santa Clarita and the Cuesta Grade is one
     * climb out of San Luis Obispo, so both are tight. The Grapevine is not a
     * pass in that sense at all: I-5 leaves the Los Angeles basin at Castaic and
     * does not come down again until Wheeler Ridge forty kilometres later, over
     * Gorman and Tejon, and a small circle round the summit would call most of
     * that ascent an error.
     */
    const passes: Array<{ at: LatLng; reach: number }> = [
      { at: [34.3917, -118.5426], reach: 0.16 }, // Newhall
      { at: [34.75, -118.8], reach: 0.4 }, // the Grapevine: Castaic to Wheeler Ridge
      { at: [35.2828, -120.6596], reach: 0.2 }, // the Cuesta Grade
    ];
    const nearAPass = (lat: number, lng: number): boolean =>
      passes.some(({ at, reach }) => Math.hypot(lat - at[0], (lng - at[1]) * 0.81) < reach);

    for (const [name, path] of [["US-101", CALIFORNIA_US_101], ["I-5", CALIFORNIA_I_5]] as const) {
      for (let index = 0; index < path.length - 1; index += 1) {
        const from = path[index];
        const to = path[index + 1];
        if (!from || !to) continue;
        for (let step = 0; step <= 20; step += 1) {
          const t = step / 20;
          const lat = from[0] + (to[0] - from[0]) * t;
          const lng = from[1] + (to[1] - from[1]) * t;
          const metres = world.elevationAt(lat, lng);
          const cap = nearAPass(lat, lng) ? 1_500 : 700;
          assert.ok(
            metres < cap,
            `${name} climbs to ${Math.round(metres)} m at ${lat.toFixed(2)},${lng.toFixed(2)}`,
          );
        }
      }
    }
  });
});

describe("California board — the built state", () => {
  const world = builtWorld(CALIFORNIA_CITY);
  const blocks = createBlocks(world);

  it("builds every district it declares", () => {
    // A district drawn inside a park envelope, or out in the bay, emits nothing
    // at all and there is no warning anywhere. Each one is rebuilt alone so the
    // empty one is named rather than hidden in the total.
    for (const district of CALIFORNIA_CITY.districts) {
      const alone = new World({ ...CALIFORNIA_CITY, districts: [district] });
      alone.lattice();
      const count = createBlocks(alone).count;
      assert.ok(count > 20, `district "${district.id}" produced ${count} lots`);
    }
  });

  it("makes Los Angeles and the Bay Area read as settlements, not as specks", () => {
    const within = (minLat: number, maxLat: number, minLng: number, maxLng: number): number => {
      let n = 0;
      for (const district of CALIFORNIA_CITY.districts) {
        const inside = district.polygon.every(
          ([lat, lng]) => lat >= minLat && lat <= maxLat && lng >= minLng && lng <= maxLng,
        );
        if (!inside) continue;
        const alone = new World({ ...CALIFORNIA_CITY, districts: [district] });
        alone.lattice();
        n += createBlocks(alone).count;
      }
      return n;
    };
    assert.ok(within(33.4, 34.4, -118.7, -117.6) > 3_000, "the Los Angeles basin is thin");
    assert.ok(within(37.1, 38.1, -122.6, -121.7) > 1_500, "the Bay Area is thin");
  });

  it("stays inside the triangle budget it was sized against", () => {
    // The California board is the tight one: 650 draw calls and 750,000
    // triangles, shared with the aircraft layer. One instanced box is twelve
    // triangles, so this ceiling is about 110k of them — roughly a seventh of
    // the whole board. A district enlarged without checking is the easy way to
    // blow the budget, and `scripts/performance-budget.mjs` needs a browser and
    // a minute to say so.
    assert.ok(blocks.count < 9_200, `${blocks.count} lots is over what the budget was sized for`);
    assert.ok(blocks.count > 7_000, `${blocks.count} lots is thinner than the board was tuned to`);
  });

  it("drops the street lattice and the shadow pass only where a lot is a neighbourhood", () => {
    // The rule in `blocks.ts` is about how much ground a lot covers, not about
    // which board it is. Stated here as the fact it is derived from, so the two
    // detailed boards are provably untouched by it.
    const lotMetres = (city: City): number => 0.42 * (111_320 / city.latScale);
    assert.ok(lotMetres(SF_CITY) < 260, "San Francisco must keep its street grid");
    assert.ok(lotMetres(SOCAL_CITY) < 260, "Southern California must keep its street grid");
    assert.ok(lotMetres(CALIFORNIA_CITY) > 260, "the state board must not draw 800 m streets");

    assert.equal(blocks.castShadow, false, "state-scale lots cost a second pass for one pixel");
    const socal = builtWorld(SOCAL_CITY);
    assert.equal(createBlocks(socal).castShadow, true, "Southern California lost its shadows");
  });
});
