// The shipped Content Security Policy, pinned in both directions.
//
// This is the anti-regression check for #182. libsodium decides its
// backend by trying to compile WebAssembly and catching the failure, so a
// CSP that refuses WASM demotes the vault to the wasm2js translation —
// roughly 20x slower per Argon2id derivation — and says so only in a
// console message nobody reads. Dropping 'wasm-unsafe-eval' from either
// manifest therefore has to fail a check, not a log line.
//
// It is equally a check against loosening. 'wasm-unsafe-eval' is granted
// deliberately and narrowly (see the backend note in src/shared/vault.js);
// 'unsafe-eval', 'unsafe-inline' and any remote script source are not, and
// an exact match on the token set is what keeps the next edit from
// smuggling one in alongside.
//
// build.js copies these files to dist/<target>/manifest.json verbatim, so
// what is asserted here is what ships.

const fs = require("fs");
const path = require("path");

const MANIFEST_DIR = path.join(__dirname, "..", "manifest");

const EXPECTED_SCRIPT_SRC = ["'self'", "'wasm-unsafe-eval'"];
const EXPECTED_OBJECT_SRC = ["'self'"];

const FORBIDDEN_SOURCES = [
    "'unsafe-eval'",
    "'unsafe-inline'",
    "http:",
    "https:",
    "data:",
    "blob:",
    "*",
];

function readManifest(name) {
    return JSON.parse(
        fs.readFileSync(path.join(MANIFEST_DIR, name + ".json"), "utf8"),
    );
}

// "script-src 'self'; object-src 'self'" -> { "script-src": ["'self'"], ... }
function parseCsp(policy) {
    const directives = {};
    for (const part of policy.split(";")) {
        const tokens = part.trim().split(/\s+/).filter(Boolean);
        if (tokens.length === 0) continue;
        directives[tokens[0]] = tokens.slice(1);
    }
    return directives;
}

function assertPolicy(policy) {
    const directives = parseCsp(policy);
    expect(Object.keys(directives).sort()).toEqual([
        "object-src",
        "script-src",
    ]);
    expect(directives["script-src"].slice().sort()).toEqual(
        EXPECTED_SCRIPT_SRC,
    );
    expect(directives["object-src"].slice().sort()).toEqual(
        EXPECTED_OBJECT_SRC,
    );
    for (const source of FORBIDDEN_SOURCES) {
        expect(directives["script-src"]).not.toContain(source);
        expect(directives["object-src"]).not.toContain(source);
    }
}

describe("shipped Content Security Policy", () => {
    // MV3 takes an object and applies extension_pages to the popup and the
    // background service worker, which is where libsodium runs.
    test("chrome MV3 allows WASM and nothing else beyond 'self'", () => {
        const csp = readManifest("chrome").content_security_policy;
        expect(typeof csp).toBe("object");
        expect(Object.keys(csp)).toEqual(["extension_pages"]);
        assertPolicy(csp.extension_pages);
    });

    // MV2 takes the policy as a bare string. Firefox does not require
    // 'wasm-unsafe-eval' for MV2 today — enforcement is report-only and
    // Bugzilla 1770909 is still open — so that token is future-proofing
    // for when it lands, not a mandate, and it stays inside Firefox's MV2
    // base-CSP ceiling. object-src 'self' is the load-bearing half: a
    // Firefox before 106 rejects an MV2 policy string that omits
    // object-src and falls back to its own default, discarding everything
    // declared here. Same policy as Chrome, different manifest shape.
    test("firefox MV2 allows WASM and nothing else beyond 'self'", () => {
        const csp = readManifest("firefox").content_security_policy;
        expect(typeof csp).toBe("string");
        assertPolicy(csp);
    });

    // The two targets share one codebase and one crypto path; a policy
    // that drifts apart between them means one of the two builds is
    // running a backend nothing tests.
    test("both targets ship the same policy", () => {
        const chrome =
            readManifest("chrome").content_security_policy.extension_pages;
        const firefox = readManifest("firefox").content_security_policy;
        expect(firefox).toBe(chrome);
    });
});
