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

import {
  CANONICAL_CAPABILITIES,
  DEVICE_RANGES,
  initialDeviceState,
} from "../../devices/types.ts";
import type { DeviceCommand, DeviceDeclaration, DeviceState } from "../../devices/types.ts";
import { FakeDocument, type FakeElement } from "./fakeDom.ts";
import { meterFraction, mountDevicePanel } from "../../ui/devicePanel.ts";

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

const SPEAKER: DeviceDeclaration = {
  id: "hq-spk-01",
  kind: "speaker",
  label: "Monitor speaker",
  assetId: "tera:device.speaker.desk",
  anchor: { levelId: "l1", propId: "desk-01" },
  capabilities: CANONICAL_CAPABILITIES.speaker,
  provenance: "simulated",
  disclosure: "Simulated studio hardware. Nothing here is a reading from a real room.",
};

function setup(declarations: readonly DeviceDeclaration[] = [MIC, SPEAKER]) {
  const doc = new FakeDocument();
  const host = doc.createElement("div");
  doc.body.append(host);
  const commands: DeviceCommand[] = [];
  const panel = mountDevicePanel(host as unknown as HTMLElement, {
    declarations,
    onCommand: (command) => commands.push(command),
  });
  return { doc, host, panel, commands, root: panel.root as unknown as FakeElement };
}

function card(root: FakeElement, id: string): FakeElement {
  const found = root.querySelector(`[data-device=${id}]`);
  assert.ok(found, `no card for ${id}`);
  return found;
}

function row(root: FakeElement, deviceId: string, capability: string): FakeElement {
  const found = card(root, deviceId).querySelector(`[data-capability=${capability}]`);
  assert.ok(found, `no ${capability} row on ${deviceId}`);
  return found;
}

describe("the device panel", () => {
  it("renders exactly one control per declared capability, and never switches on kind", () => {
    const { root } = setup();
    const micRows = card(root, MIC.id).querySelectorAll("[data-capability]");
    assert.equal(micRows.length, MIC.capabilities.length);
    assert.deepEqual(
      micRows.map((element) => element.getAttribute("data-capability")),
      [...MIC.capabilities],
    );

    const speakerRows = card(root, SPEAKER.id).querySelectorAll("[data-capability]");
    assert.equal(speakerRows.length, SPEAKER.capabilities.length);
    assert.deepEqual(
      speakerRows.map((element) => element.getAttribute("data-capability")),
      [...SPEAKER.capabilities],
    );
  });

  it("renders only the capabilities a device actually declares", () => {
    // A ceiling array with no gain stage. The panel must not grow a gain slider
    // for it — the capability list is per device, not per kind.
    const array: DeviceDeclaration = { ...MIC, id: "hq-mic-02", capabilities: ["power", "mute"] };
    const { root } = setup([array]);
    const rows = card(root, array.id).querySelectorAll("[data-capability]");
    assert.deepEqual(rows.map((r) => r.getAttribute("data-capability")), ["power", "mute"]);
    assert.equal(card(root, array.id).querySelector("[data-capability=gain]"), null);
  });

  it("prints the declaration's disclosure under every instrument", () => {
    // Per instrument, not once per panel: a disclosure at the top of a scrolling
    // list is one you have already scrolled past by the time you reach the meter
    // that needed it.
    const { root } = setup();
    for (const declaration of [MIC, SPEAKER]) {
      const text = card(root, declaration.id).text();
      assert.ok(
        text.includes(declaration.disclosure),
        `${declaration.id} does not show its disclosure`,
      );
      assert.match(text.toLowerCase(), /simulat/);
    }
  });

  it("starts at rest rather than blank, from the same initialDeviceState everything else uses", () => {
    const { root } = setup();
    assert.equal(row(root, MIC.id, "power").querySelector("button")?.getAttribute("aria-pressed"), "false");
    const gain = row(root, MIC.id, "gain").querySelector("input");
    assert.equal(gain?.value, String(DEVICE_RANGES.gain.initial));
    // The meter sits at the floor: a microphone nobody has switched on is not
    // hearing −20 dBFS of anything.
    assert.equal(row(root, MIC.id, "level").querySelector("i")?.style.width, "0.0%");
  });

  it("emits a well-formed, validated command on a switch", () => {
    const { root, commands } = setup();
    row(root, MIC.id, "power").querySelector("button")?.click();
    assert.deepEqual(commands, [{ deviceId: MIC.id, op: "power", value: true }]);

    row(root, SPEAKER.id, "playback").querySelector("button")?.click();
    assert.deepEqual(commands[1], { deviceId: SPEAKER.id, op: "playback", value: true });
  });

  it("toggles against what is on screen, not blindly to true", () => {
    // A toggle that always sends `true` is a button that works exactly once.
    const { root, panel, commands } = setup();
    panel.apply([{ ...initialDeviceState(MIC, 1), powered: true }]);
    row(root, MIC.id, "power").querySelector("button")?.click();
    assert.deepEqual(commands.at(-1), { deviceId: MIC.id, op: "power", value: false });
  });

  it("emits a clamped command from a slider, and refuses a nonsense one", () => {
    const { root, commands } = setup();
    const gain = row(root, MIC.id, "gain").querySelector("input");
    assert.ok(gain);

    gain.value = "18";
    gain.dispatch("input");
    assert.deepEqual(commands.at(-1), { deviceId: MIC.id, op: "gain", value: 18 });

    // Clamped, not refused: a slider reporting 1.0000000002 is not an attack.
    gain.value = String(DEVICE_RANGES.gain.max + 40);
    gain.dispatch("input");
    assert.deepEqual(commands.at(-1), { deviceId: MIC.id, op: "gain", value: DEVICE_RANGES.gain.max });

    const before = commands.length;
    gain.value = "not a number";
    gain.dispatch("input");
    assert.equal(commands.length, before, "a non-numeric slider value must be a silent no-op");
  });

  it("gives every slider the declared range and a label a screen reader can use", () => {
    const { root } = setup();
    const gain = row(root, MIC.id, "gain").querySelector("input");
    assert.equal(gain?.min, String(DEVICE_RANGES.gain.min));
    assert.equal(gain?.max, String(DEVICE_RANGES.gain.max));
    assert.match(gain?.getAttribute("aria-label") ?? "", /Desk mic Gain/);

    const volume = row(root, SPEAKER.id, "volume").querySelector("input");
    assert.equal(volume?.min, String(DEVICE_RANGES.volume.min));
    assert.equal(volume?.max, String(DEVICE_RANGES.volume.max));
  });

  it("never renders a command control for a read-only reading", () => {
    // `level` is a meter. `DeviceCommandOp` excludes it, and so does the panel.
    const { root, commands } = setup();
    const level = row(root, MIC.id, "level");
    assert.equal(level.querySelector("button"), null);
    assert.equal(level.querySelector("input"), null);
    assert.equal(level.querySelector(".tera-device__meter")?.getAttribute("role"), "meter");
    assert.equal(commands.length, 0);
  });

  it("shows a reading, and treats undefined as absent rather than as zero", () => {
    const { root, panel } = setup();
    const live: DeviceState = {
      ...initialDeviceState(MIC, 5),
      powered: true,
      gainDb: 22,
      levelDb: -12,
      muted: true,
    };
    panel.apply([live]);
    assert.equal(row(root, MIC.id, "gain").querySelector(".tera-device__value")?.textContent, "+22 dB");
    assert.equal(row(root, MIC.id, "level").querySelector(".tera-device__value")?.textContent, "-12 dBFS");
    assert.equal(row(root, MIC.id, "mute").querySelector("button")?.getAttribute("aria-pressed"), "true");
    assert.equal(row(root, MIC.id, "gain").getAttribute("data-unavailable"), "false");

    // A device that reports no gain at all has no gain, which is a different
    // statement from "gain is zero".
    const { gainDb: _dropped, ...withoutGain } = live;
    panel.apply([withoutGain as DeviceState]);
    assert.equal(row(root, MIC.id, "gain").getAttribute("data-unavailable"), "true");
    assert.equal(row(root, MIC.id, "gain").querySelector(".tera-device__value")?.textContent, "—");
  });

  it("maps a dBFS level onto the meter's travel", () => {
    assert.equal(meterFraction(DEVICE_RANGES.level.min), 0);
    assert.equal(meterFraction(DEVICE_RANGES.level.max), 1);
    assert.equal(meterFraction(-30), 0.5);
    assert.equal(meterFraction(undefined), 0);
    assert.equal(meterFraction(Number.NaN), 0);
    // Out of range is clamped, not wrapped or negative-width.
    assert.equal(meterFraction(-200), 0);
    assert.equal(meterFraction(20), 1);
  });

  it("mounts one stylesheet per document, however many panels are built", () => {
    const doc = new FakeDocument();
    const first = doc.createElement("div");
    const second = doc.createElement("div");
    doc.body.append(first, second);
    const a = mountDevicePanel(first as unknown as HTMLElement, {
      declarations: [MIC],
      onCommand: () => {},
    });
    const b = mountDevicePanel(second as unknown as HTMLElement, {
      declarations: [SPEAKER],
      onCommand: () => {},
    });
    assert.equal(doc.head.querySelectorAll("style").length, 1);
    b.dispose();
    a.dispose();
    assert.equal(doc.head.querySelectorAll("style").length, 0);
  });

  it("goes silent and detaches on dispose", () => {
    const { root, host, panel, commands } = setup();
    const power = row(root, MIC.id, "power").querySelector("button");
    panel.dispose();
    assert.equal(host.children.length, 0);
    power?.click();
    panel.apply([initialDeviceState(MIC, 9)]);
    assert.deepEqual(commands, [], "a disposed panel must not still be sending commands");
  });
});

/**
 * What the panel has to do once a device is real.
 *
 * Three additions, and each closes a way the panel could tell a viewer something
 * untrue about hardware in a room somebody is standing in.
 */

/** A microphone whose gain is a mixer position, not a preamp measurement. */
const YETI: DeviceDeclaration = {
  id: "la-mic-yeti",
  kind: "mic",
  label: "Blue Yeti Nano",
  assetId: "tera:device.mic.desk",
  anchor: { levelId: "l1", propId: "desk-01" },
  capabilities: ["power", "mute", "gain", "level"],
  ranges: { gain: { min: 0, max: 100, initial: 68, unit: "%" } },
  provenance: "first-party-sensor",
  disclosure: "Live reading from the studio's own desk microphone, north wall.",
  simulatedDisclosure:
    "Simulated in your browser. This deployment does not share the live room with visitors.",
};

describe("a device that declares its own range", () => {
  it("draws the slider between the declared bounds, not the global ones", () => {
    const { root } = setup([YETI]);
    const slider = row(root, YETI.id, "gain").querySelector("input");
    assert.ok(slider);
    assert.equal(slider.min, "0");
    assert.equal(slider.max, "100");
    // The global default is −12…+36 dB. A Yeti Nano's capture level is an ALSA
    // position on a 0–50 scale normalised to percent; drawing it on the dB
    // scale would peg the handle at the far right and label it "+36 dB".
    assert.notEqual(slider.max, String(DEVICE_RANGES.gain.max));
  });

  it("labels the reading in the declared unit and never invents decibels", () => {
    const { panel, root } = setup([YETI]);
    panel.apply([
      { id: YETI.id, kind: "mic", powered: true, gainDb: 68, observedAt: 1, synthetic: false },
    ]);
    const value = row(root, YETI.id, "gain").querySelector(".tera-device__value");
    assert.equal(value?.textContent, "68%");
    // "+68 dB" would be a guess wearing the typography of a measurement, and it
    // would look completely plausible next to a photograph of a desk.
    assert.ok(!String(value?.textContent).includes("dB"));
  });

  it("still reads the global range for a declaration that names none", () => {
    const { panel, root } = setup([MIC]);
    panel.apply([initialDeviceState(MIC, 1)]);
    const value = row(root, MIC.id, "gain").querySelector(".tera-device__value");
    assert.equal(value?.textContent, `+${DEVICE_RANGES.gain.initial.toFixed(0)} dB`);
  });
});

describe("which disclosure a viewer is actually reading", () => {
  it("prints the simulated sentence when a live declaration is being simulated", () => {
    // The hole the twin creates. An anonymous visitor cannot read the device
    // route, so they are handed the local simulator running THIS declaration —
    // and `disclosure` on a live device says the room is live.
    const { panel, root } = setup([YETI]);
    panel.apply([
      { id: YETI.id, kind: "mic", powered: true, gainDb: 68, observedAt: 1, synthetic: true },
    ]);
    const line = card(root, YETI.id).querySelector(".tera-device__disclosure");
    assert.equal(line?.textContent, YETI.simulatedDisclosure);
  });

  it("prints the live sentence once a reading is actually observed", () => {
    const { panel, root } = setup([YETI]);
    panel.apply([
      { id: YETI.id, kind: "mic", powered: true, gainDb: 68, observedAt: 1, synthetic: false },
    ]);
    const line = card(root, YETI.id).querySelector(".tera-device__disclosure");
    assert.equal(line?.textContent, YETI.disclosure);
  });

  it("leaves a simulated declaration's one sentence alone", () => {
    const { panel, root } = setup([MIC]);
    panel.apply([initialDeviceState(MIC, 1)]);
    assert.equal(
      card(root, MIC.id).querySelector(".tera-device__disclosure")?.textContent,
      MIC.disclosure,
    );
  });
});

describe("a device nobody could reach", () => {
  it("says so, and says how old the reading it is still showing is", () => {
    const { panel, root } = setup([YETI]);
    const observedAt = Date.now() - 5 * 60_000;
    panel.apply([
      {
        id: YETI.id,
        kind: "mic",
        powered: false,
        muted: true,
        reachable: false,
        observedAt,
        synthetic: false,
      },
    ]);
    const status = card(root, YETI.id).querySelector(".tera-device__status");
    assert.equal(status?.attributes.get("data-reachable"), "false");
    assert.match(String(status?.textContent), /Not reached/);
    assert.match(String(status?.textContent), /5 min ago/);
    // The mute reading it last reported still stands. `reachable: false` is not
    // `powered: false` — a microphone on a machine that is asleep is not a
    // switched-off microphone, and the panel must not redraw it as one.
    assert.equal(
      row(root, YETI.id, "mute").querySelector(".tera-device__value")?.textContent,
      "Muted",
    );
  });

  it("says nothing at all when reachability does not apply", () => {
    // Every simulated device. A state machine in this tab is never unreachable,
    // and a line claiming it was reached would be a fact about nothing.
    const { panel, root } = setup([MIC]);
    panel.apply([initialDeviceState(MIC, 1)]);
    const status = card(root, MIC.id).querySelector(".tera-device__status");
    assert.equal(status?.textContent, "");
    assert.equal(status?.attributes.get("data-reachable"), undefined);
  });
});
