/**
 * 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, 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();
  const terrain = createTerrain(world);
  assert.equal(terrain.castShadow, true, "the hills shadow nothing again");
  assert.equal(terrain.receiveShadow, true);
  const material = terrain.material as THREE.MeshLambertMaterial;
  // 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);
  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);
});
