// acceptance grid for samba-provision.ts pure layer.
//
// Locks every branch of the rendering, merging, removal, interface-pick, and
// marker-format functions. No spawnSync, no /etc reads — every test feeds
// concrete strings or interface fixtures. Mirrors apt-resolve.test.ts in
// style and runs under `node --test dist/__tests__/*.test.js` after build.
import test from "node:test";
import assert from "node:assert/strict";
import { renderBrandStanza, renderGlobalSection, renderFullSmbConf, pickLanInterface, mergeSmbConf, removeBrandStanza, hasAnyBrandStanza, formatSambaMarker, SAMBA_STEPS, } from "../samba-provision.js";
// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------
const PI_WLAN0_ONLY = {
    lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
    wlan0: [{ address: "192.168.1.50", netmask: "255.255.255.0", family: "IPv4", mac: "aa:bb:cc:dd:ee:ff", internal: false, cidr: "192.168.1.50/24" }],
};
const PI_BOTH_IFACES = {
    lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
    eth0: [{ address: "10.0.0.5", netmask: "255.255.255.0", family: "IPv4", mac: "11:11:11:11:11:11", internal: false, cidr: "10.0.0.5/24" }],
    wlan0: [{ address: "192.168.1.50", netmask: "255.255.255.0", family: "IPv4", mac: "aa:bb:cc:dd:ee:ff", internal: false, cidr: "192.168.1.50/24" }],
};
const PI_ETH0_ONLY = {
    lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
    eth0: [{ address: "10.0.0.5", netmask: "255.255.255.0", family: "IPv4", mac: "11:11:11:11:11:11", internal: false, cidr: "10.0.0.5/24" }],
};
const PI_LO_ONLY = {
    lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
};
const PI_IPV6_ONLY_LAN = {
    lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
    eth0: [{ address: "fe80::1", netmask: "ffff:ffff:ffff:ffff::", family: "IPv6", mac: "11:11:11:11:11:11", internal: false, cidr: "fe80::1/64", scopeid: 0 }],
};
const PI_USB0_FALLBACK = {
    lo: [{ address: "127.0.0.1", netmask: "255.0.0.0", family: "IPv4", mac: "00:00:00:00:00:00", internal: true, cidr: "127.0.0.1/8" }],
    usb0: [{ address: "172.17.0.2", netmask: "255.255.255.0", family: "IPv4", mac: "22:22:22:22:22:22", internal: false, cidr: "172.17.0.2/24" }],
};
// ---------------------------------------------------------------------------
// pickLanInterface
// ---------------------------------------------------------------------------
test("pickLanInterface: wlan0 wins over eth0 when both present", () => {
    assert.equal(pickLanInterface(PI_BOTH_IFACES), "wlan0");
});
test("pickLanInterface: wlan0 alone is picked", () => {
    assert.equal(pickLanInterface(PI_WLAN0_ONLY), "wlan0");
});
test("pickLanInterface: eth0 alone is picked", () => {
    assert.equal(pickLanInterface(PI_ETH0_ONLY), "eth0");
});
test("pickLanInterface: fallback to any non-loopback IPv4 (usb0)", () => {
    assert.equal(pickLanInterface(PI_USB0_FALLBACK), "usb0");
});
test("pickLanInterface: returns null when only loopback is present", () => {
    assert.equal(pickLanInterface(PI_LO_ONLY), null);
});
test("pickLanInterface: returns null when the only non-loopback iface has no IPv4", () => {
    // smbd cannot bind to an IPv6-only address with `interfaces = lo eth0` syntax
    // unmodified; treat as no usable LAN interface so caller loud-fails rather
    // than writing a broken smb.conf.
    assert.equal(pickLanInterface(PI_IPV6_ONLY_LAN), null);
});
// ---------------------------------------------------------------------------
// renderBrandStanza — exact spec match
// ---------------------------------------------------------------------------
test("renderBrandStanza emits the spec directives in order", () => {
    const stanza = renderBrandStanza({ brand: "maxy-code", sharePath: "/home/admin/maxy-code", installOwner: "admin" });
    assert.equal(stanza, [
        "[maxy-code]",
        "   path = /home/admin/maxy-code",
        "   read only = no",
        "   valid users = admin",
        "   force user = admin",
        "   browseable = yes",
        "   create mask = 0664",
        "   directory mask = 0775",
        "",
    ].join("\n"));
});
test("renderBrandStanza handles realagent-code with its own share path", () => {
    const stanza = renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code", installOwner: "admin" });
    assert.match(stanza, /^\[realagent-code\]\n/);
    assert.match(stanza, /\n   path = \/home\/admin\/realagent-code\n/);
    assert.match(stanza, /\n   force user = admin\n/);
});
test("renderBrandStanza substitutes a non-admin install owner into valid users and force user", () => {
    // Laptop case (Task 534): the install owner is the operator's Unix user
    // (`neo` etc.), not the literal `admin`. The same renderer must produce a
    // stanza whose `valid users` and `force user` name that owner, so SMB
    // clients authenticate as a Unix user that actually exists on the device.
    const stanza = renderBrandStanza({ brand: "maxy-code", sharePath: "/home/neo/maxy-code", installOwner: "neo" });
    assert.match(stanza, /\n   valid users = neo\n/);
    assert.match(stanza, /\n   force user = neo\n/);
    assert.doesNotMatch(stanza, /valid users = admin/);
    assert.doesNotMatch(stanza, /force user = admin/);
});
test("mergeSmbConf preserves a peer brand stanza when the new install owner differs", () => {
    // Peer-brand isolation must hold across owners: a laptop install
    // (installOwner=neo) adding its `[maxy-code]` stanza to a conf that
    // already holds a peer `[realagent-code]` stanza provisioned for owner
    // `admin` must leave the peer stanza byte-identical. The new stanza
    // carries the laptop owner; the peer keeps its own. Task 534.
    const peerStanza = renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code", installOwner: "admin" });
    const existing = renderGlobalSection({ lanInterface: "eth0" }) + peerStanza;
    const merged = mergeSmbConf({ existing, brand: "maxy-code", sharePath: "/home/neo/maxy-code", lanInterface: "wlan0", installOwner: "neo" });
    assert.ok(merged.includes(peerStanza), "peer brand stanza must survive unchanged");
    assert.match(merged, /\[maxy-code\][\s\S]+valid users = neo[\s\S]+force user = neo/);
});
// ---------------------------------------------------------------------------
// renderGlobalSection — LAN-only enforcement
// ---------------------------------------------------------------------------
test("renderGlobalSection binds to lo + LAN interface only", () => {
    const globals = renderGlobalSection({ lanInterface: "wlan0" });
    assert.match(globals, /^\[global\]\n/);
    assert.match(globals, /\n   interfaces = lo wlan0\n/);
    assert.match(globals, /\n   bind interfaces only = yes\n/);
    // No `map to guest = ok` — guests are explicitly rejected as bad-user.
    assert.match(globals, /\n   map to guest = bad user\n/);
});
// ---------------------------------------------------------------------------
// renderFullSmbConf — composition order
// ---------------------------------------------------------------------------
test("renderFullSmbConf places globals before the brand stanza", () => {
    const conf = renderFullSmbConf({ brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
    const globalIdx = conf.indexOf("[global]");
    const brandIdx = conf.indexOf("[maxy-code]");
    assert.ok(globalIdx >= 0 && brandIdx > globalIdx, "[global] must come before [maxy-code]");
});
// ---------------------------------------------------------------------------
// mergeSmbConf — idempotency and section preservation
// ---------------------------------------------------------------------------
test("mergeSmbConf: empty existing conf gets globals + brand stanza", () => {
    const merged = mergeSmbConf({ existing: "", brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
    assert.match(merged, /\[global\][\s\S]+\[maxy-code\]/);
    assert.match(merged, /interfaces = lo wlan0/);
});
test("mergeSmbConf: idempotent on second application", () => {
    const once = mergeSmbConf({ existing: "", brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
    const twice = mergeSmbConf({ existing: once, brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
    assert.equal(twice, once, "second merge must produce byte-identical output");
});
test("mergeSmbConf: replaces an existing global section in place, preserving peer stanzas", () => {
    const existing = [
        "[global]",
        "   workgroup = OLDGROUP",
        "   interfaces = lo eth0 wlan0",
        "",
        "[printers]",
        "   path = /var/spool/samba",
        "   guest ok = yes",
        "",
    ].join("\n");
    const merged = mergeSmbConf({ existing, brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
    assert.match(merged, /interfaces = lo wlan0/);
    assert.doesNotMatch(merged, /workgroup = OLDGROUP/);
    assert.match(merged, /\[printers\][\s\S]+path = \/var\/spool\/samba/, "[printers] stanza must survive");
    assert.match(merged, /\[maxy-code\][\s\S]+path = \/home\/admin\/maxy-code/);
});
test("mergeSmbConf: replaces an existing brand stanza in place, leaves peer brand alone", () => {
    const peerStanza = renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code", installOwner: "admin" });
    const oldBrandStanza = "[maxy-code]\n   path = /OLD/PATH\n   read only = yes\n\n";
    const existing = renderGlobalSection({ lanInterface: "eth0" }) + oldBrandStanza + peerStanza;
    const merged = mergeSmbConf({ existing, brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
    assert.doesNotMatch(merged, /\/OLD\/PATH/, "old maxy-code path must be replaced");
    assert.doesNotMatch(merged, /read only = yes/, "old maxy-code directives must be replaced");
    assert.match(merged, /\[realagent-code\][\s\S]+path = \/home\/admin\/realagent-code/, "peer brand stanza must survive");
    assert.match(merged, /\[maxy-code\][\s\S]+path = \/home\/admin\/maxy-code/);
});
test("mergeSmbConf: appends brand stanza when not present, keeps existing peer stanzas", () => {
    const peerStanza = renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code", installOwner: "admin" });
    const existing = renderGlobalSection({ lanInterface: "eth0" }) + peerStanza;
    const merged = mergeSmbConf({ existing, brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
    assert.match(merged, /\[realagent-code\]/, "peer brand stanza must survive");
    assert.match(merged, /\[maxy-code\][\s\S]+path = \/home\/admin\/maxy-code/, "new brand stanza must be appended");
});
// ---------------------------------------------------------------------------
// removeBrandStanza — peer-brand isolation
// ---------------------------------------------------------------------------
test("removeBrandStanza: removes only this brand, leaves peer brand and global intact", () => {
    const conf = mergeSmbConf({
        existing: renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code", installOwner: "admin" }),
        brand: "maxy-code",
        sharePath: "/home/admin/maxy-code",
        lanInterface: "wlan0",
        installOwner: "admin",
    });
    const after = removeBrandStanza({ existing: conf, brand: "maxy-code" });
    assert.doesNotMatch(after, /\[maxy-code\]/);
    assert.match(after, /\[realagent-code\]/);
    assert.match(after, /\[global\]/);
});
test("removeBrandStanza: returns input unchanged when stanza is absent", () => {
    const conf = mergeSmbConf({ existing: "", brand: "realagent-code", sharePath: "/home/admin/realagent-code", lanInterface: "wlan0", installOwner: "admin" });
    const after = removeBrandStanza({ existing: conf, brand: "maxy-code" });
    assert.equal(after, conf);
});
// ---------------------------------------------------------------------------
// hasAnyBrandStanza — apt-purge gate
// ---------------------------------------------------------------------------
test("hasAnyBrandStanza: false when only [global] is present", () => {
    const conf = renderGlobalSection({ lanInterface: "wlan0" });
    assert.equal(hasAnyBrandStanza(conf), false);
});
test("hasAnyBrandStanza: true when any non-global stanza is present", () => {
    const conf = renderGlobalSection({ lanInterface: "wlan0" }) +
        renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code", installOwner: "admin" });
    assert.equal(hasAnyBrandStanza(conf), true);
});
test("hasAnyBrandStanza: false when both brand stanzas have been removed", () => {
    let conf = renderFullSmbConf({ brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
    conf = mergeSmbConf({ existing: conf, brand: "realagent-code", sharePath: "/home/admin/realagent-code", lanInterface: "wlan0", installOwner: "admin" });
    conf = removeBrandStanza({ existing: conf, brand: "maxy-code" });
    conf = removeBrandStanza({ existing: conf, brand: "realagent-code" });
    assert.equal(hasAnyBrandStanza(conf), false);
});
// ---------------------------------------------------------------------------
// formatSambaMarker / SAMBA_STEPS — the install-invariant contract
// ---------------------------------------------------------------------------
test("SAMBA_STEPS is exactly [apt, conf, user, units] in order", () => {
    // Locked here so a refactor that reorders the steps must update this test
    // deliberately. The task brief pins the four-step contract.
    assert.deepEqual([...SAMBA_STEPS], ["apt", "conf", "user", "units"]);
});
test("formatSambaMarker emits the `[install-invariant] samba-provision-<step> <state>` shape", () => {
    assert.equal(formatSambaMarker("apt", "ok"), "[install-invariant] samba-provision-apt ok");
    assert.equal(formatSambaMarker("user", "deferred reason=no-plaintext-pin"), "[install-invariant] samba-provision-user deferred reason=no-plaintext-pin");
    assert.equal(formatSambaMarker("units", "fail: Job for smbd.service failed"), "[install-invariant] samba-provision-units fail: Job for smbd.service failed");
});
