/**
 * The fire layer, held to the four things a screenshot cannot check.
 *
 * **It draws nothing on a quiet board.** This is the whole round in one
 * assertion. The fixture is a real body captured through the whole wire on an
 * ordinary day: clipped to the SoCal board it contains twenty-two live incident
 * records, every single one with `acres: null`, fifteen of them nameless LA
 * County dispatch numbers. The correct picture is an empty one, and "empty"
 * means `instanceCount === 0` and a mesh the renderer never visits — not a mesh
 * drawing twenty-two zero-sized glyphs.
 *
 * **It constructs no light.** CONTRACT.md §4 gives `Atmosphere` sole ownership
 * of the rig, and a wildfire is the most tempting exception in the codebase. The
 * build spec's `grep` catches the letter; this catches the spirit, by walking
 * the whole subtree and asserting nothing in it is a `THREE.Light`.
 *
 * **It reads two confidence scales out of one column.** MODIS publishes an
 * integer 0–100 and VIIRS publishes `low`/`nominal`/`high` in the same field. A
 * renderer that maps the raw value to an opacity is wrong for one of the two on
 * every frame, and wrong in the direction that puts `NaN` in an alpha.
 *
 * **Momentum brightens and lengthens a fire that was already drawn, and never
 * draws one.** Across all 665 observations in the upstream store's life not one
 * incident has ever recorded two different acreage values, so this code path's
 * first real execution will be in production during a fire. It is therefore
 * asserted against a synthetic series here or it is not shipped.
 *
 * The world below is a real board projection rather than a tidy 1:1 fake —
 * California's `latScale: 58`, which puts one scene unit at 1,919 m. A 1:1 fake
 * would pass while every plume was a thousand times too long.
 */

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

import {
  createFireLayer,
  createMomentumTracker,
  extentRadiusKm,
  fireHeat,
  growthPerHour,
  hotPixelStyle,
  markGlow,
  momentumOf,
  plumeLengthKm,
  plumeWidthKm,
  smokeLoad,
  type DrawnFireMark,
  type FireView,
} from "../../engine/fires.ts";
import { promote, type FirePromotion } from "../../server/fires.ts";
import type { FireLayerFactory, FireView as SceneFireView } from "../../engine/scene.ts";
import { LIVE_FIRES_BODY } from "../data/firesFixture.ts";
import type { World } from "../../engine/world.ts";

// ---- The boards, exactly as the packs declare them -------------------------

const CALIFORNIA = { minLat: 32.55, maxLat: 38.05, minLng: -123.05, maxLng: -114.0 };
const SOCAL = { minLat: 33.28, maxLat: 34.36, minLng: -118.88, maxLng: -117.22 };

/** `california.ts`: centre 35.3/-118.55, `latScale: 58` — 1,919 m to the unit. */
function board(
  latScale: number,
  centre: { lat: number; lng: number },
  ground: (lat: number, lng: number) => number = () => 0,
): World {
  const lngScale = latScale * Math.cos((centre.lat * Math.PI) / 180);
  return {
    project(lat: number, lng: number): [number, number] {
      return [(lng - centre.lng) * lngScale, -(lat - centre.lat) * latScale];
    },
    groundAt: ground,
    metresPerUnit: 111_320 / latScale,
    metres(value: number): number {
      return value / (111_320 / latScale);
    },
  } as unknown as World;
}

const californiaWorld = () => board(58, { lat: 35.3, lng: -118.55 });
const socalWorld = () => board(285, { lat: 33.82, lng: -118.05 });

/** The larger projected extent of the pack's bounds — what `scene.ts` passes. */
const CALIFORNIA_SPAN = 428;
const SOCAL_SPAN = 393;

const NOW = Date.parse("2026-08-22T22:26:00Z");

function promoteFor(bounds: typeof CALIFORNIA): FirePromotion {
  return promote(LIVE_FIRES_BODY, bounds, NOW);
}

/**
 * `FirePromotion` must flow into the layer with no adapter at all. Asserted at
 * compile time, which is the only place it can be: if `promote()` ever renames a
 * field the renderer reads, this line stops the build instead of quietly
 * drawing an empty board.
 */
const _assignable: (p: FirePromotion) => FireView = (p) => p;
void _assignable;

/**
 * And `createFireLayer` must be a `FireLayerFactory` — the one seam `scene.ts`
 * constructs through.
 *
 * A type-only import, so nothing about `scene.ts` is pulled into this test at
 * runtime, and `engine/fires.ts` still imports nothing from it. That is the
 * point of the arrangement: the two modules each declare their own copy of the
 * shape and are checked against each other *here* and at `SceneOptions.fires`,
 * rather than one of them depending on the other. Without this line the first
 * time anyone found out the two had drifted would be the moment WS5 wired them.
 */
const _factory: FireLayerFactory = createFireLayer;
void _factory;

/** And the view `scene.ts` forwards must be one this layer accepts. */
const _viewIn: (v: SceneFireView) => FireView = (v) => v;
void _viewIn;

function meshNamed(root: THREE.Object3D, name: string): THREE.Mesh | THREE.Points {
  const found = root.getObjectByName(name);
  assert.ok(found, `no object named ${name}`);
  return found as THREE.Mesh | THREE.Points;
}

function instanceCount(root: THREE.Object3D, name: string): number {
  const mesh = meshNamed(root, name);
  const geometry = mesh.geometry as THREE.InstancedBufferGeometry;
  return geometry.instanceCount;
}

// ---- The quiet board ------------------------------------------------------

describe("the fire layer on a quiet board", () => {
  it("draws nothing at all for twenty-two live records with no acreage", () => {
    const gated = promoteFor(SOCAL);
    // The fixture is the claim: this is a real day, not a constructed one.
    assert.equal(gated.drawn.length, 0, "the gate must refuse every SoCal row today");
    assert.equal(gated.suppressed, 22, "and there must be twenty-two of them to refuse");

    const layer = createFireLayer(socalWorld(), { span: SOCAL_SPAN, reducedMotion: true });
    layer.setFires(gated);

    assert.equal(layer.markCount(), 0);
    assert.equal(layer.plumeCount(), 0);
    assert.equal(instanceCount(layer.group, "fire-marks"), 0);
    // Invisible, not merely empty. An empty-but-visible mesh is a draw call the
    // renderer still pays for on a board where nothing is happening.
    assert.equal(meshNamed(layer.group, "fire-marks").visible, false);
    assert.equal(meshNamed(layer.group, "fire-smoke-puffs").visible, false);
    layer.dispose();
  });

  it("clears back to empty when the feed goes away", () => {
    const layer = createFireLayer(californiaWorld(), {
      span: CALIFORNIA_SPAN,
      reducedMotion: true,
    });
    layer.setFires(promoteFor(CALIFORNIA));
    assert.ok(layer.markCount() > 0, "California must draw something to begin with");
    layer.setFires(null);
    assert.equal(layer.markCount(), 0);
    assert.equal(layer.detectionCount(), 0);
    assert.equal(meshNamed(layer.group, "fire-marks").visible, false);
    assert.equal(meshNamed(layer.group, "fire-hot-pixels").visible, false);
    layer.dispose();
  });

  it("survives a null, an undefined and a body full of nonsense", () => {
    const layer = createFireLayer(socalWorld(), { span: SOCAL_SPAN, reducedMotion: true });
    layer.setFires(null);
    layer.setFires(undefined as unknown as FireView);
    layer.setFires({
      drawn: [null, { id: "x" }] as unknown as readonly DrawnFireMark[],
      detections: [null, { sat: "MODIS" }] as unknown as FireView["detections"],
      fetchedAt: "not a date",
      ageMs: null,
    });
    // The consumer is a render loop. A throw here is a black page, so the only
    // acceptable behaviour for a shape we did not build is to draw less.
    assert.ok(layer.markCount() <= 1);
    layer.dispose();
  });
});

// ---- The truthful full board ----------------------------------------------

describe("the fire layer on the California board", () => {
  it("draws exactly the five fires the gate admitted, worst first", () => {
    const gated = promoteFor(CALIFORNIA);
    assert.deepEqual(
      gated.drawn.map((fire) => fire.name),
      ["Timber Fire", "Alpaugh Fire", "Carrizo Fire", "Amber Fire", "GREEN"],
    );

    const layer = createFireLayer(californiaWorld(), {
      span: CALIFORNIA_SPAN,
      reducedMotion: true,
    });
    layer.setFires(gated);

    assert.equal(layer.markCount(), 5);
    assert.equal(instanceCount(layer.group, "fire-marks"), 5);
    assert.equal(meshNamed(layer.group, "fire-marks").visible, true);
    layer.dispose();
  });

  it("gives a plume to every tier-2 fire and to no tier-1 fire", () => {
    const gated = promoteFor(CALIFORNIA);
    const tier2 = gated.drawn.filter((fire) => fire.tier === 2);
    assert.deepEqual(
      tier2.map((fire) => fire.name),
      ["Timber Fire", "Alpaugh Fire", "Carrizo Fire"],
      "tier is `promote()`'s decision and this layer must not re-derive it",
    );

    const layer = createFireLayer(californiaWorld(), {
      span: CALIFORNIA_SPAN,
      reducedMotion: true,
    });
    layer.setFires(gated);
    assert.equal(layer.plumeCount(), 3);

    for (const fire of gated.drawn) {
      const state = layer.inspect(fire.id);
      assert.ok(state, `${fire.name} must be inspectable`);
      if (fire.tier === 2) assert.ok(state.plumeUnits > 0, `${fire.name} must have a plume`);
      else assert.equal(state.plumeUnits, 0, `${fire.name} must not have a plume`);
    }
    layer.dispose();
  });

  it("gives the biggest fire the longest plume, and caps it", () => {
    const layer = createFireLayer(californiaWorld(), {
      span: CALIFORNIA_SPAN,
      reducedMotion: true,
    });
    const gated = promoteFor(CALIFORNIA);
    layer.setFires(gated);

    const timber = layer.inspect(gated.drawn[0]?.id ?? "");
    const carrizo = layer.inspect(gated.drawn[2]?.id ?? "");
    assert.ok(timber && carrizo);
    assert.ok(timber.plumeUnits > carrizo.plumeUnits);

    // 40 km is the hard cap, and California's board is 1,919 m to the unit.
    assert.ok(timber.plumeUnits <= (40 * 1000) / (111_320 / 58) + 1e-6);
    layer.dispose();
  });

  it("draws the hot pixels the gate handed it, and not one more", () => {
    const gated = promoteFor(CALIFORNIA);
    assert.ok(gated.detections.length > 0);
    assert.ok(gated.persistentDetections > 0, "the fixture must contain known furniture");

    const layer = createFireLayer(californiaWorld(), {
      span: CALIFORNIA_SPAN,
      reducedMotion: true,
    });
    layer.setFires(gated);
    assert.equal(layer.detectionCount(), gated.detections.length);
    assert.equal(meshNamed(layer.group, "fire-hot-pixels").visible, true);
    layer.dispose();
  });
});

// ---- CONTRACT §4 ----------------------------------------------------------

describe("the fire layer and the light rig", () => {
  it("constructs no THREE.Light anywhere in its subtree", () => {
    const layer = createFireLayer(californiaWorld(), {
      span: CALIFORNIA_SPAN,
      reducedMotion: true,
    });
    layer.setFires(promoteFor(CALIFORNIA));
    layer.setSolarElevation(-12);

    const lights: string[] = [];
    layer.group.traverse((object) => {
      if ((object as THREE.Light).isLight === true) lights.push(object.type);
    });
    assert.deepEqual(lights, [], "Atmosphere is the sole light owner — CONTRACT.md §4");
    layer.dispose();
  });

  it("takes night from the solar elevation seam and nothing else", () => {
    const layer = createFireLayer(californiaWorld(), {
      span: CALIFORNIA_SPAN,
      reducedMotion: true,
    });
    const material = (meshNamed(layer.group, "fire-marks") as THREE.Mesh)
      .material as THREE.ShaderMaterial;

    layer.setSolarElevation(40);
    assert.equal(material.uniforms.uNight?.value, 0, "noon is not night");
    layer.setSolarElevation(-12);
    assert.equal(material.uniforms.uNight?.value, 1, "well after sunset is fully night");
    layer.setSolarElevation(Number.NaN);
    assert.equal(material.uniforms.uNight?.value, 1, "a bad number must change nothing");
    layer.dispose();
  });
});

// ---- Hot pixels are evidence ----------------------------------------------

describe("hot-pixel styling", () => {
  const base = { lat: 36.2, lon: -121.7, acquiredAt: "2026-08-22T21:00:00Z", frp: 12 };

  it("reads VIIRS confidence off the string scale", () => {
    const low = hotPixelStyle({ ...base, sat: "VIIRS-NOAA20", confidence: "low", persistent: false });
    const nominal = hotPixelStyle({
      ...base,
      sat: "VIIRS-SNPP",
      confidence: "nominal",
      persistent: false,
    });
    const high = hotPixelStyle({ ...base, sat: "VIIRS-NOAA20", confidence: "high", persistent: false });

    assert.ok(low.opacity < nominal.opacity);
    assert.ok(nominal.opacity < high.opacity);
    // The failure this exists to stop: `Number("nominal")` is NaN, and NaN in an
    // alpha is a hole in the frame rather than a dim dot.
    for (const style of [low, nominal, high]) {
      assert.ok(Number.isFinite(style.opacity), "a VIIRS string must never become NaN");
    }
  });

  it("reads MODIS confidence off the 0-100 integer scale", () => {
    const weak = hotPixelStyle({ ...base, sat: "MODIS", confidence: "26", persistent: false });
    const strong = hotPixelStyle({ ...base, sat: "MODIS", confidence: "100", persistent: false });
    assert.ok(weak.opacity < strong.opacity);

    // The same literal means opposite things on the two instruments, which is
    // the entire reason the branch exists: "100" is full confidence on MODIS and
    // is not a VIIRS value at all.
    const viirs = hotPixelStyle({ ...base, sat: "VIIRS-SNPP", confidence: "100", persistent: false });
    assert.notEqual(viirs.opacity, strong.opacity);
  });

  it("sizes by fire radiative power, so a cluster has structure in it", () => {
    const faint = hotPixelStyle({ ...base, frp: 1, sat: "MODIS", confidence: "80", persistent: false });
    const fierce = hotPixelStyle({
      ...base,
      frp: 112.9,
      sat: "MODIS",
      confidence: "80",
      persistent: false,
    });
    assert.ok(fierce.size > faint.size);
  });

  it("visibly demotes a persistent source rather than dropping it", () => {
    const fresh = hotPixelStyle({
      ...base,
      frp: 1.0,
      sat: "VIIRS-NOAA20",
      confidence: "nominal",
      persistent: false,
    });
    // The industrial heat source 4.7 km from the upstream operator's house: FRP
    // ~1.0, "nominal", on every pass on every day the store holds, with no
    // incident behind it.
    const furniture = hotPixelStyle({
      ...base,
      frp: 1.0,
      sat: "VIIRS-NOAA20",
      confidence: "nominal",
      persistent: true,
    });

    assert.ok(furniture.opacity < fresh.opacity * 0.5, "a flare stack must not read as a fire");
    assert.ok(furniture.size < fresh.size);
    assert.notEqual(furniture.color, fresh.color);
    assert.ok(furniture.opacity > 0, "counted and drawn faintly, never silently dropped");
  });

  it("survives a missing confidence and a missing FRP", () => {
    const style = hotPixelStyle({ ...base, frp: null, sat: "MODIS", confidence: null, persistent: false });
    assert.ok(Number.isFinite(style.opacity) && style.opacity > 0);
    assert.ok(Number.isFinite(style.size) && style.size > 0);
  });
});

// ---- Momentum -------------------------------------------------------------

describe("momentum", () => {
  it("is zero for a fire that has never restated its acreage", () => {
    // Which is every fire the upstream store has ever held: across all 665
    // observations in its life, no incident has recorded two different acreages.
    const tracker = createMomentumTracker();
    for (let i = 0; i < 6; i++) tracker.observe("f", NOW + i * 600_000, 7591);
    assert.equal(tracker.rateFor("f"), 0);
    assert.equal(momentumOf(tracker.rateFor("f")), 0);
  });

  it("ignores a repeated observation, so one restatement is not a spike", () => {
    const tracker = createMomentumTracker();
    tracker.observe("f", NOW, 100);
    for (let i = 0; i < 20; i++) tracker.observe("f", NOW, 100);
    tracker.observe("f", NOW + 3_600_000, 150);
    assert.equal(tracker.samples("f").length, 2);
    assert.ok(Math.abs(tracker.rateFor("f") - 0.5) < 1e-9, "50 % in an hour is a rate of 0.5");
  });

  it("forgets a fire the gate has dropped", () => {
    const tracker = createMomentumTracker();
    tracker.observe("a", NOW, 100);
    tracker.observe("b", NOW, 100);
    tracker.retain(["a"]);
    assert.equal(tracker.samples("a").length, 1);
    assert.equal(tracker.samples("b").length, 0);
  });

  it("brightens and lengthens without changing the drawn set", () => {
    /**
     * Six samples over five hours, four times, each ending at **the same**
     * acreage and starting lower. So the only thing that differs between the
     * runs is the growth *rate* — the acreage the plume is sized from is
     * identical, which is what makes this a test of momentum rather than of
     * arithmetic on `acres`.
     */
    const finalAcres = 500;
    const starts = [500, 460, 380, 240];
    const world = californiaWorld();
    const lengths: number[] = [];
    const glows: number[] = [];
    const drawnSets: string[][] = [];

    for (const start of starts) {
      const layer = createFireLayer(world, { span: CALIFORNIA_SPAN, reducedMotion: true });
      for (let i = 0; i < 6; i++) {
        const acres = start + ((finalAcres - start) * i) / 5;
        const at = new Date(NOW + i * 3_600_000).toISOString();
        layer.setFires({
          drawn: [
            {
              id: "synthetic",
              name: "Synthetic Fire",
              lat: 36.0,
              lon: -120.0,
              acres,
              pctContained: 20,
              tier: 2,
              observedAt: at,
            },
          ],
          detections: [],
          fetchedAt: at,
          ageMs: 0,
        });
      }
      const state = layer.inspect("synthetic");
      assert.ok(state, "the synthetic fire must be drawn in every run");
      lengths.push(state.plumeUnits);
      glows.push(markGlow(state.heat, state.momentum, 1));
      drawnSets.push([...Array(layer.markCount()).keys()].map(() => "synthetic"));
      assert.equal(layer.plumeCount(), 1);
      layer.dispose();
    }

    for (let i = 1; i < lengths.length; i++) {
      assert.ok(
        (lengths[i] ?? 0) > (lengths[i - 1] ?? 0),
        `plume length must rise with growth rate (${lengths.join(", ")})`,
      );
      assert.ok(
        (glows[i] ?? 0) > (glows[i - 1] ?? 0),
        `emissive must rise with growth rate (${glows.join(", ")})`,
      );
      assert.deepEqual(
        drawnSets[i],
        drawnSets[i - 1],
        "momentum modifies a fire that already passed the gate — it never promotes one",
      );
    }
  });

  it("caps: a fire that trebles in an hour is not drawn twice as long", () => {
    assert.equal(momentumOf(0.35), 1);
    assert.equal(momentumOf(3.0), 1);
    assert.equal(momentumOf(-1), 0);
    assert.equal(momentumOf(Number.NaN), 0);
  });

  it("refuses a series it cannot read", () => {
    assert.equal(growthPerHour([]), 0);
    assert.equal(growthPerHour([{ atMs: NOW, acres: 10 }]), 0);
    assert.equal(growthPerHour([{ atMs: NOW, acres: 0 }, { atMs: NOW + 3_600_000, acres: 10 }]), 0);
    assert.equal(growthPerHour([{ atMs: NOW, acres: 10 }, { atMs: NOW, acres: 20 }]), 0);
    // Shrinking is not negative momentum. An agency revising an estimate down is
    // not a fire going out, and a plume that got shorter would say it was.
    assert.equal(growthPerHour([{ atMs: NOW, acres: 20 }, { atMs: NOW + 3_600_000, acres: 10 }]), 0);
  });
});

// ---- The arithmetic -------------------------------------------------------

describe("fire arithmetic", () => {
  it("turns acreage into the radius of an equal-area disc", () => {
    // Bug Fire: 93,733 acres is 379 km², an 11 km radius, a disc 22 km across.
    assert.ok(Math.abs(extentRadiusKm(93_733) - 11.0) < 0.15);
    assert.ok(Math.abs(extentRadiusKm(10) - 0.113) < 0.005);
    assert.equal(extentRadiusKm(0), 0);
    assert.equal(extentRadiusKm(Number.NaN), 0);
  });

  it("keeps a small fire's plume small", () => {
    // The design's own warning: "a 268-acre fire with a 40 km plume is a lie
    // told in a medium that reads as truthful".
    const carrizo = plumeLengthKm(268.1, 14);
    assert.ok(carrizo > 2 && carrizo < 8, `Carrizo got ${carrizo} km`);
    const timber = plumeLengthKm(7591, 14);
    assert.ok(timber > 20 && timber <= 40, `Timber got ${timber} km`);
    assert.equal(plumeLengthKm(1e9, 200), 40, "the cap is hard");
    assert.equal(plumeLengthKm(0, 14), 0);
  });

  it("lengthens a plume in the wind and widens it with the fire", () => {
    assert.ok(plumeLengthKm(500, 40) > plumeLengthKm(500, 4));
    assert.ok(plumeWidthKm(7591) > plumeWidthKm(268));
    assert.ok(plumeWidthKm(1e9) <= 3.5);
  });

  it("reads heat off acreage and containment, and never off nothing", () => {
    assert.ok(fireHeat(7591, 29) > fireHeat(268, 29));
    assert.ok(fireHeat(500, 0) > fireHeat(500, 70), "a fire being beaten is cooler");
    // `null` containment means "the agency has not said", which is not zero and
    // must not read as a fire nobody has touched.
    assert.equal(fireHeat(500, null), fireHeat(500, 0));
    for (const acres of [10, 100, 1000, 100_000]) {
      const heat = fireHeat(acres, 50);
      assert.ok(heat >= 0 && heat <= 1);
    }
  });

  it("mirrors the shader's glow term and rises with both inputs", () => {
    assert.ok(markGlow(1, 0, 1) > markGlow(0, 0, 1));
    assert.ok(markGlow(0.5, 1, 1) > markGlow(0.5, 0, 1));
    assert.ok(markGlow(0.5, 0, 1) > markGlow(0.5, 0, 0), "night is what makes it glow");
  });
});

// ---- The scalar the office reads ------------------------------------------

describe("smoke load", () => {
  const fire: DrawnFireMark = {
    id: "f",
    name: "Test",
    lat: 34.3,
    lon: -118.0,
    acres: 5000,
    pctContained: 10,
    tier: 2,
    observedAt: null,
  };

  it("is zero with nothing burning", () => {
    assert.equal(smokeLoad([], 34.0, -118.2, 250), 0);
  });

  it("is zero beyond the reach, however big the fire", () => {
    assert.equal(smokeLoad([{ ...fire, acres: 1e6 }], 40.0, -118.0, null), 0);
  });

  it("is higher downwind than upwind of the same fire, at the same distance", () => {
    // Wind from the north (0°) blows smoke south, so a point south of the fire
    // is in it and a point the same distance north is not.
    const south = smokeLoad([fire], fire.lat - 0.3, fire.lon, 0);
    const north = smokeLoad([fire], fire.lat + 0.3, fire.lon, 0);
    assert.ok(south > north, `downwind ${south} must beat upwind ${north}`);
    assert.ok(north > 0, "an upwind floor exists because wind is a ten-minute average");
  });

  it("falls off with distance and stays inside 0..1", () => {
    const near = smokeLoad([fire], fire.lat - 0.05, fire.lon, 0);
    const far = smokeLoad([fire], fire.lat - 0.6, fire.lon, 0);
    assert.ok(near > far);
    const many = smokeLoad(
      Array.from({ length: 12 }, (_, i) => ({ ...fire, id: `f${i}`, acres: 90_000 })),
      fire.lat - 0.05,
      fire.lon,
      0,
    );
    assert.ok(many <= 1 && many > 0.5);
  });

  it("is what the layer reports, wind and all", () => {
    const layer = createFireLayer(socalWorld(), { span: SOCAL_SPAN, reducedMotion: true });
    layer.setWind(20, 0);
    layer.setFires({ drawn: [fire], detections: [], fetchedAt: "2026-08-22T22:20:00Z", ageMs: 0 });
    assert.ok(layer.smokeLoadAt(fire.lat - 0.3, fire.lon) > 0);
    layer.setFires(null);
    assert.equal(layer.smokeLoadAt(fire.lat - 0.3, fire.lon), 0);
    layer.dispose();
  });
});

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

describe("what the fire layer costs", () => {
  it("is three objects, whatever is burning", () => {
    const layer = createFireLayer(californiaWorld(), {
      span: CALIFORNIA_SPAN,
      reducedMotion: true,
    });
    layer.setFires(promoteFor(CALIFORNIA));

    const drawable: string[] = [];
    layer.group.traverse((object) => {
      if ((object as THREE.Mesh).isMesh === true || (object as THREE.Points).isPoints === true) {
        drawable.push(object.name);
      }
    });
    // One instanced mesh for the marks and their ground extent, one Points for
    // the hot pixels, one instanced mesh for every plume there will ever be.
    assert.deepEqual(drawable.sort(), ["fire-hot-pixels", "fire-marks", "fire-smoke-puffs"]);
    layer.dispose();
  });

  it("packs the ground extent into the mark's own geometry", () => {
    const layer = createFireLayer(californiaWorld(), {
      span: CALIFORNIA_SPAN,
      reducedMotion: true,
    });
    const geometry = meshNamed(layer.group, "fire-marks").geometry;
    const index = geometry.getIndex();
    assert.ok(index);
    // Six for the ember, two for the disc. Two triangles a fire and no second
    // draw call is the whole reason the extent can exist at all.
    assert.equal(index.count / 3, 8);
    assert.ok(geometry.getAttribute("aPart"), "the per-vertex part selector must be there");
    layer.dispose();
  });

  it("never exceeds its instance capacity, whatever an upstream sends", () => {
    const layer = createFireLayer(californiaWorld(), {
      span: CALIFORNIA_SPAN,
      maxFires: 8,
      maxDetections: 16,
      reducedMotion: true,
    });
    layer.setFires({
      drawn: Array.from({ length: 400 }, (_, i) => ({
        id: `f${i}`,
        name: null,
        lat: 35 + (i % 20) * 0.05,
        lon: -119 + (i % 17) * 0.05,
        acres: 200,
        pctContained: null,
        tier: 2 as const,
        observedAt: null,
      })),
      detections: Array.from({ length: 900 }, (_, i) => ({
        sat: "MODIS",
        lat: 35 + (i % 30) * 0.02,
        lon: -119 + (i % 23) * 0.02,
        frp: 5,
        confidence: "60",
        persistent: false,
        acquiredAt: "2026-08-22T21:00:00Z",
      })),
      fetchedAt: "2026-08-22T22:20:00Z",
      ageMs: 0,
    });

    assert.equal(layer.markCount(), 8);
    assert.equal(layer.detectionCount(), 16);
    assert.ok(layer.plumeCount() <= 8);
    layer.dispose();
  });
});
