#!/usr/bin/env node
// The set of npm packages whose bytes reach admin/dist must be exactly the set
// recorded in licenses/dependency-identity.txt (milestone-14 ruling 3).
//
// The shipped build carries no sourcemaps, so this makes a second build with
// them into its own directory: the `sources` list of each chunk names the
// modules that went into it, and the artifact `npm run build` produced stays
// untouched. Runs from admin/ as `npm run assert-bundled`, on a laptop exactly as
// on the runner.

import { execFileSync } from "node:child_process";
import { readdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import { bundledPackages, comparePackages, formatDiff, recordedPackages } from "./bundledPackages.mjs";

const webRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const outDir = "dist-sourcemap";
const identityFile = join(webRoot, "..", "licenses", "dependency-identity.txt");

function fail(message) {
	process.stderr.write(`${message}\n`);
	process.exit(1);
}

function mapFiles(relativeDir) {
	const absolute = join(webRoot, relativeDir);
	let entries;
	try {
		entries = readdirSync(absolute, { withFileTypes: true });
	} catch (err) {
		fail(`assert-bundled: cannot read ${relativeDir}: ${err.message}`);
	}
	const found = [];
	for (const entry of entries) {
		const child = `${relativeDir}/${entry.name}`;
		if (entry.isDirectory()) {
			found.push(...mapFiles(child));
		} else if (entry.isFile() && entry.name.endsWith(".map")) {
			found.push(child);
		}
	}
	return found.sort();
}

// The binary npm ci installed, never `npx`: npx silently downloads a package it
// cannot find locally, so a wrong working directory would turn a licence check
// into an unpinned fetch from the network.
try {
	execFileSync(
		join(webRoot, "node_modules", ".bin", "vite"),
		["build", "--sourcemap", "--outDir", outDir, "--emptyOutDir"],
		{
			cwd: webRoot,
			stdio: ["ignore", "ignore", "inherit"],
		},
	);
} catch (err) {
	fail(`assert-bundled: the sourcemap build failed: ${err.message}`);
}

const maps = mapFiles(outDir);
if (maps.length === 0) fail("assert-bundled: the sourcemap build produced no .map files; this check cannot run blind");

const sourceLists = maps.map((path) => {
	const raw = readFileSync(join(webRoot, path), "utf8");
	let parsed;
	try {
		parsed = JSON.parse(raw);
	} catch (err) {
		fail(`assert-bundled: ${path} is not JSON: ${err.message}`);
	}
	return Array.isArray(parsed.sources) ? parsed.sources : [];
});

const bundled = bundledPackages(sourceLists);

let identity;
try {
	identity = readFileSync(identityFile, "utf8");
} catch (err) {
	fail(`assert-bundled: cannot read licenses/dependency-identity.txt: ${err.message}`);
}

const recorded = recordedPackages(identity);
if (recorded === null) {
	fail("assert-bundled: licenses/dependency-identity.txt has no '[npm packages bundled into admin/dist]' section");
}
if (recorded.length === 0) {
	fail("assert-bundled: the '[npm packages bundled into admin/dist]' section is empty");
}

const { added, removed } = comparePackages(recorded, bundled);
if (added.length !== 0 || removed.length !== 0) {
	process.stderr.write(`${formatDiff(recorded, bundled)}\n\n`);
	fail(
		[
			"the set of npm packages in admin/dist has changed (-recorded +current).",
			"Work out what the change means for licenses/inventory.zon first, then record",
			"the new list in that section of licenses/dependency-identity.txt.",
		].join("\n"),
	);
}

process.stdout.write(`admin/dist bundles exactly the ${bundled.length} recorded packages:\n`);
for (const name of bundled) process.stdout.write(`${name}\n`);
