// acceptance grid for classifyPortHolder (chromium argv-anchor).
// extended grid for Xtigervnc display anchor + websockify port
// anchor + holder-type detection on argv[0] basename / full-argv scan.
//
// The wrapper in index.ts owns ss(8), /proc reads, kill(2), and the operator-
// override exit. This suite exercises only the pure decision rule: ssOutput +
// brand identities → kind. Inputs in, decision out — no fs, no exec, no spawn.
//
// Cmdlines below mimic /proc/<pid>/cmdline format: argv joined by NUL bytes,
// possibly with a trailing NUL. The classifier requires this format so it
// can distinguish a flag value (a real ownership claim) from a different
// flag whose value happens to contain the marker substring.
//
// Runs via Node's built-in test runner — same convention as
// peer-brand-detect.test.ts.
import test from "node:test";
import assert from "node:assert/strict";
import { classifyPortHolder } from "../preflight-port-classifier.js";
// Per-brand identities mirror the live brands/<brand>/brand.json values that
// bundle.js stamps into payload/platform/config/brand-registry.json. Values
// are derived from the brand manifests, not hardcoded as ground-truth here:
// the test suite asserts the rule, not the numbers. Any rebalance in
// brands/*/brand.json updates the registry on the next bundle and the
// classifier consumes the new values without source change.
const MAXY = { configDir: ".maxy", vncDisplay: 99, websockifyPort: 6080 };
const REAL = { configDir: ".realagent", vncDisplay: 100, websockifyPort: 6081 };
const MAXY_2 = { configDir: ".maxy-2", vncDisplay: 101, websockifyPort: 6082 };
const MAXY_3 = { configDir: ".maxy-3", vncDisplay: 102, websockifyPort: 6083 };
const MAXY_4 = { configDir: ".maxy-4", vncDisplay: 103, websockifyPort: 6084 };
const ALL = [MAXY, REAL, MAXY_2, MAXY_3, MAXY_4];
const peers = (own) => ALL.filter(b => b.configDir !== own.configDir);
const SS_PID_42 = "LISTEN 0 4096  *:9222  *:*  users:((\"chrome\",pid=42,fd=12))";
const SS_PID_99 = "LISTEN 0 4096  *:9223  *:*  users:((\"chrome\",pid=99,fd=12))";
const SS_PID_777 = "LISTEN 0 5  127.0.0.1:5900  0.0.0.0:*  users:((\"Xtigervnc\",pid=777,fd=9))";
const SS_PID_888 = "LISTEN 0 5  127.0.0.1:5901  0.0.0.0:*  users:((\"Xtigervnc\",pid=888,fd=9))";
const SS_PID_555 = "LISTEN 0 5  *:6080  *:*  users:((\"websockify\",pid=555,fd=8))";
const cmd = (...argv) => argv.join("\0") + "\0";
// ---------------------------------------------------------------------------
// EMPTY / no pid / cmdline race
// ---------------------------------------------------------------------------
test("empty ssOutput → EMPTY", () => {
    const r = classifyPortHolder({
        ssOutput: "",
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => { throw new Error("should not be called"); },
    });
    assert.equal(r.kind, "EMPTY");
});
test("UNRELATED: ssOutput non-empty but no pid= token", () => {
    const r = classifyPortHolder({
        ssOutput: "Recv-Q Send-Q  Local Address:Port  Peer Address:Port",
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => { throw new Error("should not be called"); },
    });
    assert.equal(r.kind, "UNRELATED");
    assert.equal(r.pid, undefined);
});
test("UNRELATED: getCmdline throws (race — process exited between ss and read)", () => {
    const r = classifyPortHolder({
        ssOutput: SS_PID_42,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => { const e = new Error("ENOENT"); e.code = "ENOENT"; throw e; },
    });
    assert.equal(r.kind, "UNRELATED");
    assert.equal(r.pid, 42);
    assert.equal(r.cmdlineReadFailed, true);
});
test("ss header line containing 'pid=': last pid= wins", () => {
    // If a future ss locale prepends header text containing the literal `pid=`,
    // the first-match heuristic would lift the wrong value. The LISTEN row is
    // always last, so taking the last pid= match is the structural fix.
    const ssOutput = `State pid= notes\nLISTEN 0 4096  *:9222 *:*  users:(("chrome",pid=42,fd=12))`;
    const cmdline = cmd("chromium", "--user-data-dir=/home/neo/.maxy/chromium-profile");
    const r = classifyPortHolder({
        ssOutput,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: pid => { assert.equal(pid, 42, "must pick the LISTEN row's pid"); return cmdline; },
    });
    assert.equal(r.kind, "OWN_BRAND");
    assert.equal(r.pid, 42);
});
// ---------------------------------------------------------------------------
// Chromium (grid, migrated to BrandIdentity API)
// ---------------------------------------------------------------------------
test("chromium OWN_BRAND: --user-data-dir=PATH single argv", () => {
    const cmdline = cmd("/opt/google/chrome/chrome", "--user-data-dir=/home/neo/.maxy/chromium-profile", "--remote-debugging-port=9222");
    const r = classifyPortHolder({
        ssOutput: SS_PID_42,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: pid => { assert.equal(pid, 42); return cmdline; },
    });
    assert.equal(r.kind, "OWN_BRAND");
    assert.equal(r.holderType, "chromium");
    assert.equal(r.pid, 42);
    assert.equal(r.profilePath, "/home/neo/.maxy/chromium-profile");
});
test("chromium OWN_BRAND: --user-data-dir PATH split across two argvs", () => {
    const cmdline = cmd("/usr/bin/chromium", "--user-data-dir", "/home/admin/.maxy/chromium-profile", "--remote-debugging-port=9222");
    const r = classifyPortHolder({
        ssOutput: SS_PID_42,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => cmdline,
    });
    assert.equal(r.kind, "OWN_BRAND");
    assert.equal(r.holderType, "chromium");
    assert.equal(r.profilePath, "/home/admin/.maxy/chromium-profile");
});
test("chromium PEER_BRAND: cmdline holds another known brand's profile path", () => {
    const cmdline = cmd("/usr/bin/chromium", "--user-data-dir=/home/admin/.realagent/chromium-profile", "--remote-debugging-port=9223");
    const r = classifyPortHolder({
        ssOutput: SS_PID_99,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => cmdline,
    });
    assert.equal(r.kind, "PEER_BRAND");
    assert.equal(r.holderType, "chromium");
    assert.equal(r.pid, 99);
    assert.equal(r.profilePath, "/home/admin/.realagent/chromium-profile");
});
test("chromium UNRELATED: cmdline matches no known brand profile", () => {
    // Anchors only on the basename — "/usr/bin/python3" is not chromium.
    // Use an actual chromium binary with a non-brand profile path instead.
    const cmdline = cmd("chromium", "--user-data-dir=/tmp/scratch", "--remote-debugging-port=9999");
    const r = classifyPortHolder({
        ssOutput: SS_PID_42,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => cmdline,
    });
    assert.equal(r.kind, "UNRELATED");
    assert.equal(r.holderType, "chromium");
    assert.equal(r.pid, 42);
});
test("chromium UNRELATED: --user-data-dir absent (no profile claim)", () => {
    // Kernel threads, GPU/zygote/utility chrome processes inherit profile via
    // fork without re-stating --user-data-dir. They wouldn't be listening
    // anyway, but if one ever showed up here we must classify as UNRELATED.
    const cmdline = cmd("/opt/google/chrome/chrome", "--type=gpu-process", "--no-sandbox");
    const r = classifyPortHolder({
        ssOutput: SS_PID_42,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => cmdline,
    });
    assert.equal(r.kind, "UNRELATED");
    assert.equal(r.holderType, "chromium");
});
test("chromium substring boundary: own=.maxy must NOT match .maxy-2 cmdline", () => {
    const cmdline = cmd("chromium", "--user-data-dir=/home/neo/.maxy-2/chromium-profile");
    const r = classifyPortHolder({
        ssOutput: SS_PID_42,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => cmdline,
    });
    assert.equal(r.kind, "PEER_BRAND");
    assert.equal(r.profilePath, "/home/neo/.maxy-2/chromium-profile");
});
test("chromium substring boundary: own=.maxy-2 must NOT misclassify .maxy cmdline as own", () => {
    const cmdline = cmd("chromium", "--user-data-dir=/home/neo/.maxy/chromium-profile");
    const r = classifyPortHolder({
        ssOutput: SS_PID_42,
        ownBrand: MAXY_2,
        peerBrands: peers(MAXY_2),
        getCmdline: () => cmdline,
    });
    assert.equal(r.kind, "PEER_BRAND");
    assert.equal(r.profilePath, "/home/neo/.maxy/chromium-profile");
});
test("chromium argv-anchor safety: marker in --load-extension value must NOT match", () => {
    // Adversarial case: a Chrome flag whose value contains the profile-path
    // substring, but no actual --user-data-dir claim. Naive substring matching
    // would false-positive OWN_BRAND.
    const cmdline = cmd("chromium", "--load-extension=/tmp/decoy/.maxy/chromium-profile", "--remote-debugging-port=9222");
    const r = classifyPortHolder({
        ssOutput: SS_PID_42,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => cmdline,
    });
    assert.equal(r.kind, "UNRELATED");
});
test("chromium variant basenames: google-chrome-stable detected as chromium", () => {
    // Ubuntu Noble laptop replaces snap-confined chromium with
    // google-chrome-stable. The classifier must still recognise
    // it as a chromium-family holder.
    const cmdline = cmd("/usr/bin/google-chrome-stable", "--user-data-dir=/home/neo/.maxy/chromium-profile", "--remote-debugging-port=9222");
    const r = classifyPortHolder({
        ssOutput: SS_PID_42,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => cmdline,
    });
    assert.equal(r.kind, "OWN_BRAND");
    assert.equal(r.holderType, "chromium");
});
// ---------------------------------------------------------------------------
// Xtigervnc (display-number anchor)
// ---------------------------------------------------------------------------
test("xtigervnc OWN_BRAND: argv[1] :99 matches own.vncDisplay", () => {
    // Reproduces the laptop-maxy and Pi-maxy install-time log:
    //   cmdline: Xtigervnc :99 -geometry 1280x800 -depth 24 -rfbport 5900 …
    const cmdline = cmd("Xtigervnc", ":99", "-geometry", "1280x800", "-depth", "24", "-rfbport", "5900", "-localhost", "-SecurityTypes", "None", "-AlwaysShared", "-BlacklistThreshold", "1000000");
    const r = classifyPortHolder({
        ssOutput: SS_PID_777,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: pid => { assert.equal(pid, 777); return cmdline; },
    });
    assert.equal(r.kind, "OWN_BRAND");
    assert.equal(r.holderType, "xtigervnc");
    assert.equal(r.pid, 777);
    assert.equal(r.vncDisplay, 99);
});
test("xtigervnc PEER_BRAND: argv[1] :100 matches Real Agent vncDisplay when own is Maxy", () => {
    const cmdline = cmd("Xtigervnc", ":100", "-rfbport", "5901");
    const r = classifyPortHolder({
        ssOutput: SS_PID_888,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => cmdline,
    });
    assert.equal(r.kind, "PEER_BRAND");
    assert.equal(r.holderType, "xtigervnc");
    assert.equal(r.vncDisplay, 100);
});
test("xtigervnc UNRELATED: argv display number matches no known brand", () => {
    const cmdline = cmd("Xtigervnc", ":42", "-rfbport", "5942");
    const r = classifyPortHolder({
        ssOutput: SS_PID_777,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => cmdline,
    });
    assert.equal(r.kind, "UNRELATED");
    assert.equal(r.holderType, "xtigervnc");
    assert.equal(r.vncDisplay, 42);
});
test("xtigervnc UNRELATED: no `:N` token in argv", () => {
    // Defence-in-depth: if argv parsing ever sees an Xtigervnc invocation
    // with no display literal, classify UNRELATED rather than guessing.
    const cmdline = cmd("Xtigervnc", "-help");
    const r = classifyPortHolder({
        ssOutput: SS_PID_777,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => cmdline,
    });
    assert.equal(r.kind, "UNRELATED");
    assert.equal(r.holderType, "xtigervnc");
    assert.equal(r.vncDisplay, undefined);
});
// ---------------------------------------------------------------------------
// websockify (bind-port anchor with full-argv scan for the binary)
// ---------------------------------------------------------------------------
test("websockify OWN_BRAND: bind port [::]:6080 matches own.websockifyPort (direct invocation)", () => {
    const cmdline = cmd("websockify", "--web", "/usr/share/novnc", "[::]:6080", "localhost:5900");
    const r = classifyPortHolder({
        ssOutput: SS_PID_555,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: pid => { assert.equal(pid, 555); return cmdline; },
    });
    assert.equal(r.kind, "OWN_BRAND");
    assert.equal(r.holderType, "websockify");
    assert.equal(r.pid, 555);
    assert.equal(r.websockifyPort, 6080);
});
test("websockify OWN_BRAND: detected via argv scan when invoked through python", () => {
    // Pi Bookworm packages websockify as a python script with no shebang
    // honoured by systemd in some configs — argv[0] may be `python3` and
    // argv[1] = `/usr/bin/websockify`. The detector scans the full argv.
    const cmdline = cmd("/usr/bin/python3", "/usr/bin/websockify", "--web", "/usr/share/novnc", "[::]:6080", "localhost:5900");
    const r = classifyPortHolder({
        ssOutput: SS_PID_555,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => cmdline,
    });
    assert.equal(r.kind, "OWN_BRAND");
    assert.equal(r.holderType, "websockify");
    assert.equal(r.websockifyPort, 6080);
});
test("websockify PEER_BRAND: bind port matches Real Agent websockifyPort when own is Maxy", () => {
    const cmdline = cmd("websockify", "--web", "/usr/share/novnc", "[::]:6081", "localhost:5901");
    const r = classifyPortHolder({
        ssOutput: SS_PID_555,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => cmdline,
    });
    assert.equal(r.kind, "PEER_BRAND");
    assert.equal(r.holderType, "websockify");
    assert.equal(r.websockifyPort, 6081);
});
test("websockify UNRELATED: bind port matches no known brand", () => {
    const cmdline = cmd("websockify", "--web", "/usr/share/novnc", "[::]:9999", "localhost:5900");
    const r = classifyPortHolder({
        ssOutput: SS_PID_555,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => cmdline,
    });
    assert.equal(r.kind, "UNRELATED");
    assert.equal(r.holderType, "websockify");
    assert.equal(r.websockifyPort, undefined);
});
// ---------------------------------------------------------------------------
// Unknown holder (defence-in-depth)
// ---------------------------------------------------------------------------
test("UNRELATED holderType=unknown: argv[0] is neither chromium, Xtigervnc, nor websockify", () => {
    const cmdline = cmd("/usr/bin/python3", "-m", "http.server", "9222");
    const r = classifyPortHolder({
        ssOutput: SS_PID_42,
        ownBrand: MAXY,
        peerBrands: peers(MAXY),
        getCmdline: () => cmdline,
    });
    assert.equal(r.kind, "UNRELATED");
    assert.equal(r.holderType, "unknown");
    assert.equal(r.pid, 42);
    assert.equal(r.cmdline, "/usr/bin/python3 -m http.server 9222");
});
