import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

const here = dirname(fileURLToPath(import.meta.url));
const src = readFileSync(join(here, "dashboard.js"), "utf8");

// Startup bindings must resolve against BOTH shells that load dashboard.js.
//
// `$` is `root.querySelector(sel)` — it returns null for a missing id, and
// `null.addEventListener` is a TypeError. init() is async and is called bare
// (`init();`, no .catch), so that TypeError becomes an unhandled rejection.
// The only thing that notices is installGlobalErrorNet(), which shows
// "Something went wrong: Cannot read properties of null (reading
// 'addEventListener')" — and every line of init() after the throw never runs:
// no wireTopbarActions, no hashchange listener, no buildSidebar, no
// handleRoute. The app is left as an empty shell.
//
// This is exactly what happened. `#settings-btn` was retired from the markup in
// b6b108f ("retired footer", which deleted the wiring too), but the big
// dashboard-split refactor 49b706d1 was authored off a base predating that
// commit and restored dashboard.js verbatim; the 22-conflict merge in 0fb007a
// kept the HTML deletion and the resurrected JS. `#refresh-btn` is the same
// class of fault one line earlier: it exists in dashboard.html but never in
// app.html, so the hosted /web app died before the extension page did.
//
// Scoped to init() on purpose. Ids looked up inside a view renderer (e.g.
// #kw-input in dashboard-signals-view.js) are created by that renderer and are
// correctly absent from the static shell; requiring those here would be noise.
// init() runs before any view renders, so everything it binds must be markup.

/** The body of `async function init()`, up to the closing brace at column 0. */
function initBody() {
  const start = src.indexOf("async function init()");
  assert.ok(start >= 0, "init() found in dashboard.js");
  const end = src.indexOf("\n}\n", start);
  assert.ok(end > start, "end of init() located");
  return src.slice(start, end);
}

/** Element ids defined in a shell page. */
function idsOf(file) {
  const html = readFileSync(join(here, file), "utf8");
  return new Set([...html.matchAll(/\bid="([^"]+)"/g)].map((m) => m[1]));
}

/** Ids that init() dereferences directly, with no null guard. */
function unguardedIds(body) {
  return [...body.matchAll(/\$\("#([a-zA-Z0-9_-]+)"\)\s*\./g)].map((m) => m[1]);
}

const SHELLS = ["dashboard.html", "app.html"];

test("init() only binds ids that exist in the markup", () => {
  const bound = unguardedIds(initBody());
  assert.ok(bound.length > 0, "init() binds at least one element (regex still matches)");

  const gaps = [];
  for (const shell of SHELLS) {
    const ids = idsOf(shell);
    for (const id of bound) if (!ids.has(id)) gaps.push(`${shell} has no #${id}`);
  }

  assert.deepEqual(
    gaps,
    [],
    "init() dereferences an element that does not exist, which throws and aborts " +
      `the whole startup path:\n  ${gaps.join("\n  ")}`,
  );
});

test("the two shells agree on every id init() depends on", () => {
  // A binding that resolves in the extension page but not the hosted one is the
  // /web-only failure mode; it hid behind manual testing of the extension.
  const bound = new Set(unguardedIds(initBody()));
  const [dash, app] = SHELLS.map(idsOf);
  const onlyDash = [...bound].filter((id) => dash.has(id) && !app.has(id));
  assert.deepEqual(onlyDash, [], `bound ids present in dashboard.html but missing from app.html`);
});
