/**
 * The reflectivity sheet, held to what a picture cannot show.
 *
 * **It is two triangles and one mesh, whatever the weather is doing.** That is
 * the entire cost argument for the layer, and it holds because `echo_cells`
 * turned out to be a regular lattice — so the field is a texture and the
 * geometry is a quad. A version built from per-cell geometry would look
 * identical in a screenshot and cost 1,596 quads.
 *
 * **It contributes nothing at all to a quiet frame.** Not an invisible mesh, not
 * 1,596 transparent texels: no scene child. California is under rain a mean
 * 0.596% of the time, so this is the layer's ordinary state and it has to cost
 * nothing.
 *
 * **It is never built for a board too fine to carry the cell.** A 0.25-degree
 * cell is 27.8 km — 4.3 texels tall on the SoCal board and 3.4 on the Bay Area
 * one. Four enormous squares over Los Angeles is a lie about resolution told in
 * a medium that reads as truthful, and the refusal is a pure function so it can
 * be asserted rather than screenshotted.
 *
 * **It constructs no light.** CONTRACT.md §4 gives `Atmosphere` sole ownership
 * of the rig; the build spec's grep catches the letter, and walking the subtree
 * catches the spirit.
 *
 * The world below is a real board projection — california's `latScale: 58`,
 * 1,919 m to the unit, `verticalExaggeration: 15` — because a 1:1 fake would
 * pass every one of these while the shipped sheet was buried in the Sierra.
 */

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";

import CALIFORNIA_CITY from "../../cities/california.ts";
import SOCAL_CITY from "../../cities/socal.ts";
import SF_CITY from "../../cities/sf.ts";
import {
  alphaForDbz,
  boardCarriesRaster,
  createPrecipLayer,
  paint,
  precipFactoryFor,
  PRECIP_ALTITUDE_M,
} from "../../engine/precip.ts";
import { radarRampRgb } from "../../assets/radarRamp.ts";
import { buildRadarField, RADAR_DRY_DBZ, RADAR_RAIN_DBZ, type RadarCell } from "../../server/radar.ts";
import type { PrecipLayerFactory } from "../../engine/scene.ts";
import type { RadarField } from "../../engine/types.ts";
import { World } from "../../engine/world.ts";

/** The real board. Its heightfield is what puts the Sierra at 39.5 units. */
const world = new World(CALIFORNIA_CITY);
/** `scene.ts`'s own derivation: the larger projected extent of `city.bounds`. */
const CALIFORNIA_SPAN = 553.9;

/**
 * `createPrecipLayer` must be a `PrecipLayerFactory` — the one seam `scene.ts`
 * constructs through. A type-only import, so nothing about `scene.ts` is pulled
 * in at runtime and `engine/precip.ts` still imports nothing from it. Without
 * this line the first time anyone found out the two had drifted would be the
 * moment somebody wired them.
 */
const _factory: PrecipLayerFactory = (w, o) => createPrecipLayer(w, o);
void _factory;

function fieldWith(cells: RadarCell[], wetFraction = 0.02): RadarField {
  const built = buildRadarField({
    cells,
    stations: [{ id: "KHNX", lat: 36.31416, lon: -119.63213, type: "WSR-88D", operability: "RDA - On-line" }],
    bounds: CALIFORNIA_CITY.bounds,
    coast: CALIFORNIA_CITY.landmasses,
    observedAt: "2026-08-23T03:55Z",
    wetFraction,
  });
  assert.ok(built.field, "the fixture must promote or the test asserts nothing");
  return built.field;
}

const RAINY = fieldWith([
  { lat: 36.375, lon: -119.625, dbz: 55 },
  { lat: 36.125, lon: -119.625, dbz: 34 },
  { lat: 36.375, lon: -119.375, dbz: 22 },
]);

function meshes(root: THREE.Object3D): THREE.Mesh[] {
  const found: THREE.Mesh[] = [];
  root.traverse((node) => {
    if ((node as THREE.Mesh).isMesh) found.push(node as THREE.Mesh);
  });
  return found;
}

function triangles(mesh: THREE.Mesh): number {
  const geometry = mesh.geometry;
  const index = geometry.getIndex();
  const count = index !== null ? index.count : (geometry.getAttribute("position")?.count ?? 0);
  return count / 3;
}

// ---- Cost -----------------------------------------------------------------

describe("the reflectivity sheet's cost", () => {
  it("is exactly one mesh and two triangles with a whole state raining", () => {
    const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
    layer.setField(RAINY);

    const drawn = meshes(layer.group);
    assert.equal(drawn.length, 1);
    assert.equal(triangles(drawn[0] as THREE.Mesh), 2);
    assert.equal(layer.group.children.length, 1);
    assert.equal(layer.drawing(), true);
    assert.equal(layer.wetTexels(), 3);
    layer.dispose();
  });

  it("costs one draw call and not the two a double-sided transparent quad costs", () => {
    // three.js renders `transparent` + `DoubleSide` in two passes by default —
    // back faces then front — so that a closed transparent solid composites
    // correctly. Measured on the shipped board with `renderer.info.render.calls`
    // before this line existed: 11 calls with no sky layers, **13** with the
    // sheet alone. A single flat quad cannot overlap itself, so the second pass
    // buys nothing and spends one of the forty-five draws this round has.
    const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
    layer.setField(RAINY);
    const material = (meshes(layer.group)[0] as THREE.Mesh).material as THREE.MeshBasicMaterial;
    assert.equal(material.side, THREE.DoubleSide, "visible from under the cloud base too");
    assert.equal(material.forceSinglePass, true, "…in one pass");
    assert.equal(material.premultipliedAlpha, true);
    assert.equal(material.depthWrite, false);
    layer.dispose();
  });

  it("contributes zero scene children when nothing is promoted", () => {
    const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
    assert.equal(layer.group.children.length, 0, "before any field at all");

    layer.setField(RAINY);
    assert.equal(layer.group.children.length, 1);

    layer.setField(null);
    assert.equal(layer.group.children.length, 0, "an invisible mesh is still a mesh");
    assert.equal(layer.drawing(), false);
    assert.equal(layer.wetTexels(), 0);
    layer.dispose();
  });

  it("holds the whole statewide raster in under eight kilobytes", () => {
    const painted = paint(RAINY);
    assert.equal(painted.bytes.length, RAINY.rows * RAINY.cols * 4);
    assert.ok(painted.bytes.length < 8 * 1024, `${painted.bytes.length} bytes`);
  });

  it("survives a null, an undefined and a field full of nonsense", () => {
    // The consumer is a render loop, so a throw here is a black page.
    const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
    layer.setField(null);
    layer.setField(undefined as unknown as RadarField);
    layer.setField({ ...RAINY, dbz: [1, 2] } as unknown as RadarField);
    layer.setField({ ...RAINY, rows: 0 });
    layer.setField({ ...RAINY, cellLat: 0 });
    layer.tick(0.016);
    assert.equal(layer.group.children.length, 0);
    layer.dispose();
  });
});

// ---- Which boards ---------------------------------------------------------

describe("which boards carry a raster", () => {
  it("is not constructed for the socal or sf packs", () => {
    assert.equal(precipFactoryFor(SOCAL_CITY.bounds), null);
    assert.equal(precipFactoryFor(SF_CITY.bounds), null);
    assert.notEqual(precipFactoryFor(CALIFORNIA_CITY.bounds), null);
  });

  it("says why, in cells rather than in board names", () => {
    // The rule is about the cell, so it survives a board being re-cut — which
    // happened to california this very round.
    assert.equal(boardCarriesRaster(CALIFORNIA_CITY.bounds), true);
    assert.equal(boardCarriesRaster(SOCAL_CITY.bounds), false);
    assert.equal(boardCarriesRaster(SF_CITY.bounds), false);
    // SoCal is 4.3 cells tall and 6.6 wide at a quarter degree.
    const socalRows = (SOCAL_CITY.bounds.maxLat - SOCAL_CITY.bounds.minLat) / 0.25;
    assert.ok(socalRows < 5, `socal is ${socalRows.toFixed(1)} cells tall`);
    // …and the same board would carry a raster at a tenth of a degree.
    assert.equal(boardCarriesRaster(SOCAL_CITY.bounds, 0.08), true);
  });
});

// ---- The picture ----------------------------------------------------------

describe("what a texel says", () => {
  it("draws unknown as a mark and dry as nothing at all", () => {
    // `null` is "nobody is looking there", and a hole drawn as clear sky is a
    // claim nobody made. `RADAR_DRY_DBZ` is "a working radar saw under 20".
    const field: RadarField = { ...RAINY, dbz: [null, RADAR_DRY_DBZ, 45], rows: 1, cols: 3 };
    const { bytes, wet } = paint(field);
    assert.equal(wet, 1);
    assert.ok((bytes[3] as number) > 0, "unknown is visible");
    assert.ok((bytes[3] as number) < 40, "…but faint");
    assert.equal(bytes[7], 0, "dry is fully transparent");
    assert.ok((bytes[11] as number) > 100, "rain is not");
  });

  it("climbs steeply off the rain threshold", () => {
    assert.equal(alphaForDbz(RADAR_RAIN_DBZ - 0.5), 0);
    assert.ok(alphaForDbz(RADAR_RAIN_DBZ) > 0.25, "a cell that has just crossed is already a mark");
    assert.ok(alphaForDbz(30) > alphaForDbz(RADAR_RAIN_DBZ) * 1.5);
    assert.ok(alphaForDbz(55) > 0.9);
    assert.ok(alphaForDbz(70) <= 1);
    assert.equal(alphaForDbz(Number.NaN), 0);
  });

  it("uses the NWS ramp, so 25 dBZ is green and 50 is red", () => {
    const { bytes } = paint({ ...RAINY, dbz: [25, 50], rows: 1, cols: 2 });
    assert.ok((bytes[1] as number) > (bytes[0] as number), "25 dBZ is green");
    assert.ok((bytes[4] as number) > 200 && (bytes[5] as number) < 60, "50 dBZ is red");
  });

  it("writes premultiplied bytes, so a rain edge fades out rather than to black", () => {
    // 1,596 texels stretched across a 554-unit board is a very long linear
    // interpolation between a coloured texel and a transparent one. With
    // straight alpha the *colour* walks to black on the way, and the first frame
    // this layer ever produced had a dark halo round every echo and a hard dark
    // line where the lattice is clipped at the board edge. Invisible in a test
    // that only checks hue; visible in the first screenshot.
    const { bytes } = paint({ ...RAINY, dbz: [RADAR_RAIN_DBZ], rows: 1, cols: 1 });
    const alpha = (bytes[3] as number) / 255;
    const [r, g, b] = [0, 1, 2].map((i) => bytes[i] as number);
    const straight = radarRampRgb(RADAR_RAIN_DBZ);
    assert.ok(alpha > 0 && alpha < 1, "the threshold texel is partly transparent");
    for (const [was, now] of [[straight[0], r], [straight[1], g], [straight[2], b]]) {
      assert.ok(Math.abs((now as number) - (was as number) * alpha) <= 1, `${now} vs ${was} * ${alpha}`);
    }
  });
});

// ---- Where it sits --------------------------------------------------------

describe("where the sheet sits", () => {
  it("sits at the cloud base and lets the Sierra rise through it", () => {
    const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
    layer.setField(RAINY);
    const sheet = meshes(layer.group)[0] as THREE.Mesh;

    // Measured, not assumed: the highest ground on this board is 39.47 units, at
    // 36.60,-118.30, because `verticalExaggeration` is 15. The sheet is at 10.6.
    // That is deliberate — see the header. A sheet high enough to clear the
    // crest is 47 units up, and 47 units of lift under a camera at fifty degrees
    // draws the rain seventy-five kilometres from where it fell.
    const crest = world.groundAt(36.6, -118.3);
    assert.ok(crest > 38 && crest < 41, `the crest measures ${crest.toFixed(2)} units`);
    assert.equal(sheet.position.y, world.metres(PRECIP_ALTITUDE_M));
    assert.ok(sheet.position.y < crest, "the Sierra rises through the rain, as it should");
    // …but it is well clear of the Central Valley floor, which is what it is
    // actually a sheet over: 1,350 m at 15x is ten units above a valley at 0.6.
    const valley = world.groundAt(36.7, -119.8);
    assert.ok(sheet.position.y > valley + 8, `the valley floor is ${valley.toFixed(2)} units`);
    // …and far under the aircraft, which fly at 0.01 units to the metre.
    assert.ok(sheet.position.y < 10_000 * 0.01);
    layer.dispose();
  });

  it("covers the lattice's own footprint, half a cell outside the outer centres", () => {
    const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
    layer.setField(RAINY);
    const sheet = meshes(layer.group)[0] as THREE.Mesh;

    const [westX] = world.project(RAINY.minLat, RAINY.minLng - RAINY.cellLng / 2);
    const [eastX] = world.project(RAINY.minLat, RAINY.minLng + (RAINY.cols - 0.5) * RAINY.cellLng);
    // Half a cell is 14 km — invisible, and wrong.
    assert.ok(Math.abs(sheet.scale.x - Math.abs(eastX - westX)) < 1e-6);
    layer.dispose();
  });

  it("crossfades a new scan in rather than cutting to it", () => {
    const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN, crossfadeSeconds: 10 });
    layer.setField(RAINY);
    const wetter = fieldWith([
      { lat: 36.375, lon: -119.625, dbz: 65 },
      { lat: 36.125, lon: -119.625, dbz: 34 },
      { lat: 36.375, lon: -119.375, dbz: 22 },
    ]);
    layer.setField(wetter);
    const sheet = meshes(layer.group)[0] as THREE.Mesh;
    const map = (sheet.material as THREE.MeshBasicMaterial).map as THREE.DataTexture;
    const before = (map.image.data as Uint8Array).slice();
    layer.tick(1);
    const after = map.image.data as Uint8Array;
    assert.notDeepEqual([...after], [...before], "a tick during the fade must move the pixels");
    layer.tick(20);
    layer.tick(1);
    const settled = (map.image.data as Uint8Array).slice();
    layer.tick(1);
    assert.deepEqual([...(map.image.data as Uint8Array)], [...settled], "…and stop when it lands");
    layer.dispose();
  });
});

// ---- The rig --------------------------------------------------------------

describe("the sheet and the light rig", () => {
  it("constructs no light of any kind", () => {
    const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
    layer.setField(RAINY);
    layer.group.traverse((node) => {
      assert.ok(!(node as THREE.Light).isLight, `${node.name || node.type} is a light`);
    });
    layer.dispose();
  });

  it("dims after dark without disappearing, and reads the sky rather than the fill", () => {
    const layer = createPrecipLayer(world, { span: CALIFORNIA_SPAN });
    layer.setField(RAINY);
    const material = (meshes(layer.group)[0] as THREE.Mesh).material as THREE.MeshBasicMaterial;

    // The two rigs are the ones measured off the shipped `atmosphere.ts` at
    // 20:00Z and 04:35Z — including `hemisphere.intensity`, which is HIGHER at
    // night (1.33) than at noon (0.95) because the fill compensates a moonlit
    // scene. The first draft of this layer read that as a day/night signal and
    // ran the sheet at full strength in the dark; the fixture carries the real
    // numbers so that cannot come back.
    layer.setLighting({
      sky: { top: 0x77a1cb, horizon: 0xe1ebf1 },
      hemisphere: { sky: 0xe6f2fb, ground: 0x74786a, intensity: 0.95 },
    } as never);
    const day = material.opacity;

    layer.setLighting({
      sky: { top: 0x0d1730, horizon: 0x232f4e },
      hemisphere: { sky: 0x374d88, ground: 0x1f2740, intensity: 1.33 },
    } as never);
    const night = material.opacity;

    assert.ok(night < day, `rain at night is darker (${night} vs ${day})`);
    assert.ok(night > 0.3, "…but a data overlay that vanishes after sunset is a defect");
    assert.ok(day > 0.9, "…and it is at full strength in daylight");

    // An interior rig has no sky at all. It must not throw and must not go dark.
    layer.setLighting({ hemisphere: { sky: 0xe6f2fb, ground: 0x74786a, intensity: 1 } } as never);
    assert.ok(material.opacity > 0.3);
    layer.dispose();
  });
});
