/**
 * Everything that reads a pack must read the pack the World is drawing.
 *
 * ## The bug this is downstream of
 *
 * `World`'s constructor is `this.city = reconciledCity(city)` (`world.ts:116`),
 * so the moment any reconciliation rule is on, there are **two** Californias in
 * the process: the object `cities/california.ts` exported, and the rewritten one
 * the engine is actually projecting, exaggerating and drawing roads from.
 *
 * Two consumers were reading the first one while the camera stood in the second:
 *
 *   - `buildLadder` (`main.ts`) derives every rung's stand-off from `focus`, so
 *     a raw-pack ladder measures a world nobody is looking at — the Places rail
 *     silently disagreeing with the camera about where it is.
 *   - `createMinimap` was handed `entry.city` beside a reconciled `handle.world`,
 *     so the plan view drew a different California than the frame beside it.
 *
 * Neither is visible with the flag off, because `reconciledCity` returns the
 * pack **by identity** when no rule is on — which is exactly what made the bug
 * survive: it is latent until the day somebody turns a rule on to take a
 * measurement, and then it corrupts the measurement rather than announcing
 * itself.
 *
 * ## What is asserted
 *
 * Not "main.ts calls the right function" — that is a source-text assertion and
 * `integration/sceneWiring.test.ts` already owns that genre. This asserts the
 * *property*: for every pack and every rule, the reconciled object differs from
 * the raw one in the columns that rule owns, so a consumer holding the wrong one
 * is holding provably different numbers. A future consumer wired to the raw pack
 * is a bug this file describes even if it cannot name the call site.
 */
import assert from "node:assert/strict";
import { after, describe, it } from "node:test";

import CALIFORNIA from "../cities/california.ts";
import SAN_FRANCISCO from "../cities/sf.ts";
import SOCAL from "../cities/socal.ts";
import { RULES, reconciledCity, setReconcile, setReconcileWarn } from "../cities/reconcile.ts";
import type { Rule } from "../cities/reconcile.ts";
import type { City } from "../engine/types.ts";

const PACKS: readonly (readonly [string, City])[] = [
  ["california", CALIFORNIA],
  ["sf", SAN_FRANCISCO],
  ["socal", SOCAL],
];

after(() => {
  setReconcile(null);
  setReconcileWarn(null);
});

/** The columns each rule owns, as the reader of a pack would see them. */
const OWNED: Readonly<Record<Rule, (city: City) => unknown>> = {
  projection: (city) => city.lngScale ?? null,
  exaggeration: (city) => city.verticalExaggeration,
  roads: (city) => city.roads.map((road) => road.width).join(","),
  ground: (city) => `${city.coastFalloff}|${JSON.stringify(city.palette ?? null)}`,
};

describe("a reconciled pack is not the pack the module exported", () => {
  it("returns the very same object when nothing is on", () => {
    setReconcile(false);
    for (const [id, pack] of PACKS) {
      assert.equal(reconciledCity(pack), pack, `${id} was copied with the flag off`);
    }
  });

  /**
   * The load-bearing one. If a rule changes nothing for every pack then a
   * consumer reading the raw pack is harmless and this whole file is theatre —
   * so the file has to prove it is not.
   */
  it("changes the column its rule owns, for at least one pack, for every rule", () => {
    for (const rule of RULES) {
      setReconcile([rule]);
      const moved = PACKS.filter(([, pack]) => {
        const read = OWNED[rule];
        return read(reconciledCity(pack)) !== read(pack);
      });
      assert.ok(
        moved.length > 0,
        `rule "${rule}" changed nothing on any pack — either the rule is dead ` +
          "or OWNED is reading the wrong column",
      );
    }
  });

  it("never mutates the pack the module exported", () => {
    const before = PACKS.map(([, pack]) => JSON.stringify(pack));
    setReconcile(true);
    for (const [, pack] of PACKS) reconciledCity(pack);
    setReconcile(false);
    for (const [index, [id]] of PACKS.entries()) {
      assert.equal(JSON.stringify(PACKS[index]![1]), before[index], `${id} was mutated in place`);
    }
  });

  /**
   * The worker's half of the contract. `World` posts `this.city` to the terrain
   * worker as a structured clone and the worker builds a second `World` from it;
   * `reconciled: true` is what stops that second pass applying every rule again
   * on top of itself. Without it the exaggeration rule would square.
   */
  it("is idempotent, so the worker's second pass is a no-op", () => {
    setReconcile(true);
    for (const [id, pack] of PACKS) {
      const once = reconciledCity(pack);
      assert.equal(once.reconciled, true, `${id} did not mark itself reconciled`);
      assert.equal(reconciledCity(once), once, `${id} was reconciled twice`);
    }
  });
});

describe("the rule parser", () => {
  it("takes exact rule names", () => {
    setReconcile(null);
    for (const rule of RULES) {
      // Through the public surface: setReconcile with an explicit list is what
      // a node script uses, and the query-string path shares `parse`.
      setReconcile([rule]);
      const read = OWNED[rule];
      const moved = PACKS.some(([, pack]) => read(reconciledCity(pack)) !== read(pack));
      assert.ok(moved, `"${rule}" did not select itself`);
    }
  });

  /**
   * The defect: `parse` prefix-matched, so `?reconcile=palette` selected nothing
   * and produced an empty set — **identical to the flag being absent**. A
   * photograph taken to judge a rule was a photograph of the raw board, and
   * nothing in the picture could tell you which.
   */
  it("warns loudly rather than silently reading an unknown rule as off", async () => {
    const said: string[] = [];
    setReconcileWarn((message) => said.push(message));
    setReconcile(null);
    const { activeRules } = await import("../cities/reconcile.ts");
    const search = { search: "?reconcile=palette" };
    const globals = globalThis as { location?: unknown };
    const had = "location" in globals;
    const previous = globals.location;
    globals.location = search;
    try {
      const on = activeRules();
      assert.equal(on.size, 0, "an unknown rule must not select a real one");
      assert.equal(said.length, 1, "an unknown rule must warn exactly once");
      assert.match(said[0] ?? "", /unknown rule "palette"/);
      assert.match(said[0] ?? "", /the flag is NOT off/);
    } finally {
      if (had) globals.location = previous;
      else delete globals.location;
      setReconcileWarn(null);
    }
  });

  /**
   * The other half of prefix-matching: one letter used to select a whole rule,
   * which is fine until two rules share it. `p` must now select nothing.
   */
  it("does not accept an abbreviation", async () => {
    const said: string[] = [];
    setReconcileWarn((message) => said.push(message));
    setReconcile(null);
    const { activeRules } = await import("../cities/reconcile.ts");
    const globals = globalThis as { location?: unknown };
    const had = "location" in globals;
    const previous = globals.location;
    globals.location = { search: "?reconcile=p" };
    try {
      assert.equal(activeRules().size, 0, '"p" selected a rule by prefix');
      assert.equal(said.length, 1);
    } finally {
      if (had) globals.location = previous;
      else delete globals.location;
      setReconcileWarn(null);
    }
  });
});
