// WI-07 verification: diff the dashboard's old inline per-module maps
// (READY_PATTERNS / moduleAppearance / moduleBrowseableRoots /
// guessPortsForModule — kept in rcc.js as fallbacks) against the values
// the manifests now serve via GET /api/modules (as loaded by
// tools/module-meta-dump, which uses the controller's own LoadDir).
//
// ready_pattern + appearance were MOVED (must be value-identical → any
// mismatch exits 1). ports + browseable_roots were already declared in
// module.yaml; drift there is reported informationally (manifest wins).
//
// Run from repo root:  node controller/.test/wi07-diff-maps.mjs
import fs from "node:fs";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const rcc = fs.readFileSync(path.join(root, "controller/cmd/controller/static/rcc.js"), "utf8");

// Extract a brace-balanced object literal following `marker`.
function extractObject(marker, from = 0) {
  const i = rcc.indexOf(marker, from);
  if (i < 0) throw new Error("marker not found: " + marker);
  const start = rcc.indexOf("{", i);
  let depth = 0, j = start;
  for (; j < rcc.length; j++) {
    const c = rcc[j];
    if (c === "{") depth++;
    else if (c === "}") { depth--; if (depth === 0) break; }
  }
  return eval("(" + rcc.slice(start, j + 1) + ")");
}

const READY = extractObject("const READY_PATTERNS = ");
const APPEAR = extractObject("const moduleAppearance = ");
const ROOTS = extractObject("const moduleBrowseableRoots = ");
// ports map lives inside guessPortsForModule()
const fnIdx = rcc.indexOf("function guessPortsForModule(id)");
if (fnIdx < 0) throw new Error("guessPortsForModule not found");
const PORTS = extractObject("const m = {", fnIdx);

// Manifest side.
const goExe = process.env.GO_EXE || "C:\\Program Files\\Go\\bin\\go.exe";
const manifest = JSON.parse(execFileSync(goExe, ["run", "./tools/module-meta-dump"], { cwd: root, encoding: "utf8" }));

// Same compile rule the dashboard uses for manifest ready_pattern strings.
function compileReady(s) {
  if (!s) return null;
  const m = /^\/([\s\S]+)\/([a-z]*)$/.exec(s);
  return m ? new RegExp(m[1], m[2]) : new RegExp(s);
}

const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
let hardFail = 0;
const ids = [...new Set([...Object.keys(manifest), ...Object.keys(READY), ...Object.keys(APPEAR), ...Object.keys(ROOTS), ...Object.keys(PORTS)])].sort();

for (const id of ids) {
  const man = manifest[id];
  const lines = [];

  // ready_pattern — moved: must be identical where both sides have it.
  const oldRe = READY[id];
  const newRe = man && compileReady(man.ready_pattern);
  if (oldRe && !man) lines.push("  ready: JS-only (no manifest dir) — fallback covers it");
  else if (oldRe && !newRe) { lines.push("  ready: MISSING in manifest"); hardFail++; }
  else if (oldRe && newRe && (oldRe.source !== newRe.source || oldRe.flags !== newRe.flags)) {
    lines.push(`  ready: MISMATCH\n    old ${oldRe}\n    new ${newRe}`); hardFail++;
  }

  // appearance — moved: compare emoji/grad/art/steam.
  const oa = APPEAR[id], na = man && man.appearance;
  if (oa && !man) lines.push("  appearance: JS-only (no manifest dir) — fallback covers it");
  else if (oa && !na) { lines.push("  appearance: MISSING in manifest"); hardFail++; }
  else if (oa && na) {
    for (const k of ["emoji", "grad", "art", "steam"]) {
      if ((oa[k] || "") !== (na[k] || "")) { lines.push(`  appearance.${k}: MISMATCH old=${JSON.stringify(oa[k] || "")} new=${JSON.stringify(na[k] || "")}`); hardFail++; }
    }
  }

  // browseable_roots — pre-existing in manifests; report drift.
  const orr = ROOTS[id], nrr = man && man.browseable_roots;
  if (orr && man && !eq(orr, nrr)) lines.push(`  roots drift (manifest wins):\n    old ${JSON.stringify(orr)}\n    new ${JSON.stringify(nrr)}`);
  if (orr && !man) lines.push("  roots: JS-only (no manifest dir) — fallback covers it");

  // ports — pre-existing in manifests; compare ignoring the additive env field.
  const op = PORTS[id];
  const np = man && man.ports && man.ports.map(p => {
    const o = { name: p.name, proto: p.proto, default: p.default };
    if (p.required) o.required = true;
    if (p.internal) o.internal = true;
    return o;
  });
  const opNorm = op && op.map(p => {
    const o = { name: p.name, proto: p.proto, default: p.default };
    if (p.required) o.required = true;
    if (p.internal) o.internal = true;
    return o;
  });
  if (op && man && !eq(opNorm, np)) lines.push(`  ports drift (manifest wins):\n    old ${JSON.stringify(opNorm)}\n    new ${JSON.stringify(np)}`);
  if (op && !man) lines.push("  ports: JS-only (no manifest dir) — fallback covers it");

  if (lines.length) console.log(id + "\n" + lines.join("\n"));
  else console.log(id + "\n  OK (identical)");
}
console.log(hardFail ? `\nFAIL: ${hardFail} moved-field mismatch(es)` : "\nPASS: all moved fields (ready_pattern, appearance) value-identical");
process.exit(hardFail ? 1 : 0);
