/**
 * The visible terrain is decimated where the ground is flat, and this is what
 * holds the decimation honest.
 *
 * Everything `lodPatches` does is invisible by construction and therefore
 * invisible to review: a wrong tolerance, a wrong diagonal or a dropped land
 * test all produce a mesh that builds, renders and passes every other test in
 * this directory, and shows up only as a board that has quietly lost its
 * coastline or grown a crack. The four facts below are the ones the pictures
 * were checked against, and each of them is a number.
 *
 * The boards are synthetic and small — twenty cells a side — for the reason
 * `seaAndTerrain.test.ts` gives: none of this is about California, and a real
 * pack would couple a render test to a city's coastline.
 */

import assert from "node:assert/strict";
import test from "node:test";
import * as THREE from "three";

import { createTerrain } from "../../engine/terrain.ts";
import type { City, ScenePalette } from "../../engine/types.ts";
import { World } from "../../engine/world.ts";

/** A square island in the middle of a one-degree board, at 0.05° per cell. */
const BASE: Omit<City, "hills"> = {
  id: "lod-board",
  name: "LOD Board",
  center: { lat: 37, lng: -122 },
  bounds: { minLat: 36.5, maxLat: 37.5, minLng: -122.5, maxLng: -121.5 },
  latScale: 100,
  verticalExaggeration: 2,
  cellLat: 0.05,
  cellLng: 0.05,
  coastFalloff: 0.02,
  /*
   * The island's rim sits half a cell outside the lattice corners it wants, so
   * its land cells run 4..15 on both axes. That is deliberate: the patch levels
   * are aligned to their own multiple, and an island whose interior straddled
   * the alignment would make this file a test of where the coast happens to
   * fall rather than of whether flat ground collapses.
   */
  landmasses: [
    [
      [36.65, -122.35],
      [37.35, -122.35],
      [37.35, -121.65],
      [36.65, -121.65],
    ],
  ],
  parks: [],
  inlandWater: [],
  districts: [],
  landmarks: [],
  bridges: [],
  roads: [],
  chapters: [],
};

/** Flat: no hills at all, so the whole island is one plane at sea level. */
const FLAT: City = { ...BASE, hills: [] };

/**
 * Rough: a hill every other cell, which is the frequency the lattice itself is
 * sized for. Nothing here may collapse, because a bilinear patch across two
 * cells of this is wrong by most of a hill.
 */
const ROUGH: City = {
  ...BASE,
  hills: (() => {
    const hills: City["hills"] = [];
    for (let i = 0; i < 6; i++) {
      for (let j = 0; j < 6; j++) {
        hills.push({
          name: `h${i}-${j}`,
          lat: 36.75 + i * 0.1,
          lng: -122.25 + j * 0.1,
          elevation: 600,
          radius: 0.05,
        });
      }
    }
    return hills;
  })(),
};

async function board(city: City): Promise<World> {
  const world = new World(city);
  assert.equal(await world.ready(), true, "the synthetic board failed to build a heightfield");
  return world;
}

/** Triangles the surface would have had if every land cell were drawn alone. */
function cellByCellTriangles(world: World): number {
  const { latSteps, lngSteps, land } = world.lattice();
  const w = lngSteps + 1;
  let cells = 0;
  for (let i = 0; i < latSteps; i++) {
    for (let j = 0; j < lngSteps; j++) {
      const a = i * w + j;
      if (land[a] && land[a + 1] && land[a + w] && land[a + w + 1]) cells++;
    }
  }
  return cells * 2;
}

/** The ground the cell-by-cell surface covered, in square scene units. */
function cellByCellArea(world: World): number {
  const { latSteps, lngSteps, lats, lngs, land } = world.lattice();
  const w = lngSteps + 1;
  let area = 0;
  for (let i = 0; i < latSteps; i++) {
    for (let j = 0; j < lngSteps; j++) {
      const a = i * w + j;
      if (!land[a] || !land[a + 1] || !land[a + w] || !land[a + w + 1]) continue;
      const [x0, z0] = world.project(lats[i] as number, lngs[j] as number);
      const [x1, z1] = world.project(lats[i + 1] as number, lngs[j + 1] as number);
      area += Math.abs((x1 - x0) * (z1 - z0));
    }
  }
  return area;
}

/**
 * The footprint of a range of the index, in square scene units.
 *
 * Area rather than a cell list because that is the property the decimation has
 * to preserve exactly: the patches cover the same ground, they just cover it
 * with fewer triangles. A merge that swallowed a coastal cell, or a T-junction
 * that left a gap, changes this number and nothing else.
 */
function footprint(geo: THREE.BufferGeometry, start: number, count: number): number {
  const index = geo.getIndex() as THREE.BufferAttribute;
  const pos = geo.getAttribute("position") as THREE.BufferAttribute;
  let area = 0;
  for (let at = start; at < start + count; at += 3) {
    const a = index.getX(at);
    const b = index.getX(at + 1);
    const c = index.getX(at + 2);
    // Twice the signed area of the triangle projected onto the ground plane.
    area += Math.abs(
      (pos.getX(b) - pos.getX(a)) * (pos.getZ(c) - pos.getZ(a)) -
        (pos.getX(c) - pos.getX(a)) * (pos.getZ(b) - pos.getZ(a)),
    ) / 2;
  }
  return area;
}

function visibleTriangles(mesh: THREE.Mesh): number {
  return mesh.geometry.drawRange.count / 3;
}

function casterTriangles(mesh: THREE.Mesh): number {
  const geo = mesh.geometry;
  return ((geo.getIndex() as THREE.BufferAttribute).count - geo.drawRange.count) / 3;
}

test("flat ground collapses and cell-scale relief does not", async () => {
  const flat = await board(FLAT);
  const rough = await board(ROUGH);
  const flatMesh = createTerrain(flat);
  const roughMesh = createTerrain(rough);

  const flatBase = cellByCellTriangles(flat);
  const roughBase = cellByCellTriangles(rough);
  assert.ok(flatBase > 200, `the flat board is too small to be a test: ${flatBase} triangles`);

  /*
   * A plane is a plane at any resolution, so the flat island must come out at
   * the coarsest level the patch list allows — a sixteenth of the cell-by-cell
   * count in the interior, plus whatever the coast leaves unaligned.
   */
  assert.ok(
    visibleTriangles(flatMesh) < flatBase / 4,
    `flat ground kept ${visibleTriangles(flatMesh)} of ${flatBase} triangles`,
  );

  /*
   * And the opposite, which is the half that a too-loose tolerance would break
   * silently: ground that moves every cell has to keep every cell. This is the
   * failure that turns a mountain range into a bump map, and it is the reason
   * the tolerance is a measured number rather than a large one.
   */
  assert.ok(
    visibleTriangles(roughMesh) > roughBase * 0.9,
    `relief at lattice frequency was decimated to ${visibleTriangles(roughMesh)} of ${roughBase}`,
  );
});

test("the collapsed surface covers exactly the ground the cells covered", async () => {
  for (const city of [FLAT, ROUGH]) {
    const world = await board(city);
    const mesh = createTerrain(world);
    const drawn = footprint(mesh.geometry, mesh.geometry.drawRange.start, mesh.geometry.drawRange.count);
    const expected = cellByCellArea(world);
    /*
     * The coastline is the whole point of this assertion. A patch is only
     * collapsed when every one of its lattice points is on land, so the set of
     * ground covered is unchanged down to the last stair-step — and if a merge
     * ever reached across the shore, or a T-junction left a hole, the area is
     * where it shows.
     */
    assert.ok(
      Math.abs(drawn - expected) < expected * 1e-6,
      `${city.id} covers ${drawn} square units against ${expected}`,
    );
  }
});

test("no point of the collapsed surface strays from the heightfield", async () => {
  const world = await board(ROUGH);
  const mesh = createTerrain(world);
  mesh.updateMatrixWorld(true);
  const { latSteps, lngSteps, lats, lngs, height, land } = world.lattice();
  const w = lngSteps + 1;

  const raycaster = new THREE.Raycaster();
  const down = new THREE.Vector3(0, -1, 0);
  const from = new THREE.Vector3();
  let worst = 0;
  let sampled = 0;

  for (let i = 0; i <= latSteps; i++) {
    for (let j = 0; j <= lngSteps; j++) {
      const k = i * w + j;
      if (!land[k]) continue;
      const [x, z] = world.project(lats[i] as number, lngs[j] as number);
      // Nudged inward, because a ray down the exact rim of the mesh is a
      // coin toss between hitting the edge triangle and missing the board.
      from.set(x + 1e-4, 10_000, z + 1e-4);
      raycaster.set(from, down);
      const hit = raycaster.intersectObject(mesh, false)[0];
      if (!hit) continue;
      sampled++;
      worst = Math.max(worst, Math.abs(hit.point.y - world.metres(height[k] as number)));
    }
  }

  assert.ok(sampled > 100, `only ${sampled} lattice points landed on the surface`);
  /*
   * `LOD_HEIGHT_TOLERANCE` is 0.1 scene units and the surface sits 0.012 above
   * the heightfield to clear the shore plate, so 0.12 is the tolerance plus
   * that lift plus a rounding allowance. This is the assertion that a raised
   * tolerance has to walk past: the decimation may not move the ground.
   */
  assert.ok(worst < 0.12, `the surface strays ${worst} scene units from the heightfield`);
});

test("a colour boundary the height test cannot see stops the merge", async () => {
  /*
   * The coast is flat and its colour is not. `groundColor` ramps `sand` into
   * `flats` over the first three metres of elevation, which is a band the
   * coastal falloff makes tens of cells wide and which no height tolerance
   * loose enough to be useful can protect. So the same board is built twice:
   * once with a palette whose beach and flats are the same colour, and once
   * with them far apart. The second must keep more triangles, and the only
   * mechanism that can produce that difference is the colour guard.
   */
  const beach: Partial<ScenePalette> = { sand: 0xffffff, flats: 0x000000 };
  const plain: Partial<ScenePalette> = { sand: 0x9d9c93, flats: 0x9d9c93 };
  // A single broad, low hill: the island climbs through the sand ramp gently
  // enough that the height test is happy everywhere.
  const gentle: City["hills"] = [
    { name: "swell", lat: 37, lng: -122, elevation: 40, radius: 0.4 },
  ];

  const flatColoured = await board({ ...BASE, hills: gentle, palette: plain });
  const rampColoured = await board({ ...BASE, hills: gentle, palette: beach });
  const a = visibleTriangles(createTerrain(flatColoured));
  const b = visibleTriangles(createTerrain(rampColoured));
  assert.ok(b > a, `the colour guard changed nothing: ${b} triangles against ${a}`);
});

test("the shadow caster is coarser than the surface and stands on the same ground", async () => {
  const world = await board(ROUGH);
  const mesh = createTerrain(world);
  const geo = mesh.geometry;

  const seen = visibleTriangles(mesh);
  const cast = casterTriangles(mesh);
  assert.ok(cast > 0, "the relief stopped casting a shadow");
  /*
   * The caster's floor is `SHADOW_CASTER_STRIDE`, so on ground rough enough to
   * defeat every merge it is a quarter of the surface and never more. A caster
   * that came out the same size as the surface would mean the stride had been
   * lost and the depth pass was paying full price for the board.
   */
  assert.ok(cast <= seen / 3, `the caster kept ${cast} triangles against ${seen} visible`);

  // Same board, so the same island: the caster may be blockier at the rim, but
  // it may not be somewhere else.
  const seenArea = footprint(geo, geo.drawRange.start, geo.drawRange.count);
  const castArea = footprint(geo, geo.drawRange.count, (geo.getIndex() as THREE.BufferAttribute).count - geo.drawRange.count);
  assert.ok(
    castArea <= seenArea * 1.0001 && castArea > seenArea * 0.5,
    `the caster covers ${castArea} square units against the surface's ${seenArea}`,
  );
});
