/**
 * The migration field, held to the claims it must not make.
 *
 * **It is one `THREE.Points`, whatever the sky is doing.** Fifty-eight counties,
 * one draw call, zero triangles — the `nightlights.ts` arrangement that puts San
 * Francisco's 12,038 street lamps on the board for the cost of one cloud. The
 * alternative that keeps suggesting itself is the articulated crow, and it is
 * 4,390 triangles across 33 meshes: **33 draw calls per bird**, against a
 * whole-board budget of 650. Forty of them would be 1,320.
 *
 * **No mote position is ever computed from two granules.** BirdCast is a
 * forecast raster aggregated to county-nights. There is no track and no
 * individual, so joining consecutive samples into a trajectory would be
 * inventing the ten minutes in between — the same lie `Vessel` refuses between
 * AIS fixes. A mote is spawned once with its county's reported heading and
 * ground speed, integrated forward on its own, and respawned from whatever the
 * newest granule says when its life runs out. The assertion below is that a mote
 * alive across a granule change keeps moving on the velocity it was born with.
 *
 * **The state row never reaches it.** `US-CA` has NULL coordinates and 793,141
 * birds aloft against the largest county's 82,549, and it looks exactly like the
 * other fifty-eight rows.
 *
 * **It constructs no light.** CONTRACT.md §4.
 *
 * The world is the real california board — 1,919 m to the unit — because a 1:1
 * fake would pass every drift assertion here while the shipped layer moved motes
 * two thousand times too far.
 */

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 {
  allocateMotes,
  createMigrationLayer,
  discRadiusKm,
  MIGRATION_ALTITUDE_UNITS_PER_METRE,
  MIGRATION_MAX_POINTS,
  MIGRATION_MOTES_PER_COUNTY,
} from "../../engine/migration.ts";
import type { MigrationLayerFactory } from "../../engine/scene.ts";
import type { MigrationCounty, MigrationField } from "../../engine/types.ts";
import { World } from "../../engine/world.ts";

const world = new World(CALIFORNIA_CITY);
const CALIFORNIA_SPAN = 553.9;

/** The seam `scene.ts` constructs through. Asserted at compile time. */
const _factory: MigrationLayerFactory = (w, o) => createMigrationLayer(w, o);
void _factory;

function county(over: Partial<MigrationCounty> = {}): MigrationCounty {
  return {
    id: "US-CA-019",
    name: "Fresno County",
    lat: 36.761006,
    lng: -119.655019,
    areaKm2: 15569,
    aloft: 82549,
    altitude: 333,
    direction: 140,
    speed: 6.9,
    ...over,
  };
}

/** All 58 counties, at the areas and the busiest-night densities they really have. */
function fullState(): MigrationCounty[] {
  const out: MigrationCounty[] = [];
  for (let i = 0; i < 58; i++) {
    out.push(
      county({
        id: `US-CA-${String(i * 2 + 1).padStart(3, "0")}`,
        lat: 33 + (i % 20) * 0.4,
        lng: -122 + Math.floor(i / 20) * 2,
        // The real spread: 601 km² (San Francisco) to 52,073 (San Bernardino).
        areaKm2: 601 + (i / 57) * (52073 - 601),
        // Every county busy, which is the worst case for the point count.
        aloft: 400_000,
      }),
    );
  }
  return out;
}

function field(counties: MigrationCounty[], over: Partial<MigrationField> = {}): MigrationField {
  return {
    counties,
    observedAt: "2026-08-23T03:20:00Z",
    statewide: null,
    quiet: null,
    ...over,
  };
}

function clouds(root: THREE.Object3D): THREE.Points[] {
  const found: THREE.Points[] = [];
  root.traverse((node) => {
    if ((node as THREE.Points).isPoints) found.push(node as THREE.Points);
  });
  return found;
}

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

describe("the migration field's cost", () => {
  it("is one Points and no triangles, for any number of counties", () => {
    for (const counties of [[county()], fullState()]) {
      const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
      layer.setField(field(counties));
      const points = clouds(layer.group);
      assert.equal(points.length, 1, `${counties.length} counties must still be one cloud`);
      assert.equal(layer.group.children.length, 1);
      let meshes = 0;
      layer.group.traverse((node) => {
        if ((node as THREE.Mesh).isMesh) meshes += 1;
      });
      assert.equal(meshes, 0, "not one triangle anywhere in it");
      layer.dispose();
    }
  });

  it("draws at most 700 points with every county in California busy", () => {
    const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
    layer.setField(field(fullState()));
    assert.ok(layer.activeCount() <= MIGRATION_MAX_POINTS, `${layer.activeCount()} points`);
    assert.equal(layer.activeCount(), 58 * MIGRATION_MOTES_PER_COUNTY);
    const cloud = clouds(layer.group)[0] as THREE.Points;
    assert.equal(cloud.geometry.drawRange.count, layer.activeCount());
    // The buffer is allocated once at the ceiling and never grows.
    assert.equal(cloud.geometry.getAttribute("position").count, MIGRATION_MAX_POINTS);
    layer.dispose();
  });

  it("holds the allocation to the buffer even if the feed sends more counties", () => {
    const counts = allocateMotes([...fullState(), ...fullState()]);
    assert.ok(counts.reduce((a, b) => a + b, 0) <= MIGRATION_MAX_POINTS);
    // …and drops the quietest rather than the last, so the busiest county always
    // draws whatever order the wire happened to use.
    const mixed = allocateMotes([county({ aloft: 10 }), county({ aloft: 400_000 })]);
    assert.ok((mixed[1] as number) > (mixed[0] as number));
  });

  it("draws nothing at all for a quiet sky", () => {
    const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
    layer.setSolarElevation(-20);
    layer.setField(field([], { quiet: { reason: "daylight", message: "…" } }));
    assert.equal(layer.activeCount(), 0);
    assert.equal((clouds(layer.group)[0] as THREE.Points).visible, false);
    layer.dispose();
  });

  it("retires every mote when the feed goes away, rather than leaving them drifting", () => {
    const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
    layer.setField(field([county()]));
    layer.setSolarElevation(-20);
    const before = layer.mote(0);
    assert.ok(before);

    layer.setField(null);
    assert.equal(layer.activeCount(), 0);

    // A different night, over a county four hundred kilometres away. Nothing
    // may survive from the granule the feed stopped claiming.
    layer.setField(field([county({ lat: 33.0, lng: -116.0, direction: 320, speed: 35 })]));
    const after = layer.mote(0);
    assert.ok(after);
    const [x, z] = world.project(33.0, -116.0);
    const radius = discRadiusKm(15569) / (world.metresPerUnit / 1000);
    assert.ok(Math.hypot(after.x - x, after.z - z) <= radius + 1e-3);
    layer.dispose();
  });

  it("survives a null, an undefined and a field full of nonsense", () => {
    const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
    layer.setField(null);
    layer.setField(undefined as unknown as MigrationField);
    layer.setField({ counties: [null, { id: "x" }] } as unknown as MigrationField);
    layer.tick(0.016);
    assert.equal(layer.activeCount(), 0);
    layer.dispose();
  });
});

// ---- The claim it must not make -------------------------------------------

describe("a mote's position", () => {
  it("is never computed by interpolating between two consecutive granules", () => {
    const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
    // Granule one: Fresno, flying south-east at 6.9 m/s.
    layer.setField(field([county()]));
    layer.setSolarElevation(-20);
    const before = layer.mote(0);
    assert.ok(before);

    // Granule two, ten minutes later: a county 400 km away, flying the opposite
    // way at five times the speed. If anything blended the two, this is where it
    // would show.
    layer.setField(field([county({ lat: 33.0, lng: -116.0, direction: 320, speed: 35 })]));
    const after = layer.mote(0);
    assert.ok(after);
    assert.deepEqual(
      [after.x, after.z, after.vx, after.vz],
      [before.x, before.z, before.vx, before.vz],
      "a live mote must not move because a new granule arrived",
    );

    layer.tick(1);
    const stepped = layer.mote(0);
    assert.ok(stepped);
    // Exactly its own velocity for exactly one second, and nothing else.
    assert.ok(Math.abs(stepped.x - (before.x + before.vx)) < 1e-6);
    assert.ok(Math.abs(stepped.z - (before.z + before.vz)) < 1e-6);
    layer.dispose();
  });

  it("takes the newest granule only when it is born again", () => {
    const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
    layer.setField(field([county()]));
    layer.setSolarElevation(-20);
    const fresno = layer.mote(0);
    assert.ok(fresno);

    layer.setField(field([county({ lat: 33.0, lng: -116.0, direction: 320, speed: 35 })]));
    // Past every mote's life, which is 900 seconds give or take a third.
    layer.tick(2_000);
    const reborn = layer.mote(0);
    assert.ok(reborn);
    const [x, z] = world.project(33.0, -116.0);
    const radius = discRadiusKm(15569) / (world.metresPerUnit / 1000);
    assert.ok(Math.hypot(reborn.x - x, reborn.z - z) <= radius + 1e-3, "inside the new county's disc");
    assert.ok(reborn.vx < 0, "…and flying north-west, the way the new granule says");
    layer.dispose();
  });

  it("drifts at the reported ground speed and no faster", () => {
    const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
    layer.setField(field([county({ direction: 90, speed: 10 })]));
    layer.setSolarElevation(-20);
    const start = layer.mote(0);
    assert.ok(start);
    layer.tick(60);
    const moved = layer.mote(0);
    assert.ok(moved);
    // Due east at 10 m/s for a minute is 600 m, which at 1,919 m to the unit is
    // 0.313 units — a slow drift, deliberately not exaggerated.
    const metres = Math.hypot(moved.x - start.x, moved.z - start.z) * world.metresPerUnit;
    assert.ok(Math.abs(metres - 600) < 1, `${metres.toFixed(0)} m in sixty seconds`);
    assert.ok(moved.x > start.x, "east is +X");
    assert.ok(Math.abs(moved.z - start.z) < 1e-6, "…and due east is not north or south");
  });

  it("flies toward the reported bearing, on a board where north is -Z", () => {
    const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
    layer.setField(field([county({ direction: 0, speed: 10 })]));
    layer.setSolarElevation(-20);
    const start = layer.mote(0);
    layer.tick(60);
    const moved = layer.mote(0);
    assert.ok(start && moved);
    // A field drifting north-west when the feed says south-east is the one bug
    // here nobody would see, because a cloud of dots has no other way to be wrong.
    assert.ok(moved.z < start.z, "heading 0 must go north, which is -Z");
    layer.dispose();
  });
});

// ---- The disc -------------------------------------------------------------

describe("where the motes are", () => {
  it("scatters them inside a disc of the county's true area", () => {
    const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
    // San Bernardino: 52,073 km², a 129 km disc.
    const big = county({ areaKm2: 52073, lat: 34.84, lng: -116.18 });
    layer.setField(field([big]));
    layer.setSolarElevation(-20);
    const [cx, cz] = world.project(big.lat, big.lng);
    const radiusUnits = (discRadiusKm(big.areaKm2) * 1000) / world.metresPerUnit;
    assert.ok(radiusUnits > 60 && radiusUnits < 75, `${radiusUnits.toFixed(1)} units`);

    let far = 0;
    for (let i = 0; i < layer.activeCount(); i++) {
      const mote = layer.mote(i);
      assert.ok(mote);
      const d = Math.hypot(mote.x - cx, mote.z - cz);
      assert.ok(d <= radiusUnits * 1.02, `${d.toFixed(1)} units from the internal point`);
      if (d > radiusUnits * 0.5) far += 1;
    }
    // Uniform over the disc, not clustered at the point: more than half the area
    // is outside half the radius, so most motes should be.
    assert.ok(far > layer.activeCount() * 0.5, "the scatter fills the disc rather than the middle");
    layer.dispose();
  });

  it("puts them above the ground they are over, not above sea level", () => {
    const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
    // Inyo County: the internal point is up against the White Mountains, and
    // `height_mean_m` is above ground level. At true scale 826 m would be 0.43
    // units and inside the hill.
    const inyo = county({ lat: 36.56216, lng: -117.404209, areaKm2: 26488, altitude: 826 });
    layer.setField(field([inyo]));
    layer.setSolarElevation(-20);
    for (let i = 0; i < layer.activeCount(); i++) {
      const mote = layer.mote(i);
      assert.ok(mote);
      assert.ok(mote.y > 0);
    }
    const mote = layer.mote(0);
    assert.ok(mote);
    const lift = 826 * MIGRATION_ALTITUDE_UNITS_PER_METRE;
    assert.ok(lift > 8 && lift < 9, "826 m is 8.3 units on the aircraft seam");
    assert.ok(mote.y >= lift, "…measured up from the terrain under it");
    layer.dispose();
  });
});

// ---- Night ----------------------------------------------------------------

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

  it("is gone in daylight, whatever the feed sent", () => {
    const layer = createMigrationLayer(world, { span: CALIFORNIA_SPAN });
    layer.setField(field([county()]));
    const cloud = clouds(layer.group)[0] as THREE.Points;

    layer.setSolarElevation(-20);
    assert.equal(cloud.visible, true);
    layer.setSolarElevation(12);
    assert.equal(cloud.visible, false, "BirdCast does not measure by day");
    layer.setSolarElevation(-8);
    assert.equal(cloud.visible, true);
    layer.dispose();
  });
});
