/**
 * The two things `engine/terrain.ts` now does that a picture found and a test
 * can keep: the sea has a surface, and the relief casts a shadow it can afford.
 *
 * Everything in this round was invisible to the type checker and to every
 * existing test. What a test *can* hold is the handful of facts underneath the
 * picture — that the sea reaches past the fog rather than stopping in a hard
 * diamond, that its swell map tiles and is not one hard diagonal rib, that the
 * terrain casts from a decimated copy of itself rather than from the mesh you
 * are looking at. Each of those is a number, each was got wrong at least once
 * on the way here, and each would go back to being wrong silently.
 *
 * The board below is synthetic and tiny — twenty cells a side — because none of
 * these facts are about California. A real pack would make the file slow and
 * 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 {
  createShorePlates,
  createTerrain,
  createWater,
  groundDetailData,
  swellNormalData,
} from "../../engine/terrain.ts";
import type { City } from "../../engine/types.ts";
import { World } from "../../engine/world.ts";

const BOARD: City = {
  id: "test-board",
  name: "Test 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,
  // One square island with a hill on it: enough land for a terrain grid, and
  // enough water around it for the sea to be the thing under everything.
  landmasses: [
    [
      [36.7, -122.3],
      [37.3, -122.3],
      [37.3, -121.7],
      [36.7, -121.7],
    ],
  ],
  parks: [],
  inlandWater: [],
  hills: [{ name: "Test Hill", lat: 37, lng: -122, elevation: 400, radius: 0.15 }],
  districts: [],
  landmarks: [],
  bridges: [],
  roads: [],
  chapters: [],
};

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

function boardSpan(world: World): number {
  const [westX, northZ] = world.project(BOARD.bounds.maxLat, BOARD.bounds.minLng);
  const [eastX, southZ] = world.project(BOARD.bounds.minLat, BOARD.bounds.maxLng);
  return Math.max(Math.abs(eastX - westX), Math.abs(southZ - northZ));
}

// ---- The swell map ---------------------------------------------------------

/** Decode one texel back to the tangent-space normal it stands for. */
function normalAt(data: Uint8Array, size: number, x: number, y: number): THREE.Vector3 {
  const i = (((y + size) % size) * size + ((x + size) % size)) * 4;
  return new THREE.Vector3(
    ((data[i] as number) / 255) * 2 - 1,
    ((data[i + 1] as number) / 255) * 2 - 1,
    ((data[i + 2] as number) / 255) * 2 - 1,
  );
}

test("the swell map is the same sea on every reload", () => {
  const a = swellNormalData(64);
  const b = swellNormalData(64);
  assert.deepEqual(a, b, "two boards would show two different oceans");
  // And it is a sea rather than a flat card: `Math.random` removed would pass
  // the equality above just as happily as a seeded field does.
  const flat = [...a].every((_, i) => i % 4 === 2 || i % 4 === 3 || a[i] === 128);
  assert.equal(flat, false, "the swell map has no swell in it");
});

test("every texel of the swell map decodes to a unit normal facing up", () => {
  const size = 64;
  const data = swellNormalData(size);
  let worst = 0;
  let lowest = 1;
  for (let y = 0; y < size; y++) {
    for (let x = 0; x < size; x++) {
      const n = normalAt(data, size, x, y);
      worst = Math.max(worst, Math.abs(n.length() - 1));
      lowest = Math.min(lowest, n.z);
    }
  }
  // One byte of quantisation is 1/255 per channel, so a little over that is the
  // whole tolerance a correctly encoded map needs.
  assert.ok(worst < 0.02, `a texel decoded to a normal of length ${1 + worst}`);
  // A tangent-space normal map for a surface, not for an overhang: z is the
  // surface's own axis and nothing may lean past horizontal.
  assert.ok(lowest > 0.5, `a texel leaned to z=${lowest}, which is a cliff, not a wave`);
});

test("the swell map tiles: the wrap is no sharper than the interior", () => {
  const size = 64;
  const data = swellNormalData(size);
  let interior = 0;
  let seam = 0;
  for (let y = 0; y < size; y++) {
    for (let x = 1; x < size - 1; x++) {
      interior = Math.max(interior, normalAt(data, size, x, y).distanceTo(normalAt(data, size, x + 1, y)));
    }
    seam = Math.max(seam, normalAt(data, size, size - 1, y).distanceTo(normalAt(data, size, 0, y)));
  }
  // The sea is drawn as hundreds of copies of this map side by side, so a
  // derivative that does not wrap paints a visible grid across the whole ocean.
  assert.ok(
    seam <= interior * 1.5,
    `the wrap steps by ${seam} against an interior maximum of ${interior}`,
  );
});

test("the swell runs in every direction rather than one", () => {
  /*
   * The failure this holds is a photographed one. With amplitude falling as
   * 1/k every component of the sum carries the *same* slope — slope is
   * amplitude times wave number — the shortest wave wins on sheer count of
   * edges, and the ocean renders as one hard diagonal rib that reads as
   * corduroy rather than water. At 1/k² the slope falls as 1/k and the eight
   * headings stay spread.
   */
  const size = 64;
  const data = swellNormalData(size);
  const buckets = new Array(12).fill(0) as number[];
  let total = 0;
  for (let y = 0; y < size; y++) {
    for (let x = 0; x < size; x++) {
      const n = normalAt(data, size, x, y);
      const slope = Math.hypot(n.x, n.y);
      if (slope < 1e-4) continue;
      // Folded to a half turn: a crest and its trough are one direction.
      const angle = (Math.atan2(n.y, n.x) + Math.PI * 2) % Math.PI;
      const at = Math.min(11, Math.floor((angle / Math.PI) * 12));
      buckets[at] = (buckets[at] as number) + slope;
      total += slope;
    }
  }
  const dominant = Math.max(...buckets) / total;
  assert.ok(dominant < 0.25, `${Math.round(dominant * 100)}% of the swell runs one way`);
});

// ---- The sea ---------------------------------------------------------------

test("the sea reaches far enough out to fade instead of ending", () => {
  return board().then((world) => {
    const sea = createWater(world).children.find((child) => child.name === "sea") as
      | THREE.Mesh<THREE.PlaneGeometry>
      | undefined;
    assert.ok(sea, "there is no sea in the water group");
    /*
     * Where the number comes from: `main.ts` gives the atmosphere a clear-day
     * fog closing at 3.9 board spans, and `scene.ts` lets the orbit retreat to
     * 2.0 spans from the middle of the board. So the furthest a fully-fogged
     * horizon can be from the origin is about 5.9 spans, and a sea that stops
     * anywhere nearer than that shows the viewer its own edge — which is
     * exactly what the 1.8-span plane this replaced did, as a hard diamond with
     * the state floating on it.
     */
    assert.ok(
      sea.geometry.parameters.width / boardSpan(world) >= 12,
      `the sea is only ${sea.geometry.parameters.width / boardSpan(world)} board spans across`,
    );
  });
});

test("the sea has a specular response, which a Lambert card cannot", () => {
  return board().then((world) => {
    const sea = createWater(world).children.find((child) => child.name === "sea") as THREE.Mesh;
    const material = sea.material as THREE.MeshStandardMaterial;
    // `MeshLambertMaterial` has no specular term at all, by construction, which
    // is the whole reason the Pacific used to render as one flat blue value at
    // every hour and from every angle.
    assert.ok(material.isMeshStandardMaterial, "the sea went back to being unlit paint");
    assert.equal(material.metalness, 0, "water is a dielectric");
    assert.ok(material.roughness > 0 && material.roughness < 0.5, "the sun would have no path");
    assert.ok(material.normalMap, "a mirror-flat plane has a specular point, not a glitter path");
    assert.ok(material.normalScale.x > 0, "the swell is switched off");
  });
});

// ---- The terrain's shadow --------------------------------------------------

test("the relief casts, and from a decimated copy of itself", async () => {
  const world = await board();
  // `createTerrain` returns a group. This board is synthetic and declares no
  // `detail` districts, so it takes the un-chunked branch and the group holds
  // one mesh — which is the path this test was always pinning.
  const terrain = createTerrain(world).children[0] as THREE.Mesh;
  assert.equal(terrain.castShadow, true, "the hills shadow nothing again");
  assert.equal(terrain.receiveShadow, true);
  // Standard since the land got a surface response; the cast is stated because
  // the previous one said `MeshLambertMaterial` and a compile-time cast is
  // exactly the kind of assertion that goes on passing after it stops being
  // true. `shadowSide` is unchanged by that move and has to be: the depth pass
  // swaps in its own `MeshDepthMaterial` and copies this flag across, so it is
  // the only thing about this material the shadow map ever reads.
  const material = terrain.material as THREE.MeshStandardMaterial;
  // The acne cure. Without it a constant bias has to cover a depth-per-texel
  // that grows as 1/tan(sun elevation), and no single value is free of acne at
  // a high sun and free of peter-panning at a low one.
  assert.equal(material.shadowSide, THREE.BackSide);

  const index = terrain.geometry.getIndex();
  assert.ok(index, "the terrain lost its index");
  const seen = terrain.geometry.drawRange.count;
  const cast = index.count - seen;
  assert.ok(seen > 0 && cast > 0, `nothing to draw: ${seen} seen, ${cast} cast`);
  assert.equal(terrain.geometry.drawRange.start, 0, "the colour pass would skip the near edge");

  /*
   * A quarter, give or take the edge cells a stride of 2 cannot cover. The
   * ratio is the whole reason this exists: submitting the visible surface to
   * the depth pass draws every triangle on the board a second time, and
   * `renderer.info` counts it — 65,566 triangles on California against about
   * 28,000 of headroom in the board's budget.
   */
  assert.ok(cast / seen > 0.1 && cast / seen < 0.45, `the caster is ${cast / seen} of the surface`);
});

test("the shadow draw range swings onto the caster and back", async () => {
  const world = await board();
  const terrain = createTerrain(world).children[0] as THREE.Mesh;
  const index = terrain.geometry.getIndex();
  assert.ok(index);
  const seen = terrain.geometry.drawRange.count;

  // three fires these either side of the one `renderBufferDirect` the depth
  // pass makes for this mesh, and the depth pass runs before the colour pass —
  // so this pair is the whole mechanism that keeps the caster out of the
  // picture without keeping it out of the shadow map.
  const nothing = null as never;
  terrain.onBeforeShadow(
    nothing, nothing, nothing, nothing,
    terrain.geometry, terrain.material as THREE.Material, nothing,
  );
  assert.equal(terrain.geometry.drawRange.start, seen, "the depth pass is still drawing the surface");
  assert.equal(terrain.geometry.drawRange.count, index.count - seen);

  terrain.onAfterShadow(
    nothing, nothing, nothing, nothing,
    terrain.geometry, terrain.material as THREE.Material, nothing,
  );
  assert.equal(terrain.geometry.drawRange.start, 0, "the caster leaked into the colour pass");
  assert.equal(terrain.geometry.drawRange.count, seen);
});

test("the shore plate receives and does not cast", async () => {
  const world = await board();
  const plate = createShorePlates(world);
  assert.equal(plate.receiveShadow, true);
  // It is the landmass polygon lying flat six hundredths of a unit above the
  // sea. A caster that thin has no volume: at a low sun it would throw the
  // whole coastline out across the water as a hard slab.
  assert.equal(plate.castShadow, false);
});

test("the shore plate wears the same surface as the ground it abuts", async () => {
  const world = await board();
  const plate = createShorePlates(world, { surfaceDetail: true }).material as THREE.MeshStandardMaterial;
  const ground = (createTerrain(world, { surfaceDetail: true }).children[0] as THREE.Mesh)
    .material as THREE.MeshStandardMaterial;
  /*
   * Two material objects — the plate is a flat `pal.shore` and the terrain is
   * `vertexColors`, which three compiles to different programs whatever this
   * file does — but every term that decides how they *light* has to agree,
   * because the coastline is the one place on the board where two materials
   * meet edge to edge in the same colour. A step there reads as a seam.
   *
   * Both halves were got wrong in turn and both were caught by a photograph.
   * Lambert plate against a standard ground: `WebGLRenderer` forwards
   * `scene.environment` only to `isMeshStandardMaterial`, so the rim sat about a
   * fifth of the hemisphere's diffuse contribution darker than the ground.
   * Mapless plate against a grained ground: the terrain grid's stair-stepped rim
   * — which the plate exists to hide — came back as a legible zigzag between a
   * textured surface and a smooth one at San Francisco.
   */
  assert.ok(plate.isMeshStandardMaterial, "the coastline is lit by different terms again");
  assert.equal(plate.roughness, ground.roughness);
  assert.equal(plate.metalness, ground.metalness);
  assert.ok(plate.normalMap, "the plate is smooth against grained ground, so the rim is legible");
  assert.equal(plate.shadowSide, ground.shadowSide);
  // Not the same *object*: it carries a colour where the ground carries a ramp.
  assert.notEqual(plate, ground);
});

// ---- The land's surface ----------------------------------------------------

/*
 * Everything below is the land's half of the argument the sea won further up
 * this file. The ground was `MeshLambertMaterial` with a vertex colour and no
 * maps of any kind, next to a sea with a swell map on it — and, less visibly,
 * `WebGLRenderer` forwards `scene.environment` only to `isMeshStandardMaterial`,
 * so the whole PMREM sky `environmentRig.ts` builds was reaching the water and
 * not the state. None of that was visible to the type checker, and the two
 * assertions above it that were about the terrain's material both went through
 * a compile-time cast that a class change passes straight through.
 */

test("the land catches the sky, which a Lambert card cannot", async () => {
  const world = await board();
  const terrain = createTerrain(world).children[0] as THREE.Mesh;
  const material = terrain.material as THREE.MeshStandardMaterial;
  /*
   * `WebGLRenderer.js` reads
   * `materialProperties.environment = material.isMeshStandardMaterial ? scene.environment : null`
   * in both of the places it decides a program's lights — so this flag is
   * literally the switch that lets the sky reach the ground. `scene.ts` applies
   * the rig to every city scene already; the land was the one large surface in
   * the frame not receiving it.
   */
  assert.ok(material.isMeshStandardMaterial, "the state went back to being unlit paint");
  assert.equal(material.metalness, 0, "a metallic ground has no diffuse term at all");
  // Ground, not water: the sea is under 0.5 so the sun has a path across it,
  // and the land must be well above that or it glints like wet tarmac at every
  // hour. The upper bound is what stops someone "simplifying" this to 1.0,
  // which switches the environment's specular lobe off again.
  assert.ok(
    material.roughness > 0.7 && material.roughness < 1,
    `the land is at roughness ${material.roughness}`,
  );
  assert.equal(material.vertexColors, true, "`groundColor`'s ramp is the board's palette");
  assert.equal(material.shadowSide, THREE.BackSide, "the acne cure did not survive the class change");
});

test("the ground's detail relief is on by default and can be switched off whole", async () => {
  const world = await board();
  const on = createTerrain(world, { surfaceDetail: true }).children[0] as THREE.Mesh;
  const off = createTerrain(world, { surfaceDetail: false }).children[0] as THREE.Mesh;
  const lit = on.material as THREE.MeshStandardMaterial;
  const flat = off.material as THREE.MeshStandardMaterial;

  assert.ok(lit.normalMap, "the ground lost the map that breaks up its 300 m facets");
  assert.equal(lit.normalMap.wrapS, THREE.RepeatWrapping, "one tile of ground, then a seam");
  assert.equal(lit.normalMap.wrapT, THREE.RepeatWrapping);
  assert.equal(lit.normalMap.generateMipmaps, true);
  // The mip chain of a normal map converges on flat, which is what stops the
  // grain crawling in the distance without anything having to decide when.
  assert.equal(lit.normalMap.minFilter, THREE.LinearMipmapLinearFilter);
  assert.ok(lit.normalMap.anisotropy >= 8, "the ground is grazing across most of a map pose");

  /*
   * "Off" is *no map*, the way `MaterialQuality`'s `low` rung is no map — not a
   * smaller one. The cost a handheld is being spared is per fragment: one
   * texture fetch and `getTangentFrame`'s three derivative pairs on 1.3 Mpx of
   * them. A quarter-size map costs exactly the same fetch.
   */
  assert.equal(flat.normalMap, null, "a handheld is still paying for the map it cannot see");
  // And it keeps the half that reads at every stand-off.
  assert.ok(flat.isMeshStandardMaterial, "the phone lost the sky as well as the grain");
  assert.equal(flat.shadowSide, THREE.BackSide);
});

test("the detail UV is built from the plan, because the ground has no uv attribute", async () => {
  const world = await board();
  const terrain = createTerrain(world, { surfaceDetail: true }).children[0] as THREE.Mesh;
  /*
   * The trap this exists for. `vNormalMapUv` is written by three's `uv_vertex`
   * from the `uv` attribute, WebGL feeds a *constant* for an attribute the
   * geometry does not supply, and the result is a ground where every fragment
   * samples texel 0 — a map that costs its fetch and returns one value, with no
   * error anywhere and nothing to see in a picture but a surface that is
   * somehow still flat.
   */
  assert.equal(terrain.geometry.getAttribute("uv"), undefined, "then the patch below is dead code");

  const material = terrain.material as THREE.MeshStandardMaterial;
  assert.equal(
    material.customProgramCacheKey?.(),
    "tera:ground",
    "a patched program sharing three's default key gets someone else's shader",
  );

  // three's own chunk names, so a rename in an upgrade breaks this rather than
  // silently dropping an edit — `String.replace` on a missing needle is a no-op.
  const shader = {
    uniforms: {} as Record<string, { value: unknown }>,
    vertexShader: "#include <common>\nvoid main() {\n#include <uv_vertex>\n}",
    fragmentShader: "#include <common>\nvoid main() {\n#include <normal_fragment_maps>\n}",
  };
  material.onBeforeCompile(shader as never, null as never);

  assert.match(shader.vertexShader, /vNormalMapUv = position\.xz \* uGroundTile;/);
  assert.match(shader.vertexShader, /uniform float uGroundTile;/);
  // The cosine of the *drawn* slope. `verticalExaggeration` draws a real 10°
  // hillside at 69° and a real 30° Sierra face at 83°, and a plan-projected
  // tile on a face at angle θ is stretched 1/cos θ along the fall line — 2.8x
  // and 8.7x. Scaling the tangent-space slope by cos θ is what turns that back
  // into the same bumps rather than a smear of vertical stripes, and it is the
  // reason there is no triplanar projection here costing three fetches.
  assert.match(shader.fragmentShader, /mapN\.xy \*= normalScale \* groundFlat \* groundSeen;/);
  // `fwidth` of the detail UV, and not a view distance. The distance form was
  // written first and was wrong by a factor of nine on this board: a camera
  // lifted to 0.6 of its stand-off sits 9.06 stand-offs away in scene units once
  // `verticalExaggeration` has multiplied the lift, so a window stated in real
  // metres closed at a ninth of the stand-off it named and the map was switched
  // off everywhere a viewer could go. It was caught by a photograph, not by a
  // test, which is why there is now a test.
  assert.match(shader.fragmentShader, /fwidth\( vNormalMapUv \)/);
  assert.match(shader.fragmentShader, /normal = normalize\( tbn \* mapN \);/);

  const tile = shader.uniforms.uGroundTile?.value as number;
  // Repeats per scene unit. One repeat is `GROUND_DETAIL_TILE_METRES` of real
  // ground on every board, which is what keeps the grain the same physical size
  // on a 94 m board and a 1,919 m one. 440 rather than 220, and the difference
  // was a photograph: at 220 `tileableNoise`'s own wrapped cross-fade was the
  // biggest feature in the tile, and the Central Valley came out as a regular
  // grid of four-pointed stars, one per repeat.
  assert.ok(
    Math.abs(tile - world.metresPerUnit / 440) < 1e-9,
    `one repeat covers ${world.metresPerUnit / tile} m, not 440`,
  );
  const fade = shader.uniforms.uGroundFade?.value as THREE.Vector2;
  /*
   * Device pixels of one repeat, not scene units — so the window means the same
   * thing on a 94 m board and a 1,919 m one, at any field of view, at any
   * viewport, at any device pixel ratio and under any vertical exaggeration. It
   * is off under 2 px, where the mip chain has already flattened the map, and
   * full over 8 px, where it is legible. The board's own budget pose is 400 km
   * out, where a 440 m repeat is 1.3 px: the fetch has to be worth nothing
   * there.
   */
  assert.ok(fade.x >= 1 && fade.x < fade.y && fade.y <= 16, `the fade window is ${fade.x}..${fade.y} px`);
});

// ---- The ground's detail map ------------------------------------------------

/*
 * The same six properties `textureMaps.test.ts` holds the office's relief maps
 * to, applied to the one the land carries: it is arithmetic rather than a
 * canvas, so it exists identically here and in a browser and every one of them
 * is checkable under `node --test`.
 */

test("the ground detail map is the same land on every reload, and is not flat", () => {
  const a = groundDetailData(64);
  const b = groundDetailData(64);
  assert.deepEqual(a, b, "two boards would show two different Californias");
  let peak = 0;
  for (let i = 0; i < a.length; i += 4) {
    peak = Math.max(peak, Math.abs((a[i] as number) - 128), Math.abs((a[i + 1] as number) - 128));
  }
  // `textureMaps.test.ts` puts it best: a flat normal map is a wasted texture
  // unit. 8/128 is its threshold and this is the same one.
  assert.ok(peak >= 8, `the ground's relief peaks at ${peak}/128, which is a flat card`);
});

test("every texel of the ground detail map decodes to a unit normal facing up", () => {
  const size = 64;
  const data = groundDetailData(size);
  let worst = 0;
  let lowest = 1;
  for (let y = 0; y < size; y++) {
    for (let x = 0; x < size; x++) {
      const n = normalAt(data, size, x, y);
      worst = Math.max(worst, Math.abs(n.length() - 1));
      lowest = Math.min(lowest, n.z);
    }
  }
  assert.ok(worst < 0.02, `a texel decoded to a normal of length ${1 + worst}`);
  // A finish on a heightfield, not an overhang. `z` is the surface's own axis
  // and ground grain that leans past about 45° stops being grain and starts
  // competing with the geography the mesh is carrying.
  assert.ok(lowest > 0.6, `a texel leaned to z=${lowest}, which is a cliff, not a field`);
});

test("the ground detail map tiles: the wrap is no sharper than the interior", () => {
  const size = 64;
  const data = groundDetailData(size);
  let interior = 0;
  let seam = 0;
  for (let y = 0; y < size; y++) {
    for (let x = 1; x < size - 1; x++) {
      interior = Math.max(interior, normalAt(data, size, x, y).distanceTo(normalAt(data, size, x + 1, y)));
    }
    seam = Math.max(seam, normalAt(data, size, size - 1, y).distanceTo(normalAt(data, size, 0, y)));
  }
  /*
   * California is 1,063 km across and this tile is 440 m, so the map is laid
   * down about two and a half thousand times along one edge of the board. A derivative
   * that does not wrap paints a grid over the entire state — which is the exact
   * failure `tileableNoise`'s four-way blend exists to prevent, reused here
   * rather than reimplemented.
   */
  assert.ok(
    seam <= interior * 1.5,
    `the wrap steps by ${seam} against an interior maximum of ${interior}`,
  );
});

test("the ground detail map lights the same at 128 as at 256", () => {
  /*
   * `normalMapData` converts gradient to slope — `x size` puts it per unit UV,
   * `/ tileMetres` puts it per metre — and that is the only reason a resolution
   * can be chosen for memory rather than for looks. Drop the division and the
   * map gets twice as steep every time the size doubles, silently.
   */
  const coarse = groundDetailData(128);
  const fine = groundDetailData(256);
  const slope = (data: Uint8Array, size: number): number => {
    let sum = 0;
    for (let y = 0; y < size; y++) {
      for (let x = 0; x < size; x++) {
        const n = normalAt(data, size, x, y);
        sum += Math.hypot(n.x, n.y) / n.z;
      }
    }
    return sum / (size * size);
  };
  const a = slope(coarse, 128);
  const b = slope(fine, 256);
  assert.ok(
    Math.abs(a - b) / a < 0.12,
    `mean slope is ${a} at 128 and ${b} at 256: the per-metre conversion is gone`,
  );
  // And it is a real slope rather than a rounding error: about 9.9° mean on the
  // shipped field, deliberately far under the board's own 15x exaggeration so
  // the grain does not out-shout the landforms.
  assert.ok(a > 0.05 && a < 0.4, `the ground's mean slope is ${Math.atan(a) * 57.3}°`);
});

test("the ground grain runs in every direction rather than one", () => {
  /*
   * The swell's failure, restated for the land: a field whose slope is carried
   * by one heading reads as corduroy, and on ground it reads as ploughing —
   * which would be a very confident claim to make about the entire state.
   */
  const size = 128;
  const data = groundDetailData(size);
  const buckets = new Array(12).fill(0) as number[];
  let total = 0;
  for (let y = 0; y < size; y++) {
    for (let x = 0; x < size; x++) {
      const n = normalAt(data, size, x, y);
      const slope = Math.hypot(n.x, n.y);
      if (slope < 1e-4) continue;
      const angle = (Math.atan2(n.y, n.x) + Math.PI * 2) % Math.PI;
      const at = Math.min(11, Math.floor((angle / Math.PI) * 12));
      buckets[at] = (buckets[at] as number) + slope;
      total += slope;
    }
  }
  const dominant = Math.max(...buckets) / total;
  assert.ok(dominant < 0.25, `${Math.round(dominant * 100)}% of the grain runs one way`);
});
