/**
 * The public package surface, and the one promise it makes.
 *
 * `src/index.ts` is what `package.json`'s `"."` export points at, so it is the
 * thing a verifier, a training harness or a Node service `import`s. The promise
 * is that everything reachable from it runs **with no renderer, no DOM and no
 * network** — because the consumers who want the arena and the simulators are
 * precisely the consumers who have none of the three.
 *
 * That is not a property you can assert by importing the file and seeing it
 * work: `import * as THREE from "three"` succeeds perfectly well under Node and
 * costs a consumer half a megabyte for nothing. So this walks the static import
 * graph and reads it. A single `from "three"` anywhere in the closure fails,
 * and names the file and the chain that reached it, which is the only form of
 * this failure anybody can act on.
 */

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

import * as barrel from "../../index.ts";

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const ENTRY = path.join(ROOT, "src/index.ts");

/**
 * Every relative specifier in a file, `import` and `export` alike.
 *
 * `export * from "./x.ts"` is the form the barrel itself is written in, and a
 * matcher that only looked at `import` would walk none of it.
 */
const SPECIFIER = /(?:^|\n)\s*(?:import|export)\b[^;\n]*?from\s+["']([^"']+)["']/g;
/** `await import("./x.ts")`, which is how a renderer would sneak in lazily. */
const DYNAMIC = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;

/** Walk the graph from `entry`, returning every file reached and how. */
function closure(entry: string): Map<string, string[]> {
  const reached = new Map<string, string[]>([[entry, []]]);
  const queue = [entry];
  while (queue.length > 0) {
    const file = queue.shift() as string;
    const source = readFileSync(file, "utf8");
    const chain = reached.get(file) ?? [];
    for (const pattern of [SPECIFIER, DYNAMIC]) {
      pattern.lastIndex = 0;
      let match: RegExpExecArray | null;
      while ((match = pattern.exec(source)) !== null) {
        const specifier = match[1];
        if (specifier === undefined || !specifier.startsWith(".")) continue;
        const resolved = path.resolve(path.dirname(file), specifier);
        if (reached.has(resolved)) continue;
        reached.set(resolved, [...chain, path.relative(ROOT, file)]);
        queue.push(resolved);
      }
    }
  }
  return reached;
}

/** Bare specifiers a module in the closure is allowed to depend on. */
const ALLOWED_PACKAGES = new Set<string>([]);

test("the barrel exports the renderer-independent surfaces this build added", () => {
  for (const name of [
    "ARENA_ENVIRONMENTS",
    "flattenObservation",
    "structureAction",
    "observationWidth",
    "rollout",
    "createSimulatedDevices",
    "createSimulatedVehicleTelemetry",
    "normalizeDeviceCommand",
    "normalizeVehicleTelemetryCommand",
    "exteriorVehicleAppearance",
    "DEVICE_RANGES",
    "Plan",
  ]) {
    assert.ok(
      name in barrel,
      `src/index.ts no longer exports ${name}; a consumer's import just broke`,
    );
  }
});

test("nothing reachable from the barrel imports three.js", () => {
  const offenders: string[] = [];
  for (const [file, chain] of closure(ENTRY)) {
    const source = readFileSync(file, "utf8");
    if (/from\s+["']three(?:\/|["'])/.test(source) || /import\s*\(\s*["']three/.test(source)) {
      offenders.push(`${path.relative(ROOT, file)} (via ${chain.join(" → ") || "the barrel itself"})`);
    }
  }
  assert.deepEqual(
    offenders,
    [],
    "three.js is on the public package surface. The render layer for a simulation " +
      "(interiors/devices.ts, engine/officeExterior.ts, interiors/officeScene.ts) is " +
      "never exported; the state machine behind it is.",
  );
});

/**
 * The render layers this build added, named so that adding one cannot quietly
 * put three.js on the package surface.
 *
 * Four new engine modules — the port kit, the vessel layer, the reflectivity
 * sheet and the migration field — are *render* layers in the same sense
 * `interiors/devices.ts` and `engine/officeExterior.ts` are, and are therefore
 * conspicuously absent from `src/index.ts`. Their **gates** are not: a gate is a
 * pure function over plain data, it is testable with no GL context, and it is
 * where every honest decision about a feed is taken. The asymmetry is the whole
 * shape of this repo, and it is written down here rather than left to be
 * rediscovered by whichever workstream lands last.
 *
 * Listed as names rather than inferred, so this test fails loudly with the
 * offending path printed rather than by the closure quietly growing.
 */
const RENDER_LAYERS = [
  "src/engine/ports.ts",
  "src/engine/vessels.ts",
  "src/engine/precip.ts",
  "src/engine/migration.ts",
];

/** The pure gates behind them. Every one of these may be exported; none may import three. */
const PURE_GATES = [
  "src/server/vessels.ts",
  "src/server/radar.ts",
  "src/server/birds.ts",
];

test("the render layers stay off the package surface", () => {
  const reached = new Set([...closure(ENTRY).keys()].map((f) => path.relative(ROOT, f)));
  const offenders = RENDER_LAYERS.filter((file) => reached.has(file));
  assert.deepEqual(
    offenders,
    [],
    "a render layer reached the barrel. The port kit, the vessels, the radar " +
      "sheet and the migration field all build meshes; their gates are the half " +
      "that belongs on the public surface.",
  );
});

test("the pure gates behind those layers import no renderer", () => {
  const offenders: string[] = [];
  for (const file of PURE_GATES) {
    const full = path.join(ROOT, file);
    if (!existsSync(full)) continue;
    const source = readFileSync(full, "utf8");
    if (/from\s+["']three(?:\/|["'])/.test(source) || /import\s*\(\s*["']three/.test(source)) {
      offenders.push(file);
    }
  }
  assert.deepEqual(
    offenders,
    [],
    "a gate imported three.js. Everything a gate asserts must be assertable " +
      "without a GL context — the argument fires.ts:16-23 makes, applied to the " +
      "three feeds this build added.",
  );
});

test("nothing reachable from the barrel takes a bare dependency at all", () => {
  const offenders: string[] = [];
  for (const [file] of closure(ENTRY)) {
    const source = readFileSync(file, "utf8");
    for (const pattern of [SPECIFIER, DYNAMIC]) {
      pattern.lastIndex = 0;
      let match: RegExpExecArray | null;
      while ((match = pattern.exec(source)) !== null) {
        const specifier = match[1];
        if (specifier === undefined) continue;
        if (specifier.startsWith(".") || specifier.startsWith("node:")) continue;
        if (ALLOWED_PACKAGES.has(specifier)) continue;
        offenders.push(`${path.relative(ROOT, file)} → ${specifier}`);
      }
    }
  }
  assert.deepEqual(offenders, [], "an unvetted runtime dependency reached the package surface");
});

test("nothing reachable from the barrel reaches for a browser global", () => {
  // Read as source rather than executed, because a `document` reference inside a
  // branch nobody takes is still a module that cannot be loaded in a worker
  // whose global object does not have one.
  const globals = /\b(?:document|localStorage|sessionStorage|navigator|requestAnimationFrame)\b/;
  const offenders: string[] = [];
  for (const [file] of closure(ENTRY)) {
    const source = readFileSync(file, "utf8")
      // Comments talk about the DOM constantly and correctly; only code counts.
      .replace(/\/\*[\s\S]*?\*\//g, "")
      .replace(/(^|\n)\s*\/\/[^\n]*/g, "$1");
    if (globals.test(source)) offenders.push(path.relative(ROOT, file));
  }
  assert.deepEqual(offenders, [], "a DOM global is reachable from the package surface");
});
