// The modules a given entry point's bundle may not contain, keyed by the
// repo-relative entry point.
//
// ONE table, read by both layers that act on it: build.js asserts it against
// esbuild's own metafile (the guarantee), and
// script/lib/eslint/noStateSingletonInBackground.js reports the same
// prohibition in the editor (fast feedback). It lives here because a second
// literal copy of the path is exactly how a rename disarms one layer while the
// other still looks enforced.
//
// src/shared/state.js holds a module-level `state` object, loaded once by
// loadState() and mutated in place from then on. That is the popup's model:
// one page, one load at boot, one lifetime. The MV3 service worker has no
// "once" — it is killed when idle and revived by the next message, nothing
// loads state at module scope, and an unpopulated read was answered out of
// DEFAULT_STATE in silence. Five defects came from that, one of which
// destroyed a wallet (https://git.eeqj.de/sneak/AutistMask/issues/324). The
// background has its own per-call storage layer in src/background/state.js
// instead.
//
// What build.js's assertion covers, measured rather than assumed:
//
//   - Any import of a listed module, at any hop, in any specifier syntax,
//     however esbuild resolved it. The check reads the input list esbuild
//     reported for the emitted bundle, so it is the resolution the shipped
//     file was built from and not a model of it. Measured on a computed
//     specifier that esbuild constant-folds (`require("../shared/" +
//     "state")`), on a computed specifier it resolves as a glob
//     (`import("../shared/" + variable)`), and on a symlink to the module
//     (esbuild reports the real path): each is `make build` exit 2.
//
//   - Every background entry point, whether or not anyone remembered to list
//     it. A bundled entry point under BACKGROUND_ENTRY_PREFIX with no line in
//     this table fails the build (assertNoForbiddenInputs()), so adding a
//     second worker entry point is protected by default rather than protected
//     only if the person adding it knew about this file. Measured: bundling
//     src/background/worker2.js with no line here is `make build` exit 2.
//
//   - NOT covered: a COPY of a listed module at another path. The table is
//     keyed by path, so `cp src/shared/state.js src/shared/stateCopy.js` plus
//     a background require of the copy is `make build` exit 0 and `make lint`
//     exit 0 (measured). The copy carries the singleton's own guard, so
//     defects 1-3 of https://git.eeqj.de/sneak/AutistMask/issues/324 — a read
//     of a field nothing loaded — become a loud StateNotLoadedError instead of
//     a silent DEFAULT_STATE. Defects 4 and 5 do NOT: a copy also carries
//     loadState(), and a stale read several awaits after a load, or a load
//     detaching the objects an in-flight handler is mutating, are silent over
//     a LOADED singleton whether it is the original or a copy. So the residual
//     is wider than "it fails loudly". A newly WRITTEN singleton has no
//     backstop at all.
//
//   - NOT covered: a background-behaving entry point outside
//     BACKGROUND_ENTRY_PREFIX. The default protection above is keyed on that
//     directory, which is also what eslint.config.js scopes the rule to, so a
//     worker entry point placed somewhere else is covered by neither layer and
//     needs its own line here.
//
// The ESLint rule's bounds are its own and are narrower: it matches specifiers
// textually, so a computed specifier and a symlink to a listed module are
// reported by the build and not by the rule. Both are pinned as non-reports in
// tests/backgroundStateLintRule.test.js and are `make build` exit 2 (measured).
// A second background entry point reached by one of those two shapes is
// therefore caught by the build and not by the rule — which is the same
// division of labour as everywhere else here, not an extra hole.
//
// Every way the table itself can rot is a failure rather than a quiet pass:
//
//   - a KEY no bundled entry point matched, and a listed MODULE this build
//     bundled nowhere: assertForbiddenTableCovered(), at the end of a build;
//   - an entry that lists NO modules, and a table with no entries at all:
//     assertTableWellFormed() below, at require time — so it fails the build
//     and the lint run alike, because the rule reads the same values and an
//     empty list leaves it with nothing to look for.
//
// All of it is pinned by tests/buildForbiddenInputs.test.js.

// What counts as a background entry point, and therefore must be listed above.
// The build has no other notion of one: entry points are the paths handed to
// bundle(), and this prefix is the narrowest rule that names the worker's
// directory. eslint.config.js scopes the lint rule with the same prefix, from
// this constant, so the two layers cannot disagree about what "background"
// means.
const BACKGROUND_ENTRY_PREFIX = "src/background/";

const FORBIDDEN_INPUTS = {
    "src/background/index.js": ["src/shared/state.js"],
};

// Refuse a table that cannot prohibit anything. An entry whose module list is
// empty passes every check in both layers while enforcing nothing: the build
// finds no module to look for and records the entry as checked, and the rule's
// forbidden set — Object.values(...).flat() — comes back empty, so a plain
// `require("../shared/state")` in the worker is green everywhere. That is a
// one-character edit, so it fails here, where the table is defined and both
// layers must load it, rather than in either layer's own checks.
function assertTableWellFormed(table) {
    const entries = Object.entries(table);
    if (entries.length === 0) {
        throw new Error(
            "FORBIDDEN_INPUTS is empty, so nothing is prohibited anywhere. " +
                "Removing the last entry disables the guarantee behind " +
                "https://git.eeqj.de/sneak/AutistMask/issues/324.",
        );
    }
    for (const [entry, modules] of entries) {
        if (!Array.isArray(modules) || modules.length === 0) {
            throw new Error(
                `FORBIDDEN_INPUTS["${entry}"] lists no modules, so it ` +
                    `prohibits nothing while still looking enforced. Give it ` +
                    `the modules that entry point may not reach, or remove ` +
                    `the entry.`,
            );
        }
    }
}

assertTableWellFormed(FORBIDDEN_INPUTS);

module.exports = {
    BACKGROUND_ENTRY_PREFIX,
    FORBIDDEN_INPUTS,
    assertTableWellFormed,
};
