/**
 * The four seams three other workstreams compile against, asserted from both
 * ends.
 *
 * `SceneOptions` grew four factory slots in one commit — `ports`, `vessels`,
 * `precip`, `migration` — and each of them is built, ticked, lit and disposed by
 * `createScene`. Every one of those five touch points is a line somebody can
 * delete without breaking a compile, and the layer would then simply never
 * appear: no error, no failing type, just a board with nothing on it, which is
 * exactly the picture a quiet day is supposed to produce. That is the failure
 * this file exists to catch.
 *
 * ## Why this reads source rather than calling `createScene`
 *
 * The same reason `sceneWiring.test.ts` gives and no other: `createScene` awaits
 * a terrain `Worker` and takes a live `Stage`, so the only place it runs is a
 * browser. Pretending otherwise with a mock `Stage` would test the mock. What is
 * asserted here instead is *structural* and is the half a browser test is
 * slowest to tell you about — the factory is called, it is called once, and it
 * is called only when it was supplied. `scripts/ui-smoke.mjs` is the brace to
 * this belt.
 *
 * The type-level half is real, though, and it is the first block below: an
 * object literal is assigned to `SceneOptions` and the layer shapes are
 * satisfied by hand-written stubs. If a signature moves, `tsc` fails here.
 */

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";

import type {
  MigrationLayerFactory,
  PortLayer,
  PortLayerFactory,
  PrecipLayerFactory,
  SceneOptions,
  VesselLayerFactory,
} from "../../engine/scene.ts";
import CALIFORNIA from "../../cities/california.ts";

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const SCENE = readFileSync(path.join(ROOT, "src/engine/scene.ts"), "utf8");
const MAIN = readFileSync(path.join(ROOT, "src/main.ts"), "utf8");
const INDEX = readFileSync(path.join(ROOT, "index.html"), "utf8");

// ---- The compile-time half -------------------------------------------------
//
// Nothing here runs anything. Its whole job is to fail `npx tsc --noEmit` the
// day one of these signatures changes under a workstream that was told it would
// not, which is cheaper than three agents discovering it separately.

const group = { name: "stub" } as unknown as PortLayer["group"];

const port: PortLayerFactory = () => ({ group, setLighting() {}, dispose() {} });
const vessels: VesselLayerFactory = () => ({
  group,
  setVessels() {},
  setLighting() {},
  tick() {},
  dispose() {},
});
const precip: PrecipLayerFactory = () => ({
  group,
  setField() {},
  setLighting() {},
  tick() {},
  dispose() {},
});
const migration: MigrationLayerFactory = () => ({
  group,
  setField() {},
  setLighting() {},
  setSolarElevation() {},
  tick() {},
  dispose() {},
});

const _options: SceneOptions = {
  city: CALIFORNIA,
  ports: port,
  vessels,
  precip,
  migration,
};

// Named so an unused-locals rule cannot delete the assertion above.
void _options;

/** Each factory, and the setter on the handle that must reach its layer. */
const SEAMS: readonly (readonly [string, string, string])[] = [
  ["ports", "portLayer", ""],
  ["vessels", "vesselLayer", "setVessels"],
  ["precip", "precipLayer", "setPrecip"],
  ["migration", "migrationLayer", "setMigration"],
];

test("SceneOptions carries all four factory slots", () => {
  for (const [slot] of SEAMS) {
    assert.match(
      SCENE,
      new RegExp(`\\n\\s{2}${slot}\\?:\\s*\\w+LayerFactory;`),
      `SceneOptions lost the ${slot} slot`,
    );
  }
});

test("createScene calls each factory exactly once, and only when it was supplied", () => {
  for (const [slot] of SEAMS) {
    const calls = [...SCENE.matchAll(new RegExp(`options\\.${slot}\\(`, "g"))].length;
    assert.equal(
      calls,
      1,
      `createScene calls options.${slot}() ${calls} times. Once, mirroring the ` +
        "fire layer: a layer built twice is two of everything on one board.",
    );
    // The guard and the call in one expression, so absence cannot allocate.
    // `options.fires ? options.fires(world, { span: boardSpan }) : null` is the
    // shape being held to, and the span is what lets a layer size itself from
    // the board rather than from a constant.
    assert.match(
      SCENE,
      new RegExp(
        `options\\.${slot}\\s*\\n?\\s*\\?\\s*options\\.${slot}\\(world,\\s*\\{\\s*span:\\s*boardSpan\\s*\\}\\)`,
      ),
      `options.${slot} must be built exactly the way options.fires is: guarded, ` +
        "handed the world under construction, and sized by the board span.",
    );
  }
});

test("a layer that was not supplied contributes nothing at all", () => {
  for (const [, local] of SEAMS) {
    // Declared `| null` and added to the scene only inside the truthy branch.
    // The empty state for every one of these feeds is the common case — a mean
    // 0.596% of California is under rain and birds are absent fourteen hours a
    // day — so "not visited at all" is the requirement, not "draws nothing".
    assert.match(
      SCENE,
      new RegExp(`const ${local}: \\w+Layer \\| null = options\\.`),
      `${local} must be nullable: absent has to cost no geometry and no draw call`,
    );
    assert.match(
      SCENE,
      // `scene` or `ground`: the ground group is a child of the scene that
      // carries everything whose height is a statement about the terrain, so
      // the relief ramp can restate all of them with one scale. Which of the
      // two a layer belongs to is a question about altitude — the port is on
      // the ground, the rain is not — and this assertion is about the *guard*,
      // not about which parent won that argument.
      new RegExp(`if \\(${local}\\) \\{[\\s\\S]{0,200}?(?:scene|ground)\\.add\\(${local}\\.group\\);`),
      `${local} is added to the scene outside its own null guard`,
    );
  }
});

test("every supplied layer is lit, ticked and disposed with the board", () => {
  for (const [slot, local, setter] of SEAMS) {
    assert.ok(
      SCENE.includes(`${local}?.setLighting(state)`),
      `${local} never receives a rig. CONTRACT §4: Atmosphere computes, a scene ` +
        "applies, nothing writes back — a layer left out of setLighting is a " +
        "layer lit for whatever hour it happened to be built at.",
    );
    assert.ok(
      SCENE.includes(`${local}?.dispose()`),
      `${local} is never disposed; a board switch would orphan its buffers`,
    );
    if (slot !== "ports") {
      assert.ok(
        SCENE.includes(`${local}?.tick(dt)`),
        `${local} declares tick() and never gets one`,
      );
    }
    if (setter !== "") {
      assert.match(
        SCENE,
        new RegExp(`${setter}:\\s*\\([\\w\\s,]*\\)\\s*=>\\s*${local}\\?\\.`),
        `SceneHandle.${setter} must reach ${local} and must be a no-op without it`,
      );
    }
  }
});

test("the port kit is static geometry and says so by having no tick", () => {
  // A deliberate asymmetry worth asserting rather than leaving to be noticed: a
  // quay does not move, so `PortLayer` has no `tick` and `createScene` must not
  // invent one. Vessels move, rain moves, birds move; stone does not.
  assert.ok(
    !SCENE.includes("portLayer?.tick("),
    "the port kit acquired a per-frame tick. Nothing in it moves.",
  );
});


// ---- The other end of the seam --------------------------------------------
//
// `scene.ts` can be perfect and the product still draw nothing, because the
// factories are optional and `main.ts` is the only thing that supplies them. A
// deleted line there is a silent, compiling, type-correct board with no ships on
// it — the same failure this file exists to catch, one file further out.

test("main.ts supplies every factory slot the scene declares", () => {
  for (const [slot] of SEAMS) {
    assert.match(
      MAIN,
      new RegExp(`\\b${slot}:\\s`),
      `main.ts never passes SceneOptions.${slot}, so the layer is never built`,
    );
  }
});

test("main.ts feeds the three layers that take a feed", () => {
  // Each setter, and the gate whose answer it must be handed. The pairing is the
  // assertion: a `setVessels` fed from anything but a promotion would be a hull
  // that skipped the sentinel strip, and a `setPrecip` fed from a body rather
  // than from `promoteRadar(...).field` would be a raster nobody clipped.
  const FEEDS: readonly (readonly [string, string])[] = [
    ["setVessels", "promoteVessels("],
    ["setPrecip", "promoteRadar("],
    ["setMigration", "promoteBirds("],
  ];
  for (const [setter, gate] of FEEDS) {
    assert.ok(MAIN.includes(setter + "("), `main.ts never calls ${setter}`);
    assert.ok(MAIN.includes(gate), `main.ts never calls ${gate}), so ${setter} has no source`);
  }
});

test("every one of the three feeds has a sentence, and a place to print it", () => {
  /**
   * The quiet day is the common day — 0.47% of California under rain, nothing
   * aloft for fourteen hours, and no AIS licence read — so an empty layer with
   * no caption beside it is what a stranger actually sees. Each gate owns a
   * sentence that is never blank; these are the three elements they are written
   * into, and a panel section that lost its paragraph would take the explanation
   * with it and leave a board that looks broken.
   */
  for (const id of ["sea-note", "radar-note", "birds-note"]) {
    assert.ok(INDEX.includes(`id="${id}"`), `index.html has no #${id} to write into`);
    assert.ok(MAIN.includes(`"${id}"`), `main.ts never writes #${id}`);
  }
  for (const gate of ["vesselSummary(", "promoteRadar(", "promoteBirds("]) {
    assert.ok(MAIN.includes(gate), `main.ts never asks ${gate}) for its sentence`);
  }
});
