/**
 * The promotion gate, against the day it was written for.
 *
 * This is the test the whole fire feature is built around, and it is worth being
 * blunt about what it is defending. On the day `firesFixture.ts` was captured,
 * **twenty-two live incident rows fell inside the SoCal board's bounds**. Every
 * single one had `acres: null`. Fifteen were nameless LA County dispatch numbers
 * — `LAC-297933`, `LAC-298861`, `LAC-297051`. Nothing was burning in Los
 * Angeles. Drawn ungated, that is twenty-two orange marks over a city on an
 * ordinary Friday, in a frame that was checked by eye and contains no other warm
 * colour at all.
 *
 * The same body, clipped to the California board, contains five real fires:
 * Timber (7,591 ac), Alpaugh (3,600 ac), Carrizo (268 ac), Amber (10 ac) and
 * GREEN (10 ac). One rule, one day, two correct answers — and if the second
 * number ever moves without the first, the gate has been loosened.
 *
 * The other half is the de-duplication watermark. The collector upstream writes
 * every row it is handed and never deletes, while the merge of CAL FIRE's and
 * WFIGS's copies of one fire happens only in the list it *returns* — so a losing
 * row keeps its old `lastSeen` forever. There really were two Timber Fires in
 * the store, 850 m apart, at 7,591 and 6,669 acres. `GHOST_TIMBER` below is the
 * loser, copied out of the store by hand, and a promotion that draws both is a
 * promotion that double-counts and under-reports the same fire at once.
 */

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
  FIRE_TIER_MIN_ACRES,
  FIRE_TIER_PLUME_ACRES,
  detectionConfidence,
  emptyPromotion,
  promote,
  type FireBounds,
} from "../../server/fires.ts";
import { LIVE_FIRES_BODY } from "./firesFixture.ts";
import type { FireDetection, FireIncident, FiresBody } from "../../server/wire.ts";

/** The three shipped boards, from `src/cities/*.ts`. Restated; those are read-only. */
const SOCAL: FireBounds = { minLat: 33.28, maxLat: 34.36, minLng: -118.88, maxLng: -117.22 };
const CALIFORNIA: FireBounds = { minLat: 32.55, maxLat: 38.05, minLng: -123.05, maxLng: -114.0 };
const BAY_AREA: FireBounds = { minLat: 37.18, maxLat: 38.03, minLng: -122.64, maxLng: -121.75 };

/** The live Timber Fire, as the projection served it. */
const LIVE_TIMBER: FireIncident = {
  id: "b7e4a30e-67ab-4964-882e-751da30b44e0",
  source: "calfire",
  name: "Timber Fire",
  lat: 36.224857,
  lon: -121.72983,
  provenance: "us-gov",
  county: "Monterey",
  type: "WF",
  url: "https://www.fire.ca.gov/incidents/2026/8/8/timber-fire/",
  firstSeen: "2026-08-22T21:14:43Z",
  lastSeen: "2026-08-22T22:20:06Z",
  observedAt: "2026-08-22T21:30:08Z",
  acres: 7591,
  pctContained: 29,
};

/**
 * The same fire, as WFIGS had it, left behind by a de-duplication it lost.
 *
 * 850 m south-east of the live row, 6,669 acres against 7,591, and a `lastSeen`
 * an hour behind. It is still in the upstream table right now and it will be
 * there next year.
 */
const GHOST_TIMBER: FireIncident = {
  id: "{51374D2C-D96B-40F7-940B-6CB8E0A483A2}",
  source: "wfigs",
  name: "Timber",
  lat: 36.215898858987735,
  lon: -121.71752094030687,
  provenance: "us-gov",
  county: "Monterey",
  type: "WF",
  url: null,
  firstSeen: "2026-08-22T21:14:43Z",
  lastSeen: "2026-08-22T21:14:43Z",
  observedAt: "2026-08-22T21:14:43Z",
  acres: 6669,
  pctContained: 29,
};

function bodyOf(incidents: FireIncident[], detections: FireDetection[] = []): FiresBody {
  return {
    source: "cloud1",
    fetchedAt: "2026-08-22T22:22:35.806Z",
    latestSeen: incidents.reduce<string | null>(
      (latest, i) => (latest === null || i.lastSeen > latest ? i.lastSeen : latest),
      null,
    ),
    incidents,
    detections,
    detectionWindowHours: 24,
    ttlSeconds: 600,
  };
}

function named(fires: readonly { name: string | null }[]): string[] {
  return fires.map((f) => f.name ?? "<unnamed>").sort();
}

describe("the promotion gate, on the day it was captured", () => {
  it("draws nothing on the SoCal board, and knows how much it refused", () => {
    const promotion = promote(LIVE_FIRES_BODY, SOCAL);

    // The number this whole feature exists for.
    assert.equal(promotion.drawn.length, 0);
    // Not silently: twenty-two live rows are inside those bounds, and the board
    // is entitled to say so rather than merely be empty.
    assert.equal(promotion.suppressed, 22);
    assert.equal(promotion.source, "cloud1");
  });

  it("draws exactly the five real fires on the California board", () => {
    const promotion = promote(LIVE_FIRES_BODY, CALIFORNIA);

    assert.equal(promotion.drawn.length, 5);
    assert.deepEqual(named(promotion.drawn), [
      "Alpaugh Fire",
      "Amber Fire",
      "Carrizo Fire",
      "GREEN",
      "Timber Fire",
    ]);
    // Worst first, so a fixed instance buffer that overflows drops the smallest.
    assert.deepEqual(
      promotion.drawn.map((f) => f.acres),
      [7591, 3600, 268.1, 10, 10],
    );
  });

  it("gives a plume only to the fires big enough to have one", () => {
    const drawn = promote(LIVE_FIRES_BODY, CALIFORNIA).drawn;
    for (const fire of drawn) {
      assert.equal(fire.tier, fire.acres >= FIRE_TIER_PLUME_ACRES ? 2 : 1);
    }
    // The two ten-acre fires are marks and nothing more. A 268-acre fire under a
    // forty-kilometre smoke column is a lie told in a truthful-looking medium.
    assert.deepEqual(
      drawn.filter((f) => f.tier === 1).map((f) => f.name),
      ["Amber Fire", "GREEN"],
    );
  });

  it("draws nothing on the Bay Area board and refuses nothing there either", () => {
    const promotion = promote(LIVE_FIRES_BODY, BAY_AREA);
    assert.equal(promotion.drawn.length, 0);
    // Zero drawn AND zero suppressed is a different sentence from SoCal's zero
    // drawn and twenty-two suppressed, and the caption should be able to tell
    // them apart: nothing is happening here at all.
    assert.equal(promotion.suppressed, 0);
  });

  it("names the biggest fires the board cannot show", () => {
    const promotion = promote(LIVE_FIRES_BODY, SOCAL);
    // Bug Fire is 93,733 acres and 94% contained, so it is off the board AND
    // past the containment gate — an all-clear caption that named it would be
    // reporting finished news as live.
    assert.ok(!named(promotion.offBoard).includes("Bug Fire"));
    assert.deepEqual(named(promotion.offBoard), ["Alpaugh Fire", "MP18 Fire", "Timber Fire"]);
  });

  it("reports the age of the answer, which is what makes an empty board readable", () => {
    const at = Date.parse("2026-08-22T22:27:35.806Z");
    const promotion = promote(LIVE_FIRES_BODY, SOCAL, at);
    assert.equal(promotion.ageMs, 5 * 60_000);
    assert.equal(promotion.fetchedAt, LIVE_FIRES_BODY.fetchedAt);
  });
});

describe("the de-duplication watermark", () => {
  it("drops every row behind the body's latest lastSeen", () => {
    const promotion = promote(bodyOf([LIVE_TIMBER, GHOST_TIMBER]), CALIFORNIA);

    assert.equal(promotion.drawn.length, 1);
    assert.equal(promotion.drawn[0]?.acres, 7591);
    assert.equal(promotion.drawn[0]?.source, "calfire");
    // The ghost is a refusal like any other, counted rather than vanished.
    assert.equal(promotion.suppressed, 1);
  });

  it("prefers the server's stated watermark over one recomputed from the rows", () => {
    // A body that arrived clipped — a bounded read, a truncated array — would
    // move its own watermark down to the newest row it happened to contain, and
    // re-admit exactly the ghosts this filter exists to drop.
    const body = bodyOf([GHOST_TIMBER]);
    body.latestSeen = LIVE_TIMBER.lastSeen;
    assert.equal(promote(body, CALIFORNIA).drawn.length, 0);
  });

  it("still filters when the server said nothing, using the rows in hand", () => {
    const body = bodyOf([LIVE_TIMBER, GHOST_TIMBER]);
    body.latestSeen = null;
    assert.equal(promote(body, CALIFORNIA).drawn.length, 1);
  });
});

describe("the four independent refusals", () => {
  const at = (over: Partial<FireIncident>): FireIncident => ({ ...LIVE_TIMBER, ...over });

  it("refuses a row with no acreage at all", () => {
    // Not "a small fire". A dispatch record with null acreage is a radio call,
    // and fifteen of them were sitting over Los Angeles on the captured day.
    assert.equal(promote(bodyOf([at({ acres: null })]), CALIFORNIA).drawn.length, 0);
  });

  it("refuses a row under the acreage floor and admits one exactly on it", () => {
    assert.equal(
      promote(bodyOf([at({ acres: FIRE_TIER_MIN_ACRES - 0.1 })]), CALIFORNIA).drawn.length,
      0,
    );
    assert.equal(
      promote(bodyOf([at({ acres: FIRE_TIER_MIN_ACRES })]), CALIFORNIA).drawn.length,
      1,
    );
  });

  it("refuses a fire that is 80% contained, and admits one at 79", () => {
    assert.equal(promote(bodyOf([at({ pctContained: 80 })]), CALIFORNIA).drawn.length, 0);
    assert.equal(promote(bodyOf([at({ pctContained: 79 })]), CALIFORNIA).drawn.length, 1);
    // Null containment is "the agency has not said", which is not 100%.
    assert.equal(promote(bodyOf([at({ pctContained: null })]), CALIFORNIA).drawn.length, 1);
  });

  it("never draws a prescribed burn as a wildfire", () => {
    // Deliberate, scheduled, frequently adjacent to real fire ground, and
    // identical to a wildfire under any distance filter.
    assert.equal(promote(bodyOf([at({ type: "RX" })]), CALIFORNIA).drawn.length, 0);
    assert.equal(promote(bodyOf([at({ type: "rx" })]), CALIFORNIA).drawn.length, 0);
    assert.equal(promote(bodyOf([at({ type: "WF" })]), CALIFORNIA).drawn.length, 1);
    // An unstated type is not a prescribed burn; refusing it would silently drop
    // an agency that stopped populating the column.
    assert.equal(promote(bodyOf([at({ type: "" })]), CALIFORNIA).drawn.length, 1);
  });

  it("refuses a name that describes an exercise rather than an event", () => {
    for (const name of ["TRAINING FIRE", "County Drill", "Exercise Ridge", "DO NOT USE"]) {
      assert.equal(promote(bodyOf([at({ name })]), CALIFORNIA).drawn.length, 0, name);
    }
    // Word-boundaried, so a real place is not caught by a substring.
    assert.equal(promote(bodyOf([at({ name: "Drilling Creek" })]), CALIFORNIA).drawn.length, 1);
  });

  it("clips to the board and counts a refusal only where it can be seen", () => {
    // Timber is in Monterey — on the California board, nowhere near SoCal. It is
    // refused for SoCal by geography, not by the ladder, and must not inflate
    // the number a SoCal caption reports.
    const promotion = promote(bodyOf([at({ acres: 5 })]), SOCAL);
    assert.equal(promotion.drawn.length, 0);
    assert.equal(promotion.suppressed, 0);
    assert.equal(promotion.offBoard.length, 0);
  });
});

describe("hot pixels are evidence, not incidents", () => {
  it("splits the known industrial furniture out of the drawn set", () => {
    const promotion = promote(LIVE_FIRES_BODY, SOCAL);
    // Every drawn pixel is one nothing has seen before at low power.
    assert.ok(promotion.detections.every((d) => d.persistent === false));
    // And the ones that were dropped are counted, not vanished — a layer that
    // silently swallowed them could not say how much of a board's thermal
    // activity is a flare stack.
    assert.ok(promotion.persistentDetections > 0);
    assert.equal(promotion.detectionWindowHours, 24);
  });

  it("keeps the permanent heat source near the upstream operator's house out", () => {
    // Verified in the live store on two consecutive days: a VIIRS pixel at
    // ~33.794 / -117.474 burning at FRP ~1.0 with no matching incident. It is an
    // industrial site on the I-15 corridor and it will be there tomorrow.
    // Drawing it is how a map puts a fire on somebody's house.
    const near = (d: FireDetection) =>
      Math.abs(d.lat - 33.794) < 0.02 && Math.abs(d.lon - -117.474) < 0.02;
    const inFixture = LIVE_FIRES_BODY.detections.filter(near);
    assert.ok(inFixture.length > 0, "the fixture should contain the industrial source");
    assert.ok(inFixture.every((d) => d.persistent));
    assert.equal(promote(LIVE_FIRES_BODY, SOCAL).detections.filter(near).length, 0);
  });

  it("reads MODIS and VIIRS confidence on their own scales", () => {
    const at = (sat: string, confidence: string | null): FireDetection => ({
      sat,
      acquiredAt: "2026-08-22T17:37:00Z",
      lat: 36.26,
      lon: -121.71,
      frp: 112.9,
      confidence,
      persistent: false,
      persistentDays: 0,
    });

    // MODIS: an integer 0-100 in the same column VIIRS puts a word in.
    assert.equal(detectionConfidence(at("MODIS", "94")), 0.94);
    assert.equal(detectionConfidence(at("MODIS", "0")), 0);
    // VIIRS: three bands. `low` is the bottom band of a published detection, not
    // an absence of confidence, so it is not zero.
    assert.equal(detectionConfidence(at("VIIRS-NOAA20", "nominal")), 0.5);
    assert.ok((detectionConfidence(at("VIIRS-SNPP", "low")) ?? 0) > 0);
    assert.ok(
      (detectionConfidence(at("VIIRS-SNPP", "high")) ?? 0) >
        (detectionConfidence(at("VIIRS-SNPP", "nominal")) ?? 0),
    );
    // The failure this branch exists to prevent: `Number("nominal")` is NaN, and
    // NaN reaches a shader as a hole.
    assert.equal(detectionConfidence(at("VIIRS-NOAA20", "27")), null);
    assert.equal(detectionConfidence(at("MODIS", null)), null);
  });
});

describe("promote is total", () => {
  it("answers with an empty promotion rather than throwing", () => {
    // The consumer is a render loop. A body from a server one version behind
    // this one, or no body at all, must be a quiet board and never an exception.
    for (const bad of [null, undefined, {}, { incidents: "yes" }, { incidents: [null, 7] }]) {
      const promotion = promote(bad as unknown as FiresBody, CALIFORNIA);
      assert.equal(promotion.drawn.length, 0);
      assert.equal(promotion.detections.length, 0);
    }
  });

  it("says it has never been fetched, rather than claiming it just was", () => {
    const empty = emptyPromotion();
    assert.equal(empty.ageMs, null);
    assert.equal(empty.source, "none");
    assert.equal(Date.parse(empty.fetchedAt), 0);
  });

  it("carries no home-relative field through from any body it is handed", () => {
    // Belt and braces over a structural guarantee. The four columns never leave
    // cloud-1 — but a fixture is exactly the artefact that would immortalise a
    // leak, so this asserts on the shape that actually reaches a renderer.
    const forbidden = ["distance_km", "bearing_deg", "threat", "distanceKm", "bearingDeg"];
    const wire = JSON.stringify(promote(LIVE_FIRES_BODY, CALIFORNIA));
    for (const key of forbidden) assert.ok(!wire.includes(key), key);
    assert.ok(!JSON.stringify(LIVE_FIRES_BODY).includes("Norco"));
  });
});
