//! process-test — a test fixture for process management (bundled in the
//! initial-ramdisk, driven by the `supervision` test case). One binary, three
//! roles picked by argv, so the whole user-side surface is exercised end to end:
//!
//! - `process-test run` — the supervisor: spawns the two children below with an
//!   exit-notification endpoint, sees them in `process_enumerate`, kills them,
//!   receives both exit notifications, and confirms they are gone. Prints
//!   "process-test: ok" for the kernel test to match, or a FAIL line naming the
//!   step that broke.
//! - `process-test sleeper` — a child that blocks in `sleep` forever: its kill
//!   exercises the immediate reap of a blocked task.
//! - `process-test spinner` — a child that spins in user mode making no system
//!   calls: its kill exercises the deferred path (kill_pending, finished by the
//!   timer tick).
//!
//! Spawned with no arguments (the initial-ramdisk sweep test starts every bundled
//! binary bare), it exits silently so it cannot derange other tests' output.

const std = @import("std");
const runtime = @import("runtime");

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

/// Whether process `id` appears in a fresh `process_enumerate` snapshot, named
/// `name` (an id present under the wrong name is a table mix-up, not a pass).
fn listed(id: u32, name: []const u8) bool {
    var table: [32]runtime.system.ProcessDescriptor = undefined;
    const total = runtime.system.processes(&table);
    for (table[0..@min(total, table.len)]) |descriptor| {
        if (descriptor.id != id) continue;
        return std.mem.eql(u8, descriptor.name[0..descriptor.name_length], name);
    }
    return false;
}

/// Block on the exit endpoint until a child-exit notification arrives; returns
/// the ended child's id. A wrong wake-up (there should be none — nothing else
/// knows this endpoint) fails the test rather than looping forever.
fn awaitChildExit(endpoint: runtime.ipc.Handle) u32 {
    var scratch: [8]u8 = undefined;
    const received = runtime.ipc.replyWait(endpoint, scratch[0..0], &scratch, null);
    if (!received.isChildExit()) fail("expected a child-exit notification");
    return received.childProcessId();
}

/// The harness-run child of the signals test: echoes requests, logs the two
/// signals it handles. Terminate makes run() return, and returning from main is
/// the clean exit the parent reads as ExitReason.exited.
fn echo(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
    _ = sender;
    _ = capability;
    const n = @min(message.len, reply.len);
    @memcpy(reply[0..n], message[0..n]);
    return n;
}

fn onReload() void {
    _ = runtime.system.write("process-test: reloaded\n");
}

fn onTerminate() void {
    _ = runtime.system.write("process-test: terminating\n");
}

/// The parent of the signals test: drives ping, echo, reload, the one-shot
/// timer, and both endings of the stop sequence (polite -> exited; deaf ->
/// killed at the deadline). Prints "process-test: signals ok" as the marker.
fn signalRun() void {
    const endpoint = runtime.ipc.createIpcEndpoint() orelse fail("create exit endpoint");
    const child = runtime.system.spawnSupervised("process-test", &.{"service"}, endpoint) orelse fail("spawn service child");

    // Reach the child's endpoint through the registry (retry: it may not be up).
    var service_handle: ?runtime.ipc.Handle = null;
    var tries: u32 = 0;
    while (service_handle == null and tries < 200) : (tries += 1) {
        service_handle = runtime.ipc.lookup(.input);
        if (service_handle == null) runtime.system.sleep(20);
    }
    const h = service_handle orelse fail("service child never registered");

    // The universal ping: a zero-length call answered zero-length by the harness.
    var reply: [16]u8 = undefined;
    const pong = runtime.ipc.call(h, &.{}, &reply) catch fail("ping call failed");
    if (pong != 0) fail("ping reply not empty");

    // An ordinary request still reaches on_message.
    const n = runtime.ipc.call(h, "echo!", &reply) catch fail("echo call failed");
    if (n != 5 or !std.mem.eql(u8, reply[0..5], "echo!")) fail("echo mismatch");

    // reload: a statement — the child logs it; the kernel test reads the serial.
    if (!runtime.process.sendSignal(child, .reload)) fail("send reload");
    runtime.system.sleep(200);

    // The one-shot timer: armed on our endpoint, lands as isTimer.
    if (!runtime.system.timerOnce(endpoint, 100)) fail("arm timer");
    var scratch: [8]u8 = undefined;
    const landing = runtime.ipc.replyWait(endpoint, scratch[0..0], &scratch, null);
    if (!landing.isTimer()) fail("expected the timer landing");

    // The stop sequence, polite path: terminate, clean exit inside the deadline.
    runtime.process.stop(child, 2000, endpoint);
    if ((runtime.process.exitReason(child) orelse .killed) != .exited) fail("service child reason not exited");

    // The deaf child: binds nothing, hears nothing — the deadline kills it.
    const deaf = runtime.system.spawnSupervised("process-test", &.{"sleeper"}, endpoint) orelse fail("spawn deaf child");
    runtime.system.sleep(50); // let it reach its sleep
    runtime.process.stop(deaf, 300, endpoint);
    if ((runtime.process.exitReason(deaf) orelse .exited) != .killed) fail("deaf child reason not killed");

    _ = runtime.system.write("process-test: signals ok\n");
}

pub fn main(init: runtime.process.Init) void {
    const role = init.arguments.get(1) orelse return; // spawned bare (ramdisk sweep): stay silent
    if (std.mem.eql(u8, role, "sleeper")) {
        while (true) runtime.system.sleep(500);
    }
    if (std.mem.eql(u8, role, "service")) {
        // Borrowed well-known id: the input service is not part of this scenario.
        runtime.service.run(64, .{
            .service = .input,
            .on_message = echo,
            .on_reload = onReload,
            .on_terminate = onTerminate,
        });
        return; // terminate arrived; returning is the clean exit
    }
    if (std.mem.eql(u8, role, "signal-run")) {
        signalRun();
        return;
    }
    if (std.mem.eql(u8, role, "spinner")) {
        var beat: u64 = 0;
        const touch: *volatile u64 = &beat;
        while (true) touch.* +%= 1; // user mode only — no system calls to die at
    }

    // The supervisor ("run").
    const endpoint = runtime.ipc.createIpcEndpoint() orelse fail("create exit endpoint");

    const sleeper = runtime.system.spawnSupervised("process-test", &.{"sleeper"}, endpoint) orelse fail("spawn sleeper");
    const spinner = runtime.system.spawnSupervised("process-test", &.{"spinner"}, endpoint) orelse fail("spawn spinner");

    runtime.system.sleep(100); // let the sleeper block and the spinner get a core
    if (!listed(sleeper, "/system/tests/process-test")) fail("sleeper not in process_enumerate");
    if (!listed(spinner, "/system/tests/process-test")) fail("spinner not in process_enumerate");

    // Kills that must be refused: a kernel task (id 0), and an id that was never
    // issued — both -ESRCH. (-EPERM needs a second supervisor; the kernel-level
    // `process-kill` test covers it.)
    if (runtime.system.kill(0)) fail("killing a kernel task was allowed");
    if (runtime.system.kill(0xFFFF_FFF0)) fail("killing an unknown id was allowed");

    // The blocked child: usually reaped on the spot (it sits in `sleep`). The
    // notification is the fence — after it, the child is certainly gone, so the
    // second kill must miss (its id is never reused).
    if (!runtime.system.kill(sleeper)) fail("kill sleeper");
    if (awaitChildExit(endpoint) != sleeper) fail("sleeper exit notification");
    if (runtime.system.kill(sleeper)) fail("double kill was allowed");

    // The running child: the deferred path — condemned now, dead by the next tick.
    if (!runtime.system.kill(spinner)) fail("kill spinner");
    if (awaitChildExit(endpoint) != spinner) fail("spinner exit notification");

    if (listed(sleeper, "/system/tests/process-test")) fail("sleeper still listed after kill");
    if (listed(spinner, "/system/tests/process-test")) fail("spinner still listed after kill");

    // M17.2: both children were killed by us, and the reason says so — the whole
    // restart-policy input, read through the runtime like a real supervisor would.
    if ((runtime.process.exitReason(sleeper) orelse .exited) != .killed) fail("sleeper reason not killed");
    if ((runtime.process.exitReason(spinner) orelse .exited) != .killed) fail("spinner reason not killed");
    if (runtime.process.exitReason(0xFFFF_FFF0) != null) fail("unknown id had a reason");

    _ = runtime.system.write("process-test: ok\n");
}
