/**
 * The chapter strip is a public interface, and this is the fast half of the
 * check that says so.
 *
 * `scripts/check-chapter-identity.mjs` is the whole contract: it boots the built
 * bundle under every `?city=` and compares the rendered `#chapters` strip —
 * `data-view`, `data-view-index`, the printed number and the short label —
 * against `scripts/fixtures/chapter-identity.json`. That takes a browser and
 * about a minute. This file re-states the part of the same fixture that lives in
 * the packs, so a reorder fails in `npm test` in a second, and so the negative
 * case can be *watched failing*: a guard nobody has seen fail is not a guard.
 *
 * ## What is actually being protected
 *
 * Twenty-six aims in `scripts/brand-assets/{shots,films,capture}.mjs` point a
 * camera at `#chapters .chapter[N]` — a **position** — and assert the short
 * label they find there; 21 of them at a board a `?city=` reaches. Every one is
 * a still, a film or the Open Graph card on lumbridgecorp.com. The
 * label assertion was added after a share card shipped for a fortnight showing
 * a chase camera on a freeway under the headline "Cities from above", and it is
 * a good guard with two holes:
 *
 * - Short labels are not unique across boards. "Whole Board" is chapter 01 of
 *   both metro boards and "The Valley" is a different valley on each of them, so
 *   a reorder that preserves labels passes every one of them and shoots the wrong place.
 * - `?city=` falls back to the first board rather than failing, so a board that
 *   lost its id serves California to a shot aimed at SoCal without an error.
 *
 * Identity here therefore means `(board, chapter id, position)` together, never
 * a label and never a position on its own.
 */

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { describe, it } from "node:test";

import CALIFORNIA_CITY from "../cities/california.ts";
import SAN_FRANCISCO_CITY from "../cities/sf.ts";
import SOCAL_CITY from "../cities/socal.ts";
import type { Chapter } from "../engine/types.ts";
import { chromeState } from "../ui/chromeState.ts";
import type { ChromeInputs } from "../ui/chromeState.ts";

interface FixtureChapter {
  index: number;
  dataView: string;
  dataViewIndex: string;
  number: string;
  shortLabel: string;
}
interface Fixture {
  takenAt: string;
  boards: Record<string, { activeBoard: string; chapters: FixtureChapter[] }>;
}

const fixture = JSON.parse(
  readFileSync(new URL("../../scripts/fixtures/chapter-identity.json", import.meta.url), "utf8"),
) as Fixture;

/** `?city=` value to the pack it selects — the same table `main.ts` holds. */
const BOARDS: readonly (readonly [string, { chapters: Chapter[] }])[] = [
  ["california", CALIFORNIA_CITY],
  ["sf", SAN_FRANCISCO_CITY],
  ["socal", SOCAL_CITY],
];

/**
 * Every way this list of chapters differs from the one the imagery was aimed at.
 *
 * A list of sentences rather than an assertion, so that the negative test can
 * call the same function on a deliberately broken list and check that it
 * *reports* rather than that it *throws*. The two callers below are the whole
 * point: one proves the packs are still what the fixture says, the other proves
 * this function can tell when they are not.
 */
function drift(boardId: string, chapters: readonly Chapter[]): string[] {
  const expected = fixture.boards[boardId];
  if (expected === undefined) return [`?city=${boardId}: not in the fixture`];
  const problems: string[] = [];
  if (expected.chapters.length !== chapters.length) {
    problems.push(
      `?city=${boardId}: ${chapters.length} chapters, the fixture has ${expected.chapters.length}`,
    );
  }
  for (let i = 0; i < Math.min(expected.chapters.length, chapters.length); i += 1) {
    const want = expected.chapters[i];
    const got = chapters[i];
    if (want === undefined || got === undefined) continue;
    // `data-view` is the chapter's id, and `data-view-index` is its position in
    // the rendered list — `mount.ts` writes both from the same array. Checking
    // them here is checking that the DOM identity and the pack identity are the
    // same fact, which is what lets a browser-free test stand in for the DOM one.
    if (got.id !== want.dataView) problems.push(`${boardId}[${i}] id ${got.id} ≠ ${want.dataView}`);
    if (String(i) !== want.dataViewIndex) {
      problems.push(`${boardId}[${i}] position ${i} ≠ ${want.dataViewIndex}`);
    }
    if (got.number !== want.number) {
      problems.push(`${boardId}[${i}] number ${got.number} ≠ ${want.number}`);
    }
    if (got.shortLabel !== want.shortLabel) {
      problems.push(`${boardId}[${i}] label "${got.shortLabel}" ≠ "${want.shortLabel}"`);
    }
  }
  return problems;
}

function inputs(overrides: Partial<ChromeInputs> = {}): ChromeInputs {
  return {
    mode: "overview",
    available: ["overview"],
    access: { tier: "anon", signInUrl: "/login.html", subject: null },
    inside: false,
    officeDepth: null,
    viewport: { width: 1440, height: 900, coarsePointer: false },
    feeds: { markers: false, weather: false, flights: false },
    degraded: [],
    devices: [],
    firstVisit: false,
    panelOpen: true,
    planOpen: true,
    ...overrides,
  };
}

describe("chapter identity", () => {
  it("matches the fixture the marketing imagery was aimed at", () => {
    const problems = BOARDS.flatMap(([id, city]) => drift(id, city.chapters));
    assert.deepEqual(
      problems,
      [],
      `The chapter strip moved. Every line below is a capture aim in ` +
        `scripts/brand-assets/** that now points somewhere else:\n  ${problems.join("\n  ")}`,
    );
  });

  it("is 26 rungs over three boards, which is the number the ladder has to carry", () => {
    // Not a style assertion: the ladder work re-expresses these 26 as one
    // ordered descent, and 24 of them are camera poses — `los-angeles` and
    // `san-francisco` on the state board are doors that call `switchCity`
    // rather than `flyTo`, which is why two capture presets that claimed to be
    // California closeups were in fact photographs of the metro boards.
    assert.equal(CALIFORNIA_CITY.chapters.length, 6);
    assert.equal(SAN_FRANCISCO_CITY.chapters.length, 12);
    assert.equal(SOCAL_CITY.chapters.length, 8);
    assert.equal(
      BOARDS.reduce((total, [, city]) => total + city.chapters.length, 0),
      26,
    );
  });

  it("puts the pack's id and short label straight onto the button", () => {
    // The DOM contract, stated where it can be checked without a browser:
    // `chromeState` maps a chapter to a row `id`/`label`, and `mount.ts` writes
    // that `id` into `data-view` and the row's position into `data-view-index`.
    // If this mapping ever stops being the identity function, the fixture above
    // stops standing in for the DOM and `check-chapter-identity.mjs` becomes the
    // only real guard.
    for (const [boardId, city] of BOARDS) {
      const state = chromeState(
        inputs({ views: city.chapters, activeViewId: city.chapters[0]?.id ?? null }),
      );
      assert.deepEqual(
        state.views.map((view) => view.id),
        city.chapters.map((chapter) => chapter.id),
        `${boardId}: row ids are not the pack's chapter ids`,
      );
      assert.deepEqual(
        state.views.map((view) => view.label),
        city.chapters.map((chapter) => chapter.shortLabel),
        `${boardId}: row labels are not the pack's short labels`,
      );
      assert.deepEqual(
        state.views.map((view) => view.number),
        city.chapters.map((chapter) => chapter.number),
        `${boardId}: printed numbers are not the pack's numbers`,
      );
    }
  });

  it("cannot be guarded by short label alone, because labels collide across boards", () => {
    // This is the reason the index-and-label aims are not enough, stated
    // as a fact about the data rather than as an opinion. If this assertion ever
    // fails because the labels became unique, the aims are still aimed by
    // position and this file still has to exist — but the failure mode gets
    // quieter, which is worth knowing about.
    const labels = BOARDS.flatMap(([, city]) => city.chapters.map((chapter) => chapter.shortLabel));
    const collisions = labels.filter((label, i) => labels.indexOf(label) !== i);
    assert.deepEqual([...new Set(collisions)].sort(), ["The Valley", "Whole Board"]);
  });

  describe("the guard itself", () => {
    // A guard nobody has watched fail is not a guard. Each of these breaks the
    // list in one of the three ways a ladder rewrite realistically breaks it,
    // and asserts that `drift` says so.

    it("fails when two chapters are swapped, even though every label survives", () => {
      const swapped = [...SAN_FRANCISCO_CITY.chapters];
      const [a, b] = [swapped[2], swapped[3]];
      assert.ok(a !== undefined && b !== undefined);
      swapped[2] = b;
      swapped[3] = a;
      const problems = drift("sf", swapped);
      assert.ok(problems.length > 0, "a reorder went unnoticed");
      assert.ok(
        problems.some((line) => line.includes("sf[2] id")),
        `expected a complaint about sf[2], got:\n  ${problems.join("\n  ")}`,
      );
    });

    it("fails when a chapter is inserted, which shifts every index after it", () => {
      const inserted: Chapter[] = [...SOCAL_CITY.chapters];
      const first = inserted[0];
      assert.ok(first !== undefined);
      inserted.splice(1, 0, { ...first, id: "san-diego", shortLabel: "San Diego", number: "99" });
      const problems = drift("socal", inserted);
      assert.ok(problems.length > 0, "an insertion went unnoticed");
      assert.ok(
        problems.some((line) => line.includes("9 chapters")),
        `expected a complaint about the count, got:\n  ${problems.join("\n  ")}`,
      );
    });

    it("fails when a chapter keeps its place and changes its id", () => {
      // The ladder rewrite's most likely shape: same 26 rungs, renamed to a
      // single sequence. `data-view` is what a re-pointed harness would key on,
      // so a silent rename is the one drift that would otherwise look like
      // nothing at all.
      const renamed = CALIFORNIA_CITY.chapters.map((chapter, i) =>
        i === 1 ? { ...chapter, id: "rung-02" } : chapter,
      );
      const problems = drift("california", renamed);
      assert.deepEqual(problems, ["california[1] id rung-02 ≠ la-sf-us-101"]);
    });
  });
});
