// Headless render guard for the `aura graph` viewer (graph-viewer.js).
//
// Property protected: a bind-bound param slot renders in the node signature as a
// DIMMED `name=value`, in TRUE slot order (by original position), with the free
// slots in their type colour — never dropped. Covers a trailing bind (the
// cycle-0036 showcase shape) AND a non-trailing (middle) bind, which an
// append-after-free design would render in the wrong order.
//
// It loads the real viewer module and drives the exported pure `genDot`. `node`
// is REQUIRED: a skipped guard is not a guard.

import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

const require = createRequire(import.meta.url);
const here = dirname(fileURLToPath(import.meta.url));

// Load the viewer once with an empty model; normalizeModel/genDot are pure and
// take the model/def explicitly, so each case is rendered by passing its own.
global.window = { AURA_MODEL: { root: { nodes: {}, edges: [] }, composites: {} } };
const { normalizeModel, genDot } = require(join(here, "..", "assets", "graph-viewer.js"));
const dotFor = (model) => genDot(normalizeModel(model).root, "root", new Set(), false).dot;

const fail = (msg, dot) => {
  console.error(msg + "\n--- dot ---\n" + dot);
  process.exit(1);
};

// --- Model A: trailing bind (the cycle-0036 showcase shape) ---
const trailing = {
  root: { nodes: { "0": { prim: {
    name: "blend", type: "LinComb", role: "node",
    params: [["weights[0]", "f64"], ["weights[1]", "f64"]],
    bound: [[2, "weights[2]", "f64", "0.5"]],
    ins: [["f64", "any", "term[0]"], ["f64", "any", "term[1]"], ["f64", "any", "term[2]"]],
    outs: [["value", "f64"]],
  } } }, edges: [] },
  composites: {},
};
const dotA = dotFor(trailing);

const BOUND = '<font color="#9399b2">weights[2]=0.5</font>';
const FREE0 = '<font color="#89b4fa">weights[0]</font>';
const FREE1 = '<font color="#89b4fa">weights[1]</font>';
if (!dotA.includes(BOUND)) fail("trailing: bound slot weights[2]=0.5 not rendered dimmed", dotA);
if (!dotA.includes(FREE0) || !dotA.includes(FREE1)) fail("trailing: a free weight slot is missing or not type-coloured", dotA);
const a0 = dotA.indexOf(FREE0), a1 = dotA.indexOf(FREE1), ab = dotA.indexOf(BOUND);
if (!(a0 < a1 && a1 < ab)) fail("trailing: slot order is not weights[0] < weights[1] < weights[2]=0.5", dotA);

// --- Model B: NON-TRAILING (middle) bind — pins slot-order faithfulness ---
const middle = {
  root: { nodes: { "0": { prim: {
    name: "mid", type: "Probe", role: "node",
    params: [["a", "f64"], ["c", "f64"]],
    bound: [[1, "b", "f64", "9.0"]],
    ins: [["f64", "any", "t0"], ["f64", "any", "t1"], ["f64", "any", "t2"]],
    outs: [["value", "f64"]],
  } } }, edges: [] },
  composites: {},
};
const dotB = dotFor(middle);

const MA = '<font color="#89b4fa">a</font>';
const MB = '<font color="#9399b2">b=9.0</font>';
const MC = '<font color="#89b4fa">c</font>';
if (!dotB.includes(MA) || !dotB.includes(MB) || !dotB.includes(MC)) fail("middle: a free or bound slot is missing", dotB);
const ba = dotB.indexOf(MA), bb = dotB.indexOf(MB), bc = dotB.indexOf(MC);
if (!(ba < bb && bb < bc)) fail("middle: bound slot b=9.0 not placed between free a and c (append-only bug)", dotB);

console.log("OK — bound slot renders dimmed name=value in true slot order (trailing + middle bind).");
process.exit(0);
