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";

// Route-drift guard for the CLIENT-SIDE render dispatch (`routes`) vs the places
// that navigate into it: the V3 sidebar (V3_SECTIONS) and the ⌘K command palette.
//
// This catches "rename-drift": a route gets renamed in the `routes` dispatch but
// a nav child / palette entry / op-card keeps the OLD name, so clicking it
// dead-ends on the "Unknown route" empty-state. Two live instances of exactly
// this (`new-workflow`, `new-campaign-wizard`) shipped undetected until 2026-07;
// this test would have failed on both. It is the client-render analogue of the
// dashboard-routes.drift.test.js check (which only covers the API proxy map).
//
// NOTE: only STATIC double-quoted routes are checked. Parametric routes assembled
// with template literals (navigate(`campaigns/${id}`)) are resolved by parseHash
// at runtime and are intentionally out of scope here.

const here = dirname(fileURLToPath(import.meta.url));
const src = readFileSync(join(here, "dashboard.js"), "utf8");
const lines = src.split(/\r?\n/);

// Slice a top-level `const NAME = {` / `[` ... balanced-close block by brace depth.
// Safe for these blocks: none of their string values contain stray braces/brackets.
function sliceBlock(startRe) {
  const s = lines.findIndex((l) => startRe.test(l));
  assert.ok(s >= 0, `block start ${startRe} found in dashboard.js`);
  let depth = 0;
  for (let i = s; i < lines.length; i++) {
    for (const ch of lines[i]) {
      if (ch === "{" || ch === "[") depth++;
      else if (ch === "}" || ch === "]") depth--;
    }
    if (i > s && depth === 0) return lines.slice(s, i + 1).join("\n");
  }
  throw new Error(`unterminated block for ${startRe}`);
}

function dispatchKeys() {
  const block = sliceBlock(/^const routes = \{/);
  const keys = new Set();
  for (const m of block.matchAll(/^\s*"?([a-zA-Z0-9_/-]+)"?\s*:/gm)) keys.add(m[1]);
  return keys;
}

test("every V3 sidebar route resolves to a render dispatch handler", () => {
  const keys = dispatchKeys();
  assert.ok(keys.size > 90, `expected the full routes dispatch, got ${keys.size}`);
  const v3 = sliceBlock(/^const V3_SECTIONS = \[/);
  const navRoutes = [...new Set([...v3.matchAll(/route:\s*"([^"]+)"/g)].map((m) => m[1]))];
  assert.ok(navRoutes.length > 20, `expected the full nav route set, got ${navRoutes.length}`);
  const dead = navRoutes.filter((r) => !keys.has(r));
  assert.deepEqual(
    dead,
    [],
    `V3_SECTIONS routes with no handler in the routes dispatch (would show "Unknown route"): ${dead.join(", ")}`,
  );
});

test("every command-palette route resolves to a render dispatch handler", () => {
  const keys = dispatchKeys();
  // The ⌘K command palette was extracted into its own module
  // (dashboard-k-command-palette.js) in the origin/main split. Its entries are
  // `{ route: "x", label: "…", keys: "…" }` (route IMMEDIATELY followed by label).
  // Every palette route must resolve to a handler in dashboard.js's dispatch, or
  // the action dead-ends on "Unknown route".
  const paletteSrc = readFileSync(join(here, "dashboard-k-command-palette.js"), "utf8");
  const palette = [
    ...new Set([...paletteSrc.matchAll(/route:\s*"([^"]+)",\s*label:/g)].map((m) => m[1])),
  ];
  assert.ok(palette.length > 30, `expected the command palette route set, got ${palette.length}`);
  const dead = palette.filter((r) => !keys.has(r));
  assert.deepEqual(
    dead,
    [],
    `command-palette routes with no handler in the routes dispatch (dead-end on "Unknown route"): ${dead.join(", ")}`,
  );
});

// ── The reverse direction ────────────────────────────────────────────────
//
// The three tests above all ask "does this nav entry reach a handler?". None
// asks the opposite: "can a user reach this handler?". A route that exists in
// the dispatch but appears in no sidebar, no palette and no other view is
// reachable only by typing its hash — so it rots unnoticed, and nothing here
// would ever say so.
//
// Most off-sidebar routes are legitimate: detail views opened by clicking a row
// (dashboard.js says so explicitly). Those are baselined below. What the gate is
// for is stopping the list from growing — a NEW orphan means someone built a
// view and never wired a way in.
//
// Baselined today: 40 routes, of which five have zero inbound navigation of any
// kind — x/hub, telegram/hub, linkedin/hub, email/hub (all four rendering the
// 738-line dashboard-platform-hub-phase-4.js) and run-previews. They are listed
// so the decision to keep or delete them is deliberate rather than forgotten.
const KNOWN_OFF_NAV = new Set(
  JSON.parse(readFileSync(join(here, "..", "tools", ".nav-orphans-baseline.json"), "utf8")).orphans,
);

test("no NEW route is unreachable from the sidebar, palette or a section map", () => {
  const keys = dispatchKeys();

  const reachable = new Set();
  const v3 = sliceBlock(/^const V3_SECTIONS = \[/);
  for (const m of v3.matchAll(/route:\s*"([^"]+)"/g)) reachable.add(m[1]);
  const footer = lines.some((l) => /^const V3_FOOTER = \[/.test(l))
    ? sliceBlock(/^const V3_FOOTER = \[/)
    : "";
  for (const m of footer.matchAll(/route:\s*"([^"]+)"/g)) reachable.add(m[1]);
  const paletteSrc = readFileSync(join(here, "dashboard-k-command-palette.js"), "utf8");
  for (const m of paletteSrc.matchAll(/route:\s*"([^"]+)",\s*label:/g)) reachable.add(m[1]);
  // The section map names every route the sidebar highlights for, which covers
  // detail routes owned by a room even when they have no nav entry of their own.
  const map = sliceBlock(/^\s*const map = \{/);
  for (const m of map.matchAll(/"?([a-zA-Z0-9_/-]+)"?\s*:\s*"[a-z0-9-]+"/g)) reachable.add(m[1]);

  const orphans = [...keys].filter((k) => !reachable.has(k)).sort();
  const fresh = orphans.filter((o) => !KNOWN_OFF_NAV.has(o));

  assert.deepEqual(
    fresh,
    [],
    `${fresh.length} NEW route(s) in the dispatch that nothing navigates to — ` +
      `wire them into V3_SECTIONS/the palette, or delete the view:\n  ${fresh.join("\n  ")}`,
  );
});

test("every V3_ROUTE_TO_SECTION target is a real V3 section key", () => {
  const v3 = sliceBlock(/^const V3_SECTIONS = \[/);
  const sectionKeys = new Set([...v3.matchAll(/\{\s*key:\s*"([^"]+)"/g)].map((m) => m[1]));
  assert.ok(sectionKeys.size >= 5, `expected the room keys, got ${sectionKeys.size}`);
  // The map lives inside the V3_ROUTE_TO_SECTION IIFE as `const map = { … }`.
  const map = sliceBlock(/^\s*const map = \{/);
  const targets = [...new Set([...map.matchAll(/:\s*"([a-z0-9-]+)"/g)].map((m) => m[1]))];
  const bad = targets.filter((t) => !sectionKeys.has(t));
  assert.deepEqual(
    bad,
    [],
    `V3_ROUTE_TO_SECTION points at section keys that don't exist in V3_SECTIONS: ${bad.join(", ")}`,
  );
});
