//! protocol-registry-test — the registrar's own test fixture
//! (docs/os-development/protocol-namespace.md). One binary, four roles picked by
//! argv, driven by the `protocol-registry` kernel case:
//!
//! - `protocol-registry-test provider <mark>` — binds `/protocol/test/registry`
//!   and answers every request with its one-byte `<mark>`, so a client can tell
//!   *which instance* it reached.
//! - `protocol-registry-test deputy` / `... launder` — the two halves of the
//!   laundering-deputy probe (assertion 7 below).
//! - `protocol-registry-test run` — the driver, and the assertions:
//!     1. a bind of a name this binary is not granted is refused, `-EPERM`;
//!     2. the kernel's reserved prefix holds: nothing may mount at or under
//!        `/protocol`, and nobody may unmount it;
//!     3. a **forged power event** posted straight into the registry endpoint
//!        does not shut the machine down — the registrar answers privileged
//!        control traffic on the sender's kernel-stamped identity, never on two
//!        bytes of payload. The system surviving to the next line is the
//!        assertion, and it is the strongest one available: a regression takes
//!        the whole boot down;
//!     4. zero-length calls carrying a capability do not consume PID 1's handle
//!        table — after sixty-four of them a legitimate bind still succeeds;
//!     5. a bind colliding with a **live** owner is refused, `-EBUSY`;
//!     6. killing the provider makes the cached channel fail (`-EPEER`), and a
//!        client that re-resolves reaches the **restarted** instance — the
//!        restart story falling out of the naming layer, with no reconnect verb
//!        anywhere;
//!     7. the laundering deputy is refused: the *same binary* under the *same
//!        supervisor name* binds when its supervisor is a task the kernel
//!        started, and is refused one hop further down, where the supervisor is
//!        a task nobody authorized. The pair is the point — one alone would pass
//!        for reasons that have nothing to do with the rule under test;
//!     8. a **forged terminate signal** does not reach PID 1 either: the kernel
//!        refuses to bind this process's signal delivery to an endpoint it does
//!        not own, so "bind init's endpoint, then signal yourself" — which needs
//!        no forgery at all, only two ungated syscalls — cannot happen;
//!     9. the same for the other kernel notifications: a timer and an exit
//!        subscription may only be armed on one's own endpoint;
//!    10. the handle-table storm of assertion 4, aimed at a **harness-run**
//!        service instead of PID 1 — the loop every other service in the system
//!        runs — after which a capability-passing operation still works.
//!
//! Assertions 8–10 each pair a refusal with a control (the same operation on an
//! endpoint this process created, which must succeed), because a refusal alone
//! would pass just as happily against a kernel that refused everything.
//!
//! Prints `protocol-registry: ok` on success, or a `protocol-registry: FAIL`
//! line naming the step. Spawned bare (the initial-ramdisk sweep starts every
//! bundled binary), it exits silently so it cannot derange other tests.

const std = @import("std");
const channel = @import("channel");
const envelope = @import("envelope");
const file_system = @import("file-system");
const ipc = @import("ipc");
const logging = @import("logging");
const power_protocol = @import("power-protocol");
const process = @import("process");
const service = @import("service");
const time = @import("time");

/// The contract this fixture's provider role claims — under `/protocol/test`,
/// the subtree every `/test/` binary is granted.
const contract = "test/registry";

/// Where the deputy and the laundered grandchild report their bind verdicts. The
/// driver binds it and listens; both children reach it by name like any client.
const verdict_contract = "test/verdict";

/// The name the deputy claims (it may — its supervisor is the kernel-started
/// driver) and the one its own child attempts (it may not — its supervisor is
/// the deputy, a task neither init nor the kernel started).
const deputy_contract = "test/deputy";
const laundered_contract = "test/laundered";

/// A contract this fixture is deliberately NOT granted. It belongs to the
/// display service, and no manifest row names this binary against it.
const forbidden = "display";

fn fail(step: []const u8) noreturn {
    _ = logging.write("protocol-registry: FAIL ");
    _ = logging.write(step);
    _ = logging.write("\n");
    process.exit(1);
}

// --- the provider role ------------------------------------------------------

/// Which instance this is, echoed to every caller. The point of the whole
/// exercise: a client cannot tell instances apart by the name it opened, so the
/// provider says so itself.
var mark: u8 = '?';

/// The provider's one operation, plus `handover_operation` — the capability-passing
/// verb the harness-exhaustion probe needs. Claiming the arriving capability with
/// `take` and *using* it is the point: it proves the harness handed over a working
/// handle, which a service whose table is full could not have been given.
fn answer(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
    _ = sender;
    if (message.len != 0 and message[0] == handover_operation) {
        if (arrived.take()) |handed| {
            _ = ipc.send(handed, &[_]u8{mark}); // speak on it, then it is ours to close
            _ = ipc.close(handed);
        }
    }
    if (reply.len == 0) return 0;
    reply[0] = mark;
    return 1;
}

/// The one request byte the provider treats as "here is an endpoint, use it".
const handover_operation: u8 = 's';

// --- the laundering-deputy roles --------------------------------------------

/// One child's bind verdict, posted to the driver over `test/verdict`. Sent
/// asynchronously (no reply owed) so a child can report and exit without the
/// driver having to be waiting at that instant.
const Verdict = extern struct {
    /// `deputy` or `launder` — which of the two spoke.
    role: u8,
    _reserved: [3]u8 = .{0} ** 3,
    /// The registry's answer: 0 bound, or a negative errno.
    status: i32,
};

const role_deputy: u8 = 'd';
const role_launder: u8 = 'l';

/// Report a verdict to the driver, best-effort: the driver has a deadline, and a
/// child that cannot reach it must not hang the boot.
fn report(role: u8, status: i32) void {
    const verdict = Verdict{ .role = role, .status = status };
    var attempts: u32 = 0;
    while (attempts < 200) : (attempts += 1) {
        if (channel.openEndpoint(verdict_contract)) |endpoint| {
            defer _ = ipc.close(endpoint);
            if (ipc.send(endpoint, std.mem.asBytes(&verdict))) return;
        }
        time.sleepMillis(20);
    }
}

/// The middle of the laundered chain: a granted binary whose own supervisor is
/// the kernel-started driver. It binds — that is the control, and it is what
/// makes the refusal below mean something — then starts an instance of *itself*
/// and stays alive while that instance asks for a name, so the registry judges a
/// live, fully attestable chain rather than an orphan.
fn deputy() void {
    const spare = ipc.createIpcEndpoint() orelse return;
    report(role_deputy, verdictOf(deputy_contract, spare));

    const exits = ipc.createIpcEndpoint() orelse return;
    const child = process.spawnSupervised("protocol-registry-test", &.{"launder"}, exits) orelse return;
    awaitExit(exits, child);
}

/// The laundered grandchild. Its binary is granted, and its supervisor's binary
/// is granted as a supervisor — every *name* in the row matches. The one thing
/// that does not is which task its supervisor is: nobody authorized this deputy,
/// so nothing it spawns inherits a name.
fn launder() void {
    const spare = ipc.createIpcEndpoint() orelse return;
    report(role_launder, verdictOf(laundered_contract, spare));
}

// --- the driver role --------------------------------------------------------

/// Ask whoever is on the other end of `endpoint` which instance they are. Null
/// if the call failed — which, after a provider dies, is `-EPEER`.
fn instanceOf(endpoint: ipc.Handle) ?u8 {
    var reply: [8]u8 = undefined;
    const n = ipc.call(endpoint, "?", &reply) catch return null;
    if (n != 1) return null;
    return reply[0];
}

/// Open `/protocol/<contract>` until the instance marked `wanted` answers,
/// re-resolving each time — exactly the recovery a client performs when its
/// provider dies. Returns the live endpoint. Handles from a failed attempt are
/// closed: a 32-slot table does not survive four hundred leaks.
fn reach(wanted: u8) ?ipc.Handle {
    var attempts: u32 = 0;
    while (attempts < 400) : (attempts += 1) {
        if (channel.openEndpoint(contract)) |endpoint| {
            if (instanceOf(endpoint)) |seen| {
                if (seen == wanted) return endpoint;
            }
            _ = ipc.close(endpoint);
        }
        time.sleepMillis(20);
    }
    return null;
}

/// Block until child `id`'s exit notification lands on `endpoint`.
fn awaitExit(endpoint: ipc.Handle, id: u32) void {
    var scratch: [8]u8 = undefined;
    var guard: u32 = 0;
    while (guard < 1000) : (guard += 1) {
        const got = ipc.replyWait(endpoint, scratch[0..0], &scratch, null);
        if (got.isChildExit() and got.childProcessId() == id) return;
    }
    fail("the provider's exit notification never arrived");
}

/// One bind, retried only while the registry is *unreachable* — a registry that
/// answered has decided. Null means it never answered, which the caller reports
/// in its own words: "not mounted yet" and "stopped serving mid-test" are the
/// same silence and very different findings.
fn verdictWithin(name: []const u8, endpoint: ipc.Handle) ?i32 {
    var attempts: u32 = 0;
    while (attempts < 200) : (attempts += 1) {
        if (channel.bind(name, endpoint)) |status| return status;
        time.sleepMillis(20);
    }
    return null;
}

/// The registry answers, whatever it answers — the first bind also waits out a
/// registry that has not mounted `/protocol` yet.
fn verdictOf(name: []const u8, endpoint: ipc.Handle) i32 {
    return verdictWithin(name, endpoint) orelse fail("the registry never came up");
}

/// The registry's endpoint, obtained the way every process obtains it: resolve
/// `/protocol`. `fs_resolve` installs the backend's capability in ANY caller's
/// table, and the registrar's backend endpoint is PID 1's *supervision* endpoint
/// — the same mailbox its signals, timers, child deaths and power events land in.
/// That is precisely why what arrives there may not be believed on its content.
fn registryEndpoint() ?ipc.Handle {
    var relative: [channel.path_maximum]u8 = undefined;
    const route = file_system.fsResolve(channel.root, 0, &relative) orelse return null;
    return switch (route) {
        .kernel => null,
        .backend => |backend| backend.handle,
    };
}

/// Wait for both children of the laundering probe to report. Taken together
/// rather than one at a time because the two are independent processes and the
/// order they reach the driver is not the driver's business.
fn awaitVerdicts(endpoint: ipc.Handle) struct { deputy: i32, launder: i32 } {
    var scratch: [64]u8 = undefined;
    var from_deputy: ?i32 = null;
    var from_launder: ?i32 = null;
    var guard: u32 = 0;
    while (guard < 2000) : (guard += 1) {
        const got = ipc.replyWait(endpoint, scratch[0..0], &scratch, null);
        if (got.isMessage() and got.len >= @sizeOf(Verdict)) {
            const verdict = std.mem.bytesToValue(Verdict, scratch[0..@sizeOf(Verdict)]);
            if (verdict.role == role_deputy) from_deputy = verdict.status;
            if (verdict.role == role_launder) from_launder = verdict.status;
        }
        if (from_deputy) |d| {
            if (from_launder) |l| return .{ .deputy = d, .launder = l };
        }
    }
    fail("the deputy chain never reported its bind verdicts");
}

fn run() void {
    // A spare endpoint to offer the registry. Every bind below is meant to be
    // refused, so it is never actually claimed by anyone.
    const spare = ipc.createIpcEndpoint() orelse fail("create an endpoint to offer");

    // 1. A name this binary is not granted. Refusal is `-EPERM` at the bind, not
    //    silence: the caller *is* the provider, and telling it its manifest is
    //    wrong is not a leak.
    const ungranted = verdictOf(forbidden, spare);
    if (ungranted != -envelope.EPERM) fail("an ungranted bind was not refused with -EPERM");
    _ = logging.write("protocol-registry: ungranted bind refused\n");

    // 2. The kernel's residual rule: `/protocol` is claimed once, by PID 1, and
    //    the prefix is closed for the boot. Shadowing one contract by mounting
    //    under it is refused too, and so is deleting the namespace outright.
    if (file_system.mount("/protocol", spare)) fail("a second mount at /protocol was allowed");
    if (file_system.mount("/protocol/display", spare)) fail("a mount under /protocol was allowed");
    if (file_system.fsUnmount("/protocol")) fail("unmounting /protocol was allowed");
    _ = logging.write("protocol-registry: /protocol reserved\n");

    // 3. A forged power event. Resolving `/protocol` hands this process — any
    //    process — a sendable handle to PID 1's mailbox, and the bytes below are
    //    byte-for-byte what the real power service publishes when the button is
    //    pressed. If the registrar believed payloads, this single `send` would
    //    run the whole stop sequence and park PID 1 in its final sleep: every
    //    service terminated, the registry gone for the rest of the boot, and no
    //    way back. So the assertion is that the machine is still here afterwards
    //    — the strongest one available, because a regression does not fail this
    //    line, it takes the entire boot down with it.
    const registry = registryEndpoint() orelse fail("resolve /protocol");
    var forged: [envelope.post_maximum]u8 = undefined;
    const packet = power_protocol.Protocol.encodeEvent(.power_button, 0, .{}, &forged) orelse
        fail("frame a forged power event");
    if (!ipc.send(registry, packet)) fail("post a forged power event");
    const still_serving = verdictWithin(forbidden, spare) orelse
        fail("the registrar went silent after a forged power event — it acted on it");
    if (still_serving != -envelope.EPERM) fail("the registry misanswered after a forged power event");
    _ = logging.write("protocol-registry: forged power event ignored\n");

    // 4. Sixty-four zero-length calls, each carrying a capability. The kernel
    //    installs a sent capability in the receiver's table whatever the message
    //    length, so every one of these hands PID 1 a handle — and PID 1 has
    //    thirty-two slots. A registrar whose ping path returns without closing
    //    runs out before this loop is half done, after which nothing can ever
    //    bind again and the machine cannot recover for the rest of the boot.
    var storm: u32 = 0;
    while (storm < 64) : (storm += 1) {
        var scratch: [8]u8 = undefined;
        _ = ipc.callCap(registry, "", &scratch, spare) catch {};
    }
    //    The proof is a legitimate bind landing afterwards. This one doubles as
    //    the driver's listening post for step 7.
    const verdicts = ipc.createIpcEndpoint() orelse fail("create the verdict endpoint");
    const after_storm = verdictWithin(verdict_contract, verdicts) orelse
        fail("the registrar went silent after capability-carrying pings");
    if (after_storm != 0) fail("a legitimate bind failed after capability-carrying pings");
    _ = logging.write("protocol-registry: capability-carrying pings did not exhaust the registrar\n");

    // 5. A live owner's name is not for the taking. Collision is an error, never
    //    last-writer-wins — otherwise any process could impersonate any service.
    const exits = ipc.createIpcEndpoint() orelse fail("create the exit endpoint");
    const first = process.spawnSupervised("protocol-registry-test", &.{ "provider", "1" }, exits) orelse
        fail("spawn the first provider");
    const one = reach('1') orelse fail("the first provider never bound its contract");
    const collision = verdictOf(contract, spare);
    if (collision != -envelope.EBUSY) fail("a collision with a live owner was not refused with -EBUSY");
    _ = logging.write("protocol-registry: collision refused\n");

    // 6. The restart story. Kill the provider: the cached channel dies with it
    //    (the kernel fails calls on a dead endpoint rather than blocking
    //    forever), and re-resolving the same name reaches whatever instance the
    //    registry now points at. No reconnect verb, no client-side repair.
    if (!process.kill(first)) fail("kill the first provider");
    awaitExit(exits, first);
    if (instanceOf(one) != null) fail("a call on a dead provider's channel succeeded");
    _ = logging.write("protocol-registry: dead channel refused\n");

    _ = process.spawnSupervised("protocol-registry-test", &.{ "provider", "2" }, exits) orelse
        fail("spawn the replacement provider");
    const two = reach('2') orelse fail("re-resolving never reached the restarted provider");
    _ = logging.write("protocol-registry: restarted provider reached\n");

    // 7. The laundering deputy. Two binds by the *same binary*, whose grant rows
    //    read identically — same claimant path, same supervisor path — differing
    //    only in which task the supervisor is:
    //
    //      kernel -> this driver -> deputy        the deputy binds  (control)
    //      kernel -> this driver -> deputy -> ...  the grandchild does not
    //
    //    Nobody authorized the deputy to be a supervisor, so nothing it spawns
    //    inherits a name — which is the whole reason a supervisor is attested by
    //    task id and not by the string the kernel stamped on it. The control
    //    matters as much as the refusal: without it this step would pass just as
    //    happily against a registrar that refused everything.
    const deputy_exits = ipc.createIpcEndpoint() orelse fail("create the deputy's exit endpoint");
    _ = process.spawnSupervised("protocol-registry-test", &.{"deputy"}, deputy_exits) orelse
        fail("spawn the deputy");
    const chain = awaitVerdicts(verdicts);
    if (chain.deputy != 0) fail("a granted binary spawned by an AUTHORIZED task was refused");
    if (chain.launder != -envelope.EPERM) fail("a granted binary spawned by an UNAUTHORIZED task was not refused");
    _ = logging.write("protocol-registry: laundering deputy refused\n");

    // 8. A forged terminate SIGNAL. Step 3 proved a *payload* cannot reach PID 1's
    //    shutdown path; this is the same destination by the other road, and the
    //    interesting one, because nothing here is forged at all. `signal_bind`
    //    nominates where a process's own signals are delivered, and `process_signal`
    //    lets any task signal itself — both correct in isolation, and together a
    //    shutdown primitive the moment the delivery point may be somebody else's
    //    endpoint. `fs_resolve` hands every process a handle to PID 1's, so all it
    //    would take is: bind, then signal yourself. The badge the kernel stamps is
    //    genuine, which is exactly why init cannot filter it and the kernel has to.
    //
    //    The control comes first and matters as much as the refusal: an endpoint we
    //    created is accepted, so what the second line refuses is *foreignness* and
    //    not signals-in-general. If the second line ever succeeds again it also
    //    steals our own binding, so the terminate below lands in PID 1 and takes the
    //    boot with it — the assertion is the machine still being here.
    const signals = ipc.createIpcEndpoint() orelse fail("create the signal endpoint");
    if (!process.bindSignals(signals)) fail("binding signals to an endpoint we created was refused");
    if (process.bindSignals(registry)) fail("binding signals to the registry's endpoint was allowed");
    if (!process.sendSignal(process.taskId(), .terminate)) fail("post ourselves a terminate signal");
    const after_signal = verdictWithin(forbidden, spare) orelse
        fail("the registrar went silent after a redirected terminate signal — it acted on it");
    if (after_signal != -envelope.EPERM) fail("the registry misanswered after a redirected terminate signal");
    _ = logging.write("protocol-registry: foreign signal binding refused\n");

    // 9. The same rule for the other two kernel notifications. A timer landing
    //    carries no identity — no sender, no cookie — so a service cannot tell a
    //    timer it armed from one a stranger armed on its endpoint; init re-arms its
    //    heartbeat on every landing, so N smuggled timers leave N+1 beats running
    //    forever. Exit subscriptions are the same shape with a smaller blast radius
    //    (an eight-slot table, and a firehose of deaths aimed at a stranger).
    //    Control first again, and for the timer the control's *landing* is the proof
    //    the refusal is about ownership rather than a timer syscall that just fails.
    const ticker = ipc.createIpcEndpoint() orelse fail("create the timer endpoint");
    if (!time.timerOnce(ticker, 1)) fail("arming a timer on an endpoint we created was refused");
    if (time.timerOnce(registry, 1)) fail("arming a timer on the registry's endpoint was allowed");
    var tick: [8]u8 = undefined;
    if (!ipc.replyWait(ticker, tick[0..0], &tick, null).isTimer()) fail("our own timer did not land");
    if (process.subscribeExits(registry)) fail("subscribing the registry's endpoint to exit events was allowed");
    if (!process.subscribeExits(ticker)) fail("subscribing an endpoint we created to exit events was refused");
    _ = logging.write("protocol-registry: foreign timer and exit binding refused\n");

    // 9b. Receiving is the same privilege, and it was the one member of the family
    //     left unguarded. Every process holds a sendable handle to the registrar's
    //     mailbox — `fs_resolve` installs one for anyone who asks — and a stranger
    //     that could *dequeue* there would not merely evade the grants this fixture
    //     checks: it would take the provider endpoints that ride `bind` requests
    //     straight out of the queue, and answer other clients' opens in the
    //     registrar's name. Sending to it stays legal; receiving on it must not be.
    //     A refusal comes back as a negative errno in the length register, which
    //     is the whole point: the call returns instead of parking us on someone
    //     else's queue, where a success would have blocked until a request it was
    //     never ours to see arrived.
    var stolen: [8]u8 = undefined;
    const theft = ipc.replyWait(registry, stolen[0..0], &stolen, null);
    if (theft.len <= ~@as(usize, 0) - 4095) fail("receiving on the registry's endpoint was allowed");
    _ = logging.write("protocol-registry: foreign receive refused\n");

    // 10. The handle-table storm again, aimed at a **harness-run service** this
    //     time. Step 4 covers PID 1, which runs its own hand-written loop; every
    //     other service in the system — the VFS, the display compositor, the device
    //     manager, every driver — runs `library/kernel/service.zig`, and that loop
    //     had the identical hole: the zero-length ping answered without closing what
    //     the ping carried, and callbacks handed a bare handle they had no use for.
    //     The provider role below runs that same harness, so it stands in for all of
    //     them. Sixty-four capability-carrying pings is twice its thirty-two slots.
    var storm2: u32 = 0;
    while (storm2 < 64) : (storm2 += 1) {
        var scratch: [8]u8 = undefined;
        _ = ipc.callCap(two, "", &scratch, spare) catch {};
    }
    //     The proof is a *capability-passing* operation still working: the kernel
    //     installs the sent handle in the receiver's table before the service sees
    //     the request, so against an exhausted service the call fails outright. And
    //     the provider answers on the handed-over endpoint, so the handle it claimed
    //     is a working one and not merely a number.
    const handover = ipc.createIpcEndpoint() orelse fail("create the handover endpoint");
    var handover_reply: [8]u8 = undefined;
    const served = ipc.callCap(two, &[_]u8{handover_operation}, &handover_reply, handover) catch
        fail("a capability-passing call failed after capability-carrying pings — the harness leaked them");
    if (served.len != 1 or handover_reply[0] != '2') fail("the harness-run provider misanswered after capability-carrying pings");
    var echoed: [8]u8 = undefined;
    const back = ipc.replyWait(handover, echoed[0..0], &echoed, null);
    if (!back.isMessage() or back.len != 1 or echoed[0] != '2') fail("the provider never spoke on the endpoint it was handed");
    _ = logging.write("protocol-registry: capability-carrying pings did not exhaust the harness\n");

    _ = logging.write("protocol-registry: ok\n");
}

pub fn main(startup: process.Init) void {
    const role = startup.arguments.get(1) orelse return; // bare (ramdisk sweep): stay silent
    if (std.mem.eql(u8, role, "provider")) {
        const label = startup.arguments.get(2) orelse "?";
        if (label.len != 0) mark = label[0];
        service.run(64, .{ .service = contract, .on_message = answer });
        return; // terminate arrived, or the bind was refused; either way we are done
    }
    if (std.mem.eql(u8, role, "deputy")) return deputy();
    if (std.mem.eql(u8, role, "launder")) return launder();
    if (std.mem.eql(u8, role, "run")) run();
}
