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

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

class ElementFixture {
  constructor() {
    this.value = "";
    this.textContent = "";
    this.innerHTML = "";
    this.style = {};
    this.dataset = {};
    this.options = [];
    this.selectedOptions = [];
    this.children = [];
  }
  addEventListener() {}
  appendChild(child) { this.children.push(child); return child; }
  add(child) { this.options.push(child); }
  remove(index) { this.options.splice(index, 1); }
  focus() {}
  querySelector() { return null; }
}

// Mirrors the `window.trx` host contract published by app.ts.  The plugin is a
// separate bundle, so every application service it uses arrives this way.
function hostFixture(overrides = {}) {
  const calls = { postPath: [], setRigFrequency: [], armOptimisticFrequency: [], applyLocalTunedFrequency: [], syncBandwidthInput: [], scheduleSpectrumDraw: 0, syncModePicker: 0 };
  const state = {
    authEnabled: false,
    authRole: "control",
    lastActiveRigId: null,
    lastRigIds: [],
    lastRigDisplayNames: {},
    lastFreqHz: 14_074_000,
    lastModeName: "USB",
    lastSpectrumData: null,
    currentBandwidthHz: 2400,
    decoderRegistry: [],
    ...overrides,
  };
  const core = {
    postPath: async (path) => { calls.postPath.push(path); },
    setRigFrequency: (hz) => { calls.setRigFrequency.push(hz); },
    applyLocalTunedFrequency: (hz, force) => { calls.applyLocalTunedFrequency.push([hz, force]); },
    armOptimisticFrequency: (hz) => { calls.armOptimisticFrequency.push(hz); },
    syncBandwidthInput: (hz) => { calls.syncBandwidthInput.push(hz); },
    scheduleSpectrumDraw: () => { calls.scheduleSpectrumDraw += 1; },
    syncModePicker: () => { calls.syncModePicker += 1; },
    onDecoderRegistryReady: () => {},
  };
  return { window: { trx: { state, core, modules: {} }, trxUi: { confirm: async () => true } }, calls };
}

function documentFixture(element) {
  return {
    getElementById: element,
    querySelector: () => new ElementFixture(),
    querySelectorAll: () => [],
    createElement: () => new ElementFixture(),
    createTextNode: (text) => ({ textContent: text }),
    addEventListener() {},
  };
}

const source = await bundleEntry(new URL("../src/plugins/bookmarks.ts", import.meta.url));

test("bookmarks register an explicit typed service for application consumers", async () => {
  const elements = new Map();
  const element = (id) => {
    if (!elements.has(id)) elements.set(id, new ElementFixture());
    return elements.get(id);
  };
  element("bm-scope-picker").value = "general";
  element("bm-category-filter").options.push({ value: "" });
  element("bm-mode-filter").options.push({ value: "" });
  const bookmarks = [{ id: "one", name: "Local", freq_hz: 145_500_000, mode: "FM", scope: "general" }];
  const { window } = hostFixture();
  const context = vm.createContext({
    window,
    document: documentFixture(element),
    fetch: async () => ({ ok: true, json: async () => bookmarks }),
    CSS: { escape: (value) => value },
    Element: ElementFixture,
    console,
  });
  new vm.Script(source).runInContext(context);
  await new Promise((resolve) => setImmediate(resolve));

  assert.equal(window.trx.modules.bookmarks.overlayList.length, 1);
  assert.equal(window.trx.modules.bookmarks.overlayList[0].id, "one");
  assert.equal(typeof window.trx.modules.bookmarks.apply, "function");
  assert.equal(typeof window.trx.modules.bookmarks.formatFrequency, "function");
  assert.equal(typeof window.trx.modules.bookmarks.populateScopePicker, "function");
  assert.equal(globalThis.bmOverlayList, undefined);
});

test("applying a bookmark drives tuning through the host services", async () => {
  const elements = new Map();
  const element = (id) => {
    if (!elements.has(id)) elements.set(id, new ElementFixture());
    return elements.get(id);
  };
  const { window, calls } = hostFixture({
    lastActiveRigId: "sdr",
    decoderRegistry: [
      { id: "ft8", label: "FT8", activation: "toggle", active_modes: ["USB"], bookmark_selectable: true },
      { id: "cw", label: "CW", activation: "toggle", active_modes: ["CW"], bookmark_selectable: true },
    ],
  });
  const context = vm.createContext({
    window,
    document: documentFixture(element),
    fetch: async (url) => ({
      ok: true,
      json: async () => (url.startsWith("/status") ? { ft8_decode_enabled: false, cw_decode_enabled: true } : []),
    }),
    CSS: { escape: (value) => value },
    Element: ElementFixture,
    console,
  });
  new vm.Script(source).runInContext(context);
  await new Promise((resolve) => setImmediate(resolve));
  calls.postPath.length = 0;

  window.trx.modules.bookmarks.apply({
    id: "ft8-20m", name: "FT8 20m", freq_hz: 14_074_000, mode: "USB", bandwidth_hz: 3000, decoders: ["ft8"],
  });
  await new Promise((resolve) => setImmediate(resolve));
  await new Promise((resolve) => setImmediate(resolve));

  // Optimistic UI: the guard is armed before the display update so a stale SSE
  // frame cannot snap the marker back while the tune is in flight.
  assert.deepEqual(calls.armOptimisticFrequency, [14_074_000]);
  assert.deepEqual(calls.applyLocalTunedFrequency, [[14_074_000, true]]);
  assert.deepEqual(calls.syncBandwidthInput, [3000]);
  assert.equal(window.trx.state.currentBandwidthHz, 3000);
  assert.equal(element("mode").value, "USB");

  // Rig commands: mode and bandwidth over postPath, frequency via the core
  // tuning service so virtual-channel redirection still applies.
  assert.deepEqual(calls.setRigFrequency, [14_074_000]);
  assert.ok(calls.postPath.includes("/set_mode?mode=USB"));
  assert.ok(calls.postPath.includes("/set_bandwidth?hz=3000"));
  // ft8 is selected and off; cw is on but incompatible with USB.
  assert.ok(calls.postPath.includes("/toggle_ft8_decode"));
  assert.ok(calls.postPath.includes("/toggle_cw_decode"));
});

test("bookmark controls follow the host authentication state", async () => {
  const elements = new Map();
  const element = (id) => {
    if (!elements.has(id)) elements.set(id, new ElementFixture());
    return elements.get(id);
  };
  const { window } = hostFixture({ authEnabled: true, authRole: "rx" });
  const context = vm.createContext({
    window,
    document: documentFixture(element),
    fetch: async () => ({ ok: true, json: async () => [] }),
    CSS: { escape: (value) => value },
    Element: ElementFixture,
    console,
  });
  new vm.Script(source).runInContext(context);
  await new Promise((resolve) => setImmediate(resolve));

  assert.equal(element("bm-add-btn").style.display, "none");

  window.trx.state.authRole = "control";
  await window.trx.modules.bookmarks.fetch("");
  assert.equal(element("bm-add-btn").style.display, "");
});
