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

import { CANONICAL_CAPABILITIES } from "../../devices/types.ts";
import type { DeviceCommand, DeviceDeclaration } from "../../devices/types.ts";
import type { ControlMode } from "../../play/controlMode.ts";
import { chromeState } from "../../ui/chromeState.ts";
import type { ChromeInputs } from "../../ui/chromeState.ts";
import { markOverflow, mountChrome } from "../../ui/mount.ts";
import type { ChromeMountOptions } from "../../ui/mount.ts";
import { FakeDocument, type FakeElement, el, fakeStorage } from "./fakeDom.ts";

const MIC: DeviceDeclaration = {
  id: "hq-mic-01",
  kind: "mic",
  label: "Desk mic",
  assetId: "tera:device.mic.desk",
  anchor: { levelId: "l1", propId: "desk-01" },
  capabilities: CANONICAL_CAPABILITIES.mic,
  provenance: "simulated",
  disclosure: "Simulated studio hardware. This is demonstration data, never presence data.",
};

/**
 * The page, as `mount.ts` expects to find it.
 *
 * Built rather than parsed: the ids are the contract between this applier and
 * `index.html`, and `stylesheet.test.ts` asserts the real document carries every
 * one of them. Between the two tests, a renamed id fails somewhere rather than
 * silently ceasing to apply one decision.
 */
function buildPage(): FakeDocument {
  const doc = new FakeDocument();
  const body = doc.body;

  el(doc, body, "canvas", "scene");
  el(doc, body, "button", "panel-toggle");
  const panel = el(doc, body, "div", "panel");
  el(doc, panel, "span", "panel-toggle-label");
  el(doc, body, "div", "scrim");
  el(doc, panel, "h1", "title");
  el(doc, panel, "p", "subtitle");
  el(doc, panel, "p", "clock");
  el(doc, panel, "p", "boards-title");
  el(doc, panel, "nav", "cities");
  el(doc, panel, "button", "enter");
  el(doc, panel, "button", "walk");
  el(doc, panel, "button", "fly");
  el(doc, panel, "button", "screens");
  el(doc, panel, "button", "devices");
  const deviceSection = el(doc, panel, "section", "device-section");
  el(doc, deviceSection, "div", "device-host");
  el(doc, panel, "p", "office-invite");
  el(doc, panel, "p", "office-note");
  el(doc, panel, "nav", "chapters");
  el(doc, panel, "p", "blurb");

  const topright = el(doc, body, "div", "topright");
  const tier = el(doc, topright, "p", "tier");
  el(doc, tier, "span", "tier-label");
  el(doc, tier, "span", "tier-who");
  el(doc, tier, "a", "tier-signin");
  el(doc, tier, "button", "tier-character");
  el(doc, topright, "p", "tier-adds");
  el(doc, topright, "div", "presence-host");
  const corner = el(doc, topright, "aside", "corner");
  const minimap = el(doc, corner, "div", "minimap");
  el(doc, minimap, "p", "minimap-readout");

  const dock = el(doc, body, "nav", "mode-dock");
  for (const mode of ["overview", "drive", "actor", "aircraft", "office-walk"]) {
    const button = doc.createElement("button");
    button.setAttribute("data-control-mode", mode);
    dock.append(button);
  }

  const hud = el(doc, body, "section", "play-hud");
  el(doc, hud, "span", "play-hud-mode");
  el(doc, hud, "strong", "play-hud-primary");
  el(doc, hud, "span", "play-hud-status");

  const rail = el(doc, body, "div", "rail");
  const detail = el(doc, rail, "div", "detail");
  el(doc, detail, "div", "detail-text");
  el(doc, detail, "button", "detail-close");
  el(doc, rail, "div", "hint");
  el(doc, rail, "button", "plan-toggle");
  el(doc, rail, "button", "help");

  const touch = el(doc, body, "div", "touch-play-controls");
  const stick = el(doc, touch, "div", "play-stick");
  el(doc, stick, "span", "play-stick-knob");
  el(doc, stick, "span", "play-stick-label");
  for (const id of [
    "touch-primary", "touch-secondary", "touch-pitch-up", "touch-pitch-down",
    "touch-assist", "touch-reset", "touch-camera", "touch-map",
  ]) {
    el(doc, touch, "button", id);
  }

  el(doc, body, "p", "source");
  const shortcuts = el(doc, body, "div", "shortcuts");
  shortcuts.hidden = true;
  el(doc, shortcuts, "div", "shortcuts-body");
  el(doc, shortcuts, "button", "shortcuts-close");
  el(doc, body, "div", "onboarding-host");

  return doc;
}

function baseInputs(overrides: Partial<ChromeInputs> = {}): ChromeInputs {
  return {
    mode: "overview",
    available: ["overview", "drive", "actor", "aircraft"],
    access: { tier: "anon", signInUrl: "/login.html", subject: null },
    inside: false,
    officeDepth: null,
    viewport: { width: 1440, height: 900, coarsePointer: false },
    feeds: { markers: false, weather: true, flights: true },
    degraded: [],
    devices: [],
    firstVisit: false,
    panelOpen: true,
    planOpen: true,
    board: {
      cityId: "california",
      cityLabel: "California",
      officeLabel: "SF HQ",
      officeStatus: "active",
      isCalifornia: true,
    },
    boards: [
      { id: "california", label: "California" },
      { id: "bay-area", label: "Bay Area" },
    ],
    views: [
      { id: "one", shortLabel: "Approach", description: "A long way out." },
      { id: "two", shortLabel: "Paseo" },
    ],
    activeViewId: "one",
    ...overrides,
  };
}

interface Recorded {
  boards: string[];
  views: string[];
  modes: ControlMode[];
  panel: boolean[];
  plan: boolean[];
  holds: [string, boolean][];
  edges: string[];
  stick: ({ moveX: number; moveY: number } | null)[];
  commands: DeviceCommand[];
  clicks: string[];
}

function setup(extra: Partial<ChromeMountOptions> = {}) {
  const doc = buildPage();
  const recorded: Recorded = {
    boards: [], views: [], modes: [], panel: [], plan: [],
    holds: [], edges: [], stick: [], commands: [], clicks: [],
  };
  const chrome = mountChrome(doc as unknown as Document, {
    storage: fakeStorage(),
    onSelectBoard: (id) => recorded.boards.push(id),
    onSelectView: (id) => recorded.views.push(id),
    onMode: (mode) => recorded.modes.push(mode),
    onTogglePanel: (open) => recorded.panel.push(open),
    onTogglePlan: (open) => recorded.plan.push(open),
    onTouchHold: (control, pressed) => recorded.holds.push([control, pressed]),
    onTouchEdge: (edge) => recorded.edges.push(edge),
    onStick: (axes) => recorded.stick.push(axes),
    onDeviceCommand: (command) => recorded.commands.push(command),
    onEnter: () => recorded.clicks.push("enter"),
    onWalk: () => recorded.clicks.push("walk"),
    onFly: () => recorded.clicks.push("fly"),
    onScreens: () => recorded.clicks.push("screens"),
    onDevices: () => recorded.clicks.push("devices"),
    onCharacter: () => recorded.clicks.push("character"),
    onDismissDetail: () => recorded.clicks.push("detail-close"),
    ...extra,
  });
  const id = (name: string): FakeElement => {
    const found = doc.querySelector(`#${name}`);
    assert.ok(found, `#${name} missing from the fixture`);
    return found;
  };
  return { doc, chrome, recorded, id };
}

describe("mountChrome", () => {
  it("applies a whole frame of chrome from one call", () => {
    const { chrome, id } = setup();
    chrome.apply(chromeState(baseInputs()));

    assert.equal(id("title").textContent, "California");
    assert.equal(id("subtitle").textContent, "Tera · Lumbridge Simulate");
    assert.equal(id("boards-title").textContent, "Boards");
    assert.equal(id("cities").children.length, 2);
    assert.equal(id("chapters").children.length, 2);
    assert.equal(id("enter").textContent, "Open SF HQ →");
    assert.equal(id("blurb").hidden, false);
    assert.equal(id("blurb").textContent, "A long way out.");
    assert.equal(id("source").textContent, "live weather · live traffic");
    assert.equal(id("source").classList.contains("live"), true);
  });

  it("marks the active board and the active view with aria-pressed, not a class", () => {
    const { chrome, id } = setup();
    chrome.apply(chromeState(baseInputs()));
    const boards = id("cities").children;
    assert.equal(boards[0]?.getAttribute("aria-pressed"), "true");
    assert.equal(boards[1]?.getAttribute("aria-pressed"), "false");
    const views = id("chapters").children;
    assert.equal(views[0]?.getAttribute("aria-pressed"), "true");
    assert.equal(views[0]?.querySelector(".num")?.textContent, "01");
  });

  it("rebuilds a list only when its content changed", () => {
    // `replaceChildren` on the chapter list every frame would destroy focus and
    // restart every transition on it.
    const { chrome, id } = setup();
    chrome.apply(chromeState(baseInputs()));
    const first = id("chapters").children[0];
    chrome.apply(chromeState(baseInputs()));
    assert.equal(id("chapters").children[0], first, "the list was rebuilt for nothing");

    chrome.apply(chromeState(baseInputs({ activeViewId: "two" })));
    assert.notEqual(id("chapters").children[0], first);
  });

  it("routes a board click and a chapter click back by id", () => {
    const { chrome, id, recorded } = setup();
    chrome.apply(chromeState(baseInputs()));
    id("cities").children[1]?.click();
    assert.deepEqual(recorded.boards, ["bay-area"]);
    // The click lands on the inner span; `closest` has to find the button.
    id("chapters").children[1]?.querySelector(".num")?.click();
    assert.deepEqual(recorded.views, ["two"]);
  });

  it("drives the dock from the state and reports a press back", () => {
    const { chrome, id, recorded } = setup();
    chrome.apply(chromeState(baseInputs({ mode: "drive" })));
    const buttons = id("mode-dock").children;
    assert.equal(buttons[0]?.getAttribute("aria-pressed"), "false");
    assert.equal(buttons[1]?.getAttribute("aria-pressed"), "true");
    assert.equal(buttons[4]?.hidden, true, "office-walk is not available outside a building");
    buttons[2]?.click();
    assert.deepEqual(recorded.modes, ["actor"]);
  });

  it("hides the dock entirely rather than drawing one pressed button", () => {
    const { chrome, id } = setup();
    chrome.apply(chromeState(baseInputs({ available: ["overview"] })));
    assert.equal(id("mode-dock").hidden, true);
  });

  it("writes the play HUD only when there is telemetry, and colours a warning", () => {
    const { chrome, id } = setup();
    chrome.apply(chromeState(baseInputs()));
    assert.equal(id("play-hud").hidden, true);

    chrome.apply(
      chromeState(
        baseInputs({
          mode: "drive",
          telemetry: {
            kind: "drive",
            speedMps: 30,
            roadName: "I-5",
            driveMode: "manual",
            progress: 0.9,
            camera: "driver",
            guardrailContact: true,
            collisionRisk: 0.2,
          },
        }),
      ),
    );
    assert.equal(id("play-hud").hidden, false);
    assert.equal(id("play-hud-mode").textContent, "Drive");
    assert.match(id("play-hud-primary").textContent, /I-5/);
    assert.equal(id("play-hud-status").classList.contains("warning"), true);
  });

  it("renders an aircraft pick as a card rather than a sentence", () => {
    const { chrome, id } = setup();
    chrome.apply(
      chromeState(
        baseInputs({
          detail: {
            kind: "aircraft",
            aircraft: { id: "a1b2c3", callsign: "SWA 44", lat: 34.05, lng: -118.24, altitude: 2400, heading: 270 },
          },
        }),
      ),
    );
    assert.equal(id("detail").hidden, false);
    const text = id("detail-text").text();
    assert.match(text, /SWA 44/);
    assert.match(text, /Mode S A1B2C3/);
    assert.match(text, /7,874 ft/);
    assert.match(text, /270° W/);
  });

  it("replaces the aircraft card when a plain marker is picked next", () => {
    const { chrome, id } = setup();
    chrome.apply(
      chromeState(
        baseInputs({
          detail: { kind: "aircraft", aircraft: { id: "a1", lat: 0, lng: 0, altitude: 0, heading: 0 } },
        }),
      ),
    );
    chrome.apply(chromeState(baseInputs({ detail: { kind: "text", text: "A marker." } })));
    assert.equal(id("detail-text").text(), "A marker.");
    assert.equal(id("detail-text").querySelector(".aircraft-card"), null);
  });

  it("keeps the top-right column in one piece", () => {
    const { chrome, id } = setup();
    chrome.apply(chromeState(baseInputs()));
    assert.equal(id("tier").hidden, false);
    assert.equal(id("tier-label").textContent, "Open demo");
    assert.equal(id("tier-who").hidden, true);
    assert.equal(id("tier-signin").hidden, false);
    assert.equal(id("tier-signin").href, "/login.html");
    assert.equal(id("tier-adds").hidden, false);
    assert.match(id("tier-adds").textContent, /Sign in adds/);
    assert.equal(id("tier-character").hidden, true);

    chrome.apply(
      chromeState(
        baseInputs({
          access: { tier: "god", signInUrl: null, subject: "a@b.c", displayName: "Karti", hasProfile: true },
        }),
      ),
    );
    assert.equal(id("tier").getAttribute("data-tier"), "god");
    assert.equal(id("tier-who").textContent, "Karti");
    assert.equal(id("tier-signin").hidden, true);
    assert.equal(id("tier-adds").hidden, true);
    assert.equal(id("tier-character").hidden, false);
  });

  it("diffs the body classes rather than accumulating them", () => {
    const { doc, chrome } = setup();
    chrome.apply(chromeState(baseInputs({ panelOpen: false })));
    assert.equal(doc.body.classList.contains("panel-closed"), true);
    chrome.apply(chromeState(baseInputs({ panelOpen: true })));
    assert.equal(doc.body.classList.contains("panel-closed"), false);
    assert.equal(doc.body.classList.contains("layout-desktop"), true);
  });

  it("wires the panel and plan toggles to the state's own next value", () => {
    const { chrome, id, recorded } = setup();
    chrome.apply(chromeState(baseInputs({ panelOpen: true, planOpen: false })));
    id("panel-toggle").click();
    id("plan-toggle").click();
    assert.deepEqual(recorded.panel, [false]);
    assert.deepEqual(recorded.plan, [true]);
  });

  it("holds a touch control down on pointerdown and releases it on pointerup", () => {
    // A click is a press *and* a release, and a sprint button bound to a click
    // sprints for zero milliseconds.
    const { chrome, id, recorded } = setup();
    chrome.apply(chromeState(baseInputs({ mode: "drive", viewport: { width: 390, height: 844, coarsePointer: true } })));
    const primary = id("touch-primary");
    primary.dispatch("pointerdown", { pointerId: 1 });
    assert.deepEqual(recorded.holds, [["primary", true]]);
    assert.equal(primary.getAttribute("aria-pressed"), "true");
    primary.dispatch("pointerup", { pointerId: 1 });
    assert.deepEqual(recorded.holds.at(-1), ["primary", false]);
    assert.equal(primary.getAttribute("aria-pressed"), "false");

    // A finger that slides off the button still releases it.
    primary.dispatch("pointerdown", { pointerId: 2 });
    primary.dispatch("lostpointercapture", { pointerId: 2 });
    assert.deepEqual(recorded.holds.at(-1), ["primary", false]);
  });

  it("fires the one-shot actions on a click", () => {
    const { chrome, id, recorded } = setup();
    chrome.apply(chromeState(baseInputs({ mode: "drive" })));
    id("touch-assist").click();
    id("touch-reset").click();
    id("touch-camera").click();
    assert.deepEqual(recorded.edges, ["assist", "reset", "camera"]);
  });

  it("drives the analogue stick and returns to centre on release", () => {
    const { chrome, id, recorded } = setup();
    chrome.apply(
      chromeState(baseInputs({ mode: "actor", viewport: { width: 390, height: 844, coarsePointer: true } })),
    );
    const stick = id("play-stick");
    // The fixture's bounds are 100x100 at the origin, so the centre is (50, 50).
    stick.dispatch("pointerdown", { pointerId: 7, clientX: 100, clientY: 50 });
    assert.deepEqual(recorded.stick.at(-1), { moveX: 1, moveY: 0 });
    assert.equal(id("play-stick-knob").style.getPropertyValue("--stick-x"), "26.0px");

    stick.dispatch("pointermove", { pointerId: 7, clientX: 50, clientY: 0 });
    assert.deepEqual(recorded.stick.at(-1), { moveX: 0, moveY: 1 });

    // A second finger's move must not steal the gesture.
    const before = recorded.stick.length;
    stick.dispatch("pointermove", { pointerId: 9, clientX: 0, clientY: 0 });
    assert.equal(recorded.stick.length, before);

    stick.dispatch("pointerup", { pointerId: 7 });
    assert.equal(recorded.stick.at(-1), null);
    assert.equal(id("play-stick-knob").style.getPropertyValue("--stick-x"), "0.0px");
  });

  it("names what the joystick moves", () => {
    const { chrome, id } = setup();
    chrome.apply(
      chromeState(baseInputs({ mode: "drive", viewport: { width: 390, height: 844, coarsePointer: true } })),
    );
    assert.equal(id("play-stick-label").textContent, "Steer");
    assert.match(id("play-stick").getAttribute("aria-label") ?? "", /Steer joystick/);
  });

  it("builds the ? sheet from the keymap, and opens and closes it", () => {
    const { chrome, id } = setup();
    chrome.apply(chromeState(baseInputs()));
    assert.ok(id("shortcuts-body").querySelectorAll("dt").length > 10);
    assert.equal(chrome.shortcutsOpen(), false);
    id("help").click();
    assert.equal(chrome.shortcutsOpen(), true);
    id("shortcuts-close").click();
    assert.equal(chrome.shortcutsOpen(), false);
  });

  it("mounts the device panel once per office and feeds it separately", () => {
    const { chrome, id, recorded } = setup();
    const inside = baseInputs({
      inside: true,
      mode: "office-overview",
      available: ["office-overview", "office-walk"],
      officeDepth: "public",
      devices: [MIC],
    });
    chrome.apply(chromeState(inside));
    assert.equal(id("device-section").hidden, false);
    const panel = id("device-host").children[0];
    assert.ok(panel);
    assert.match(id("device-host").text(), /Desk mic/);
    assert.match(id("device-host").text(), /Simulated studio hardware/);

    // A second frame with the same devices must not rebuild it — a mixing desk
    // that rebuilt its sliders every frame would throw away a drag in progress.
    chrome.apply(chromeState(inside));
    assert.equal(id("device-host").children[0], panel);

    chrome.applyDeviceStates([
      { id: MIC.id, kind: "mic", powered: true, muted: false, gainDb: 20, levelDb: -18, observedAt: 1, synthetic: true },
    ]);
    assert.match(id("device-host").text(), /\+20 dB/);

    id("device-host")
      .querySelector("[data-capability=mute]")
      ?.querySelector("button")
      ?.click();
    assert.deepEqual(recorded.commands, [{ deviceId: MIC.id, op: "mute", value: true }]);

    // And it goes away with the building.
    chrome.apply(chromeState(baseInputs()));
    assert.equal(id("device-section").hidden, true);
    assert.equal(id("device-host").children.length, 0);
  });

  it("mounts the coach on a first visit and takes it away afterwards", () => {
    const { chrome, id } = setup();
    chrome.apply(chromeState(baseInputs({ firstVisit: true })));
    assert.equal(id("onboarding-host").children.length, 1);
    assert.match(id("onboarding-host").text(), /Fly the board/);

    chrome.apply(chromeState(baseInputs({ firstVisit: false })));
    assert.equal(id("onboarding-host").children.length, 0);
  });

  it("describes the canvas for whatever is happening on it", () => {
    const { chrome, id } = setup();
    chrome.apply(chromeState(baseInputs({ mode: "drive" })));
    assert.match(id("scene").getAttribute("aria-label") ?? "", /following your car/);
    chrome.apply(chromeState(baseInputs()));
    assert.match(id("scene").getAttribute("aria-label") ?? "", /Map of California/);
  });

  it("unbinds everything and cleans the body on dispose", () => {
    const { doc, chrome, id, recorded } = setup();
    chrome.apply(chromeState(baseInputs({ panelOpen: false, firstVisit: true })));
    assert.equal(doc.body.classList.contains("panel-closed"), true);
    chrome.dispose();
    assert.equal(doc.body.classList.contains("panel-closed"), false);
    assert.equal(id("onboarding-host").children.length, 0);
    id("enter").click();
    id("panel-toggle").click();
    assert.deepEqual(recorded.clicks, []);
    assert.deepEqual(recorded.panel, []);
  });

  it("does nothing at all when a handler was never wired", () => {
    // Every handler is optional so `integration` can adopt this a call site at a
    // time. An unwired control must be inert, not a thrown error.
    const doc = buildPage();
    const chrome = mountChrome(doc as unknown as Document, { storage: null });
    assert.doesNotThrow(() => chrome.apply(chromeState(baseInputs())));
    assert.doesNotThrow(() => doc.querySelector("#enter")?.click());
    assert.doesNotThrow(() => doc.querySelector("#mode-dock")?.children[1]?.click());
    chrome.dispose();
  });
});

describe("the places rail's overflow edge", () => {
  /** The bare minimum a real element offers: three numbers and an attribute bag. */
  function rail(scrollTop: number, clientHeight: number, scrollHeight: number) {
    const attributes = new Map<string, string>();
    return {
      scrollTop,
      clientHeight,
      scrollHeight,
      attributes,
      setAttribute(name: string, value: string) {
        attributes.set(name, value);
      },
    };
  }

  function edgeOf(scrollTop: number, clientHeight: number, scrollHeight: number) {
    const node = rail(scrollTop, clientHeight, scrollHeight);
    markOverflow(node as unknown as HTMLElement);
    return node.attributes.get("data-overflow");
  }

  it("says nothing is clipped when the list fits", () => {
    assert.equal(edgeOf(0, 400, 400), "none");
  });

  it("fades the bottom when there are rows below the cap", () => {
    assert.equal(edgeOf(0, 352, 900), "bottom");
  });

  it("fades the top once the reader has scrolled past the first row", () => {
    assert.equal(edgeOf(548, 352, 900), "top");
  });

  it("fades both ends in the middle of a long list", () => {
    assert.equal(edgeOf(200, 352, 900), "both");
  });

  /**
   * The bug this guards: fractional layout means a list scrolled fully down
   * reports a scrollTop a fraction of a pixel short, and a `<` with no slack
   * leaves the bottom permanently dimmed over nothing.
   */
  it("treats a sub-pixel shortfall at either end as arrival", () => {
    assert.equal(edgeOf(0.4, 352, 352.4), "none");
    assert.equal(edgeOf(547.6, 352.2, 900), "top");
  });

  /**
   * This module is tested with no DOM, and half the ways of running it — the
   * `FakeElement` harness, a node without layout, an element measured before
   * first paint — have no geometry to report. That must set no attribute at
   * all, rather than `data-overflow="NaN"` matching no rule and masking nothing
   * while looking like a live value in the inspector.
   */
  it("sets nothing at all when there is no geometry to read", () => {
    const node = rail(Number.NaN, Number.NaN, Number.NaN);
    markOverflow(node as unknown as HTMLElement);
    assert.equal(node.attributes.has("data-overflow"), false);

    const bare = { setAttribute() {} } as unknown as HTMLElement;
    assert.doesNotThrow(() => markOverflow(bare));
    assert.doesNotThrow(() => markOverflow(null));
  });
});
