// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later

import assert from "node:assert/strict";
import vm from "node:vm";
import { bundleEntry } from "./bundle-entry.mjs";

class ClassList {
  constructor() { this.values = new Set(); }
  add(...names) { names.forEach(name => this.values.add(name)); }
  remove(...names) { names.forEach(name => this.values.delete(name)); }
  contains(name) { return this.values.has(name); }
  toggle(name, force) {
    const enabled = force === undefined ? !this.contains(name) : force;
    if (enabled) this.add(name); else this.remove(name);
    return enabled;
  }
}

class Element {
  constructor(tagName, document) {
    this.tagName = tagName.toUpperCase();
    this.ownerDocument = document;
    this.children = [];
    this.dataset = {};
    this.attributes = new Map();
    this.classList = new ClassList();
    this.listeners = new Map();
    this.style = {};
    this.options = [];
    this.value = "";
    this.textContent = "";
  }
  set id(value) { this._id = value; if (value) this.ownerDocument.elements.set(value, this); }
  get id() { return this._id || ""; }
  set className(value) { this.classList = new ClassList(); value.split(/\s+/).filter(Boolean).forEach(name => this.classList.add(name)); }
  set innerHTML(value) {
    this._innerHTML = value;
    if (value.includes('value="confirm"')) {
      const title = new Element("h2", this.ownerDocument); title.id = "ui-confirm-title";
      const message = new Element("p", this.ownerDocument); message.id = "ui-confirm-message";
      const confirm = new Element("button", this.ownerDocument); confirm.value = "confirm";
      this.append(title, message, confirm);
      this._confirmButton = confirm;
    }
  }
  get innerHTML() { return this._innerHTML || ""; }
  appendChild(child) { this.children.push(child); child.parentElement = this; return child; }
  append(...children) { children.forEach(child => this.appendChild(child)); }
  insertBefore(child) { return this.appendChild(child); }
  remove() { if (this.parentElement) this.parentElement.children = this.parentElement.children.filter(child => child !== this); }
  replaceChildren(...children) { this.children = []; this.options = []; this.append(...children); }
  setAttribute(name, value) { this.attributes.set(name, String(value)); }
  getAttribute(name) { return this.attributes.get(name); }
  addEventListener(type, listener) { this.listeners.set(type, listener); }
  dispatch(type, event = {}) { this.listeners.get(type)?.({ target: this, preventDefault() {}, ...event }); }
  querySelector(selector) {
    if (selector === '[value="confirm"]') return this._confirmButton || null;
    return null;
  }
  querySelectorAll(selector) {
    if (selector === ".tab[data-tab]") return this.children.filter(child => child.dataset.tab);
    if (selector === ".sub-tab[data-subtab]") return this.children.filter(child => child.dataset.subtab);
    if (selector === '[role="tab"]') return this.children.filter(child => child.getAttribute("role") === "tab");
    return [];
  }
  add(option) { this.options.push(option); if (!this.value) this.value = option.value; }
  showModal() { this.open = true; }
  close(value) { this.returnValue = value; this.open = false; this.dispatch("close"); }
  focus() { this.focused = true; }
  click() { this.clicked = true; }
}

class DocumentFixture {
  constructor() {
    this.readyState = "loading";
    this.elements = new Map();
    this.body = new Element("body", this);
  }
  createElement(tagName) { return new Element(tagName, this); }
  getElementById(id) { return this.elements.get(id) || null; }
  querySelector() { return null; }
  querySelectorAll() { return []; }
  addEventListener() {}
}

const document = new DocumentFixture();
const storage = new Map();
const localStorage = {
  getItem(key) { return storage.has(key) ? storage.get(key) : null; },
  setItem(key, value) { storage.set(key, String(value)); },
};
const window = { document, localStorage, addEventListener() {} };
const context = vm.createContext({
  window, document, localStorage,
  HTMLElement: Element,
  Node: Element,
  Option: class Option { constructor(label, value) { this.label = label; this.value = value; } },
  MutationObserver: class MutationObserver { observe() {} },
  requestAnimationFrame(callback) { callback(); },
  setTimeout() { return 1; },
  clearTimeout() {},
  console,
});

const source = await bundleEntry(new URL("../src/ui-core.ts", import.meta.url));
new vm.Script(source, { filename: "ui-core.js" }).runInContext(context);
const ui = window.trxUi;

const toast = ui.notify("Saved", { kind: "success" });
assert.equal(toast.getAttribute("role"), "status");
assert.equal(toast.classList.contains("toast-success"), true);
assert.equal(document.getElementById("toast-region").children.length, 1);

const confirmation = ui.confirm({ title: "Delete?", message: "Permanent", confirmLabel: "Delete" });
const dialog = document.getElementById("ui-confirm-dialog");
assert.equal(dialog.open, true);
assert.equal(document.getElementById("ui-confirm-title").textContent, "Delete?");
dialog.close("confirm");
assert.equal(await confirmation, true);

const tabBar = new Element("div", document);
const firstTab = new Element("button", document); firstTab.dataset.tab = "main"; firstTab.classList.add("active");
const secondTab = new Element("button", document); secondTab.dataset.tab = "map";
tabBar.append(firstTab, secondTab);
const mainPanel = new Element("section", document); mainPanel.id = "tab-main";
const mapPanel = new Element("section", document); mapPanel.id = "tab-map";
ui.prepareTabList(tabBar, "primary");
assert.equal(firstTab.getAttribute("aria-selected"), "true");
tabBar.dispatch("keydown", { target: firstTab, key: "ArrowRight" });
assert.equal(secondTab.focused, true);
assert.equal(secondTab.clicked, true);

localStorage.setItem("trxOperatorLayout:rig-a", "broadcast");
ui.setActiveRig("rig-a");
assert.equal(document.body.dataset.operatorLayout, "compact");
ui.setLayoutCapabilities({ broadcast: true });
ui.setActiveRig("rig-a");
assert.equal(document.body.dataset.operatorLayout, "broadcast");
ui.setActiveRig("rig-b");
ui.applyLayout("digital");
assert.equal(document.body.dataset.operatorLayout, "compact");
assert.equal(localStorage.getItem("trxOperatorLayout:rig-b"), "compact");

// A layout seeds the collapsible sections but must not hold them there:
// applyLayout runs on every rig-state refresh, so an operator who expands a
// section a layout collapses by default had it shut again about once a second.
const schedulerSection = new Element("details", document);
schedulerSection.id = "scheduler-controls";
const audioSection = new Element("details", document);
audioSection.id = "audio-controls";

ui.setActiveRig("rig-c");
ui.applyLayout("compact");
assert.equal(schedulerSection.open, false, "compact seeds the scheduler section closed");

schedulerSection.open = true;             // operator expands it
ui.applyLayout("compact");                // the next rig-state refresh
assert.equal(schedulerSection.open, true, "re-applying the same layout leaves sections alone");
ui.setActiveRig("rig-c");
assert.equal(schedulerSection.open, true, "a rig-state refresh leaves sections alone");

// Choosing a different layout is an explicit request for its defaults.
ui.applyLayout("full");
assert.equal(schedulerSection.open, true, "full seeds the scheduler section open");
assert.equal(audioSection.open, true, "full seeds the audio section open");
audioSection.open = false;
ui.applyLayout("compact");
assert.equal(audioSection.open, false, "compact seeds the audio section closed");
assert.equal(schedulerSection.open, false, "switching layout reseeds every section");

console.log("ui-core component tests passed");
