mod support;

use nix::libc;
use secure_exec_sidecar::wire::{
    EventPayload, GetSignalStateRequest, GuestRuntimeKind, KillProcessRequest,
    ProcessSnapshotStatus, RequestPayload, ResponsePayload, SignalDispositionAction,
    SignalHandlerRegistration, StreamChannel,
};
use std::collections::HashMap;
use std::time::{Duration, Instant};
use support::{
    assert_node_available, authenticate_wire, create_vm_wire_with_metadata, execute_wire,
    new_sidecar, open_session_wire, temp_dir, wire_request, wire_vm, write_fixture,
};

fn wait_for_process_output(
    sidecar: &mut secure_exec_sidecar::NativeSidecar<support::RecordingBridge>,
    connection_id: &str,
    session_id: &str,
    vm_id: &str,
    process_id: &str,
    expected: &str,
) {
    let ownership = wire_vm(connection_id, session_id, vm_id);
    let deadline = Instant::now() + Duration::from_secs(10);

    loop {
        assert!(
            Instant::now() < deadline,
            "timed out waiting for process output containing {expected:?}"
        );
        let event = sidecar
            .poll_event_wire_blocking(&ownership, Duration::from_millis(100))
            .expect("poll sidecar event");
        let Some(event) = event else {
            continue;
        };
        if let EventPayload::ProcessOutputEvent(output) = event.payload {
            if output.process_id == process_id
                && String::from_utf8_lossy(&output.chunk).contains(expected)
            {
                return;
            }
        }
    }
}

fn wait_for_process_status(
    sidecar: &mut secure_exec_sidecar::NativeSidecar<support::RecordingBridge>,
    connection_id: &str,
    session_id: &str,
    vm_id: &str,
    process_id: &str,
    expected: ProcessSnapshotStatus,
) {
    let ownership = wire_vm(connection_id, session_id, vm_id);
    let deadline = Instant::now() + Duration::from_secs(10);

    loop {
        let snapshot = sidecar
            .dispatch_wire_blocking(wire_request(
                1,
                ownership.clone(),
                RequestPayload::GetProcessSnapshotRequest,
            ))
            .expect("query process snapshot");
        match snapshot.response.payload {
            ResponsePayload::ProcessSnapshotResponse(snapshot) => {
                if snapshot
                    .processes
                    .iter()
                    .find(|entry| entry.process_id == process_id)
                    .is_some_and(|entry| entry.status == expected)
                {
                    return;
                }
            }
            other => panic!("unexpected process snapshot response: {other:?}"),
        }

        assert!(
            Instant::now() < deadline,
            "timed out waiting for process status {expected:?}"
        );
        let _ = sidecar
            .poll_event_wire_blocking(&ownership, Duration::from_millis(25))
            .expect("pump process events while waiting for status");
    }
}

fn embedded_runtime_signal_routes_sigterm_and_process_kill() {
    assert_node_available();

    let mut sidecar = new_sidecar("embedded-runtime-signal-routing");
    let cwd = temp_dir("embedded-runtime-signal-routing-cwd");
    let entry = cwd.join("signal-routing.mjs");

    write_fixture(
        &entry,
        [
            "let sigtermCount = 0;",
            "process.on('SIGHUP', () => {});",
            "process.on('SIGWINCH', () => {});",
            "process.on('SIGTERM', () => {",
            "  sigtermCount += 1;",
            "  console.log(`sigterm:${sigtermCount}`);",
            "  if (sigtermCount === 1) {",
            "    process.kill(process.pid, 'SIGTERM');",
            "    return;",
            "  }",
            "  process.exit(0);",
            "});",
            "console.log('signal-handlers-ready');",
            "setInterval(() => {}, 25);",
        ]
        .join("\n"),
    );

    let connection_id = authenticate_wire(&mut sidecar, "conn-embedded-runtime-signal-routing");
    let session_id = open_session_wire(&mut sidecar, 2, &connection_id);
    let (vm_id, _) = create_vm_wire_with_metadata(
        &mut sidecar,
        3,
        &connection_id,
        &session_id,
        GuestRuntimeKind::JavaScript,
        &cwd,
        HashMap::new(),
    );

    execute_wire(
        &mut sidecar,
        4,
        &connection_id,
        &session_id,
        &vm_id,
        "signal-routing",
        GuestRuntimeKind::JavaScript,
        &entry,
        Vec::new(),
    );

    wait_for_process_output(
        &mut sidecar,
        &connection_id,
        &session_id,
        &vm_id,
        "signal-routing",
        "signal-handlers-ready",
    );

    let ownership = wire_vm(&connection_id, &session_id, &vm_id);
    let registration_deadline = Instant::now() + Duration::from_secs(10);
    loop {
        let signal_state = sidecar
            .dispatch_wire_blocking(wire_request(
                5,
                ownership.clone(),
                RequestPayload::GetSignalStateRequest(GetSignalStateRequest {
                    process_id: String::from("signal-routing"),
                }),
            ))
            .expect("query signal state");
        let ready = match signal_state.response.payload {
            ResponsePayload::SignalStateResponse(snapshot) => {
                snapshot.handlers.get(&(libc::SIGTERM as u32))
                    == Some(&SignalHandlerRegistration {
                        action: SignalDispositionAction::User,
                        mask: vec![],
                        flags: 0,
                    })
            }
            other => panic!("unexpected signal state response: {other:?}"),
        };
        if ready {
            break;
        }
        let _ = sidecar
            .poll_event_wire_blocking(&ownership, Duration::from_millis(25))
            .expect("pump signal registration events");
        assert!(
            Instant::now() < registration_deadline,
            "timed out waiting for SIGTERM registration"
        );
    }

    sidecar
        .dispatch_wire_blocking(wire_request(
            6,
            ownership.clone(),
            RequestPayload::KillProcessRequest(KillProcessRequest {
                process_id: String::from("signal-routing"),
                signal: String::from("SIGTERM"),
            }),
        ))
        .expect("deliver SIGTERM");

    let event_deadline = Instant::now() + Duration::from_secs(10);
    let mut saw_first_sigterm = false;
    let mut saw_second_sigterm = false;
    let mut exit_code = None;

    while exit_code.is_none() {
        let event = sidecar
            .poll_event_wire_blocking(&ownership, Duration::from_millis(100))
            .expect("poll signal events");
        let Some(event) = event else {
            assert!(
                Instant::now() < event_deadline,
                "timed out waiting for SIGTERM delivery"
            );
            continue;
        };

        match event.payload {
            EventPayload::ProcessOutputEvent(output) if output.process_id == "signal-routing" => {
                let chunk = String::from_utf8_lossy(&output.chunk);
                saw_first_sigterm |= chunk.contains("sigterm:1");
                saw_second_sigterm |= chunk.contains("sigterm:2");
            }
            EventPayload::ProcessExitedEvent(exited) if exited.process_id == "signal-routing" => {
                exit_code = Some(exited.exit_code);
            }
            _ => {}
        }
    }

    assert!(saw_first_sigterm, "expected control-plane SIGTERM delivery");
    assert!(
        saw_second_sigterm,
        "expected guest process.kill(SIGTERM) delivery"
    );
    assert_eq!(exit_code, Some(0));
}

fn embedded_runtime_signal_stop_continue_updates_kernel_state_and_guest_handler() {
    assert_node_available();

    let mut sidecar = new_sidecar("embedded-runtime-signal-stop-cont");
    let cwd = temp_dir("embedded-runtime-signal-stop-cont-cwd");
    let entry = cwd.join("signal-stop-cont.mjs");

    write_fixture(
        &entry,
        [
            "let sigcontCount = 0;",
            "process.on('SIGCONT', () => {",
            "  sigcontCount += 1;",
            "  console.log(`sigcont:${sigcontCount}`);",
            "});",
            "console.log('ready');",
            "setInterval(() => {}, 25);",
        ]
        .join("\n"),
    );

    let connection_id = authenticate_wire(&mut sidecar, "conn-embedded-runtime-signal-stop-cont");
    let session_id = open_session_wire(&mut sidecar, 2, &connection_id);
    let (vm_id, _) = create_vm_wire_with_metadata(
        &mut sidecar,
        3,
        &connection_id,
        &session_id,
        GuestRuntimeKind::JavaScript,
        &cwd,
        HashMap::new(),
    );

    execute_wire(
        &mut sidecar,
        4,
        &connection_id,
        &session_id,
        &vm_id,
        "signal-stop-cont",
        GuestRuntimeKind::JavaScript,
        &entry,
        Vec::new(),
    );

    wait_for_process_output(
        &mut sidecar,
        &connection_id,
        &session_id,
        &vm_id,
        "signal-stop-cont",
        "ready",
    );

    let ownership = wire_vm(&connection_id, &session_id, &vm_id);
    sidecar
        .dispatch_wire_blocking(wire_request(
            5,
            ownership.clone(),
            RequestPayload::KillProcessRequest(KillProcessRequest {
                process_id: String::from("signal-stop-cont"),
                signal: String::from("SIGSTOP"),
            }),
        ))
        .expect("deliver SIGSTOP");
    wait_for_process_status(
        &mut sidecar,
        &connection_id,
        &session_id,
        &vm_id,
        "signal-stop-cont",
        ProcessSnapshotStatus::Stopped,
    );

    sidecar
        .dispatch_wire_blocking(wire_request(
            6,
            ownership.clone(),
            RequestPayload::KillProcessRequest(KillProcessRequest {
                process_id: String::from("signal-stop-cont"),
                signal: String::from("SIGCONT"),
            }),
        ))
        .expect("deliver SIGCONT");
    wait_for_process_status(
        &mut sidecar,
        &connection_id,
        &session_id,
        &vm_id,
        "signal-stop-cont",
        ProcessSnapshotStatus::Running,
    );
    wait_for_process_output(
        &mut sidecar,
        &connection_id,
        &session_id,
        &vm_id,
        "signal-stop-cont",
        "sigcont:1",
    );

    sidecar
        .dispatch_wire_blocking(wire_request(
            7,
            ownership,
            RequestPayload::KillProcessRequest(KillProcessRequest {
                process_id: String::from("signal-stop-cont"),
                signal: String::from("SIGTERM"),
            }),
        ))
        .expect("terminate stopped/continued process");
}

fn embedded_runtime_kill_process_rejects_invalid_signal_without_killing_process() {
    assert_node_available();

    let mut sidecar = new_sidecar("embedded-runtime-invalid-signal");
    let cwd = temp_dir("embedded-runtime-invalid-signal-cwd");
    let entry = cwd.join("invalid-signal.mjs");

    write_fixture(
        &entry,
        [
            "console.log('invalid-signal-ready');",
            "setInterval(() => {}, 25);",
        ]
        .join("\n"),
    );

    let connection_id = authenticate_wire(&mut sidecar, "conn-embedded-runtime-invalid-signal");
    let session_id = open_session_wire(&mut sidecar, 2, &connection_id);
    let (vm_id, _) = create_vm_wire_with_metadata(
        &mut sidecar,
        3,
        &connection_id,
        &session_id,
        GuestRuntimeKind::JavaScript,
        &cwd,
        HashMap::new(),
    );

    execute_wire(
        &mut sidecar,
        4,
        &connection_id,
        &session_id,
        &vm_id,
        "invalid-signal",
        GuestRuntimeKind::JavaScript,
        &entry,
        Vec::new(),
    );

    wait_for_process_output(
        &mut sidecar,
        &connection_id,
        &session_id,
        &vm_id,
        "invalid-signal",
        "invalid-signal-ready",
    );

    let ownership = wire_vm(&connection_id, &session_id, &vm_id);
    let invalid_signal = sidecar
        .dispatch_wire_blocking(wire_request(
            5,
            ownership.clone(),
            RequestPayload::KillProcessRequest(KillProcessRequest {
                process_id: String::from("invalid-signal"),
                signal: String::from("SIGBOGUS"),
            }),
        ))
        .expect("dispatch invalid signal");
    let ResponsePayload::RejectedResponse(response) = invalid_signal.response.payload else {
        panic!("unexpected invalid signal response");
    };
    assert_eq!(response.code, "invalid_state");
    assert!(
        response.message.contains("unsupported kill_process signal"),
        "unexpected invalid signal rejection: {}",
        response.message
    );

    wait_for_process_status(
        &mut sidecar,
        &connection_id,
        &session_id,
        &vm_id,
        "invalid-signal",
        ProcessSnapshotStatus::Running,
    );

    sidecar
        .dispatch_wire_blocking(wire_request(
            6,
            ownership,
            RequestPayload::KillProcessRequest(KillProcessRequest {
                process_id: String::from("invalid-signal"),
                signal: String::from("SIGTERM"),
            }),
        ))
        .expect("terminate invalid-signal process");
}

fn embedded_runtime_process_kill_signal_zero_checks_child_liveness() {
    assert_node_available();

    let mut sidecar = new_sidecar("embedded-runtime-process-kill-sig0");
    let cwd = temp_dir("embedded-runtime-process-kill-sig0-cwd");
    let entry = cwd.join("process-kill-sig0.mjs");

    write_fixture(
        &entry,
        [
            "const { spawn, spawnSync } = require('node:child_process');",
            "const live = spawn(process.execPath, ['-e', 'setTimeout(() => {}, 5000)'], { stdio: 'ignore' });",
            "console.log(`live:${process.kill(live.pid, 0)}`);",
            "live.kill('SIGTERM');",
            "const stale = spawnSync(process.execPath, ['-e', ''], { encoding: 'utf8' });",
            "if (typeof stale.pid !== 'number') {",
            "  throw new Error('spawnSync result did not include child pid');",
            "}",
            "let staleResult = 'alive';",
            "try {",
            "  process.kill(stale.pid, 0);",
            "} catch (error) {",
            "  staleResult = error && typeof error.code === 'string' ? error.code : 'error';",
            "}",
            "console.log(`stale:${staleResult}`);",
            "process.exit(staleResult === 'alive' ? 1 : 0);",
        ]
        .join("\n"),
    );

    let connection_id = authenticate_wire(&mut sidecar, "conn-embedded-runtime-process-kill-sig0");
    let session_id = open_session_wire(&mut sidecar, 2, &connection_id);
    let (vm_id, _) = create_vm_wire_with_metadata(
        &mut sidecar,
        3,
        &connection_id,
        &session_id,
        GuestRuntimeKind::JavaScript,
        &cwd,
        HashMap::new(),
    );

    execute_wire(
        &mut sidecar,
        4,
        &connection_id,
        &session_id,
        &vm_id,
        "process-kill-sig0",
        GuestRuntimeKind::JavaScript,
        &entry,
        Vec::new(),
    );

    let ownership = wire_vm(&connection_id, &session_id, &vm_id);
    let deadline = Instant::now() + Duration::from_secs(10);
    let mut saw_live = false;
    let mut saw_stale_esrch = false;
    let mut exit_code = None;

    while exit_code.is_none() || !saw_live || !saw_stale_esrch {
        let event = sidecar
            .poll_event_wire_blocking(&ownership, Duration::from_millis(100))
            .expect("poll process.kill signal-zero events");
        let Some(event) = event else {
            assert!(
                Instant::now() < deadline,
                "timed out waiting for process.kill signal-zero output"
            );
            continue;
        };

        match event.payload {
            EventPayload::ProcessOutputEvent(output)
                if output.process_id == "process-kill-sig0" =>
            {
                let chunk = String::from_utf8_lossy(&output.chunk);
                saw_live |= chunk.contains("live:true");
                saw_stale_esrch |= chunk.contains("stale:ESRCH");
            }
            EventPayload::ProcessExitedEvent(exited)
                if exited.process_id == "process-kill-sig0" =>
            {
                exit_code = Some(exited.exit_code);
            }
            _ => {}
        }

        assert!(
            Instant::now() < deadline,
            "timed out waiting for process.kill signal-zero completion"
        );
    }

    assert!(saw_live, "live child should be visible to signal 0");
    assert!(
        saw_stale_esrch,
        "stale child PID should throw ESRCH for signal 0"
    );
    assert_eq!(exit_code, Some(0));
}

fn embedded_runtime_process_group_kill_terminates_detached_tree() {
    assert_node_available();

    let mut sidecar = new_sidecar("embedded-runtime-process-group-kill");
    let cwd = temp_dir("embedded-runtime-process-group-kill-cwd");
    let parent_entry = cwd.join("group-parent.mjs");
    let child_entry = cwd.join("group-child.mjs");

    write_fixture(
        &child_entry,
        [
            "import { spawn } from 'node:child_process';",
            "const makeChild = () => spawn(",
            "  process.execPath,",
            "  ['-e', 'setTimeout(() => {}, 100000)'],",
            "  { stdio: ['ignore', 'ignore', 'ignore'] },",
            ");",
            "const first = makeChild();",
            "const second = makeChild();",
            "console.log(`group-ready:${first.pid}:${second.pid}`);",
            "setInterval(() => {}, 1000);",
        ]
        .join("\n"),
    );
    write_fixture(
        &parent_entry,
        [
            "import { spawn } from 'node:child_process';",
            "const child = spawn(process.execPath, ['./group-child.mjs'], {",
            "  detached: true,",
            "  stdio: ['ignore', 'pipe', 'pipe'],",
            "});",
            "let buffered = '';",
            "const grandchildPids = await new Promise((resolve, reject) => {",
            "  child.on('error', reject);",
            "  child.stdout.on('data', (chunk) => {",
            "    buffered += chunk.toString();",
            "    const match = buffered.match(/group-ready:(\\d+):(\\d+)/);",
            "    if (match) {",
            "      resolve([Number(match[1]), Number(match[2])]);",
            "    }",
            "  });",
            "});",
            "const closePromise = new Promise((resolve) => {",
            "  child.on('close', (code, signal) => resolve({ code, signal }));",
            "});",
            "const killResult = process.kill(-child.pid, 'SIGKILL');",
            "console.log('kill-returned:' + killResult);",
            "const closed = await closePromise;",
            "console.log('group-close:' + closed.code + ':' + closed.signal);",
            "const errorCode = (error) => {",
            "  if (error && typeof error.code === 'string' && error.syscall === 'kill') {",
            "    return error.code;",
            "  }",
            "  return 'missing-errno-error';",
            "};",
            "const probe = (pid) => {",
            "  try {",
            "    process.kill(pid, 0);",
            "    return 'alive';",
            "  } catch (error) {",
            "    return errorCode(error);",
            "  }",
            "};",
            "console.log('probe-child:' + probe(child.pid));",
            "console.log('probe-grandchild-a:' + probe(grandchildPids[0]));",
            "console.log('probe-grandchild-b:' + probe(grandchildPids[1]));",
            "let missingGroup;",
            "try {",
            "  process.kill(-999999, 'SIGKILL');",
            "  missingGroup = 'no-error';",
            "} catch (error) {",
            "  missingGroup = errorCode(error);",
            "}",
            "console.log('probe-missing-group:' + missingGroup);",
        ]
        .join("\n"),
    );

    let connection_id = authenticate_wire(&mut sidecar, "conn-embedded-runtime-process-group-kill");
    let session_id = open_session_wire(&mut sidecar, 2, &connection_id);
    let (vm_id, _) = create_vm_wire_with_metadata(
        &mut sidecar,
        3,
        &connection_id,
        &session_id,
        GuestRuntimeKind::JavaScript,
        &cwd,
        HashMap::new(),
    );

    execute_wire(
        &mut sidecar,
        4,
        &connection_id,
        &session_id,
        &vm_id,
        "group-kill-parent",
        GuestRuntimeKind::JavaScript,
        &parent_entry,
        Vec::new(),
    );

    let ownership = wire_vm(&connection_id, &session_id, &vm_id);
    let deadline = Instant::now() + Duration::from_secs(30);
    let mut stdout = String::new();
    let mut stderr = String::new();
    let mut exit_code = None;

    while exit_code.is_none() {
        let event = sidecar
            .poll_event_wire_blocking(&ownership, Duration::from_millis(100))
            .expect("poll process group kill events");
        let Some(event) = event else {
            assert!(
                Instant::now() < deadline,
                "timed out waiting for group kill completion\nstdout:\n{stdout}\nstderr:\n{stderr}"
            );
            continue;
        };

        match event.payload {
            EventPayload::ProcessOutputEvent(output)
                if output.process_id == "group-kill-parent" =>
            {
                let chunk = String::from_utf8_lossy(&output.chunk);
                match output.channel {
                    StreamChannel::Stdout => stdout.push_str(&chunk),
                    StreamChannel::Stderr => stderr.push_str(&chunk),
                }
            }
            EventPayload::ProcessExitedEvent(exited)
                if exited.process_id == "group-kill-parent" =>
            {
                exit_code = Some(exited.exit_code);
            }
            _ => {}
        }

        assert!(
            Instant::now() < deadline,
            "timed out waiting for group kill completion\nstdout:\n{stdout}\nstderr:\n{stderr}"
        );
    }

    assert_eq!(
        exit_code,
        Some(0),
        "group kill parent should exit cleanly\nstdout:\n{stdout}\nstderr:\n{stderr}"
    );
    assert!(
        stdout.contains("kill-returned:true"),
        "group kill should report success\nstdout:\n{stdout}\nstderr:\n{stderr}"
    );
    assert!(
        stdout.contains("group-close:"),
        "detached child should emit close after group kill\nstdout:\n{stdout}\nstderr:\n{stderr}"
    );
    assert!(
        stdout.contains("probe-child:ESRCH"),
        "killed group leader should probe as ESRCH\nstdout:\n{stdout}\nstderr:\n{stderr}"
    );
    assert!(
        stdout.contains("probe-grandchild-a:ESRCH"),
        "first grandchild should be killed with the group\nstdout:\n{stdout}\nstderr:\n{stderr}"
    );
    assert!(
        stdout.contains("probe-grandchild-b:ESRCH"),
        "second grandchild should be killed with the group\nstdout:\n{stdout}\nstderr:\n{stderr}"
    );
    assert!(
        stdout.contains("probe-missing-group:ESRCH"),
        "missing process group should raise ESRCH\nstdout:\n{stdout}\nstderr:\n{stderr}"
    );
}

fn embedded_runtime_signal_delivers_sigchld_on_child_exit() {
    assert_node_available();

    let mut sidecar = new_sidecar("embedded-runtime-signal-sigchld");
    let cwd = temp_dir("embedded-runtime-signal-sigchld-cwd");
    let parent_entry = cwd.join("parent.mjs");
    let child_entry = cwd.join("child.mjs");

    write_fixture(
        &child_entry,
        [
            "await new Promise((resolve) => setTimeout(resolve, 200));",
            "console.log('child-exit');",
        ]
        .join("\n"),
    );
    write_fixture(
        &parent_entry,
        [
            "import { spawn } from 'node:child_process';",
            "let sigchldCount = 0;",
            "process.on('SIGCHLD', () => {",
            "  sigchldCount += 1;",
            "  console.log(`sigchld:${sigchldCount}`);",
            "});",
            "console.log('sigchld-registered');",
            "const child = spawn('node', ['./child.mjs'], { stdio: ['ignore', 'ignore', 'ignore'] });",
            "await new Promise((resolve, reject) => {",
            "  child.on('error', reject);",
            "  child.on('close', (code) => {",
            "    if (code !== 0) {",
            "      reject(new Error(`child exit ${code}`));",
            "      return;",
            "    }",
            "    resolve();",
            "  });",
            "});",
            "const deadline = Date.now() + 2000;",
            "while (sigchldCount === 0 && Date.now() < deadline) {",
            "  await new Promise((resolve) => setTimeout(resolve, 10));",
            "}",
            "if (sigchldCount === 0) {",
            "  throw new Error('SIGCHLD was not delivered');",
            "}",
            "console.log(`sigchld-final:${sigchldCount}`);",
        ]
        .join("\n"),
    );

    let connection_id = authenticate_wire(&mut sidecar, "conn-embedded-runtime-signal-sigchld");
    let session_id = open_session_wire(&mut sidecar, 2, &connection_id);
    let allowed_builtins = serde_json::to_string(&[
        "assert",
        "buffer",
        "child_process",
        "console",
        "crypto",
        "events",
        "fs",
        "path",
        "querystring",
        "stream",
        "string_decoder",
        "timers",
        "url",
        "util",
        "zlib",
    ])
    .expect("serialize builtins");
    let (vm_id, _) = create_vm_wire_with_metadata(
        &mut sidecar,
        3,
        &connection_id,
        &session_id,
        GuestRuntimeKind::JavaScript,
        &cwd,
        HashMap::from([(
            String::from("env.AGENTOS_ALLOWED_NODE_BUILTINS"),
            allowed_builtins,
        )]),
    );

    execute_wire(
        &mut sidecar,
        4,
        &connection_id,
        &session_id,
        &vm_id,
        "sigchld-parent",
        GuestRuntimeKind::JavaScript,
        &parent_entry,
        Vec::new(),
    );

    let ownership = wire_vm(&connection_id, &session_id, &vm_id);
    let deadline = Instant::now() + Duration::from_secs(10);
    let mut signal_registered = false;
    let mut saw_registered_output = false;
    let mut saw_sigchld_output = false;
    let mut saw_final_output = false;
    let mut exit_code = None;

    while exit_code.is_none() || !signal_registered {
        let signal_state = sidecar
            .dispatch_wire_blocking(wire_request(
                5,
                ownership.clone(),
                RequestPayload::GetSignalStateRequest(GetSignalStateRequest {
                    process_id: String::from("sigchld-parent"),
                }),
            ))
            .expect("query SIGCHLD state");
        match signal_state.response.payload {
            ResponsePayload::SignalStateResponse(snapshot) => {
                if snapshot.handlers.get(&(libc::SIGCHLD as u32))
                    == Some(&SignalHandlerRegistration {
                        action: SignalDispositionAction::User,
                        mask: vec![],
                        flags: 0,
                    })
                {
                    signal_registered = true;
                }
            }
            other => panic!("unexpected signal state response: {other:?}"),
        }

        let event = sidecar
            .poll_event_wire_blocking(&ownership, Duration::from_millis(100))
            .expect("poll SIGCHLD process");
        if let Some(event) = event {
            match event.payload {
                EventPayload::ProcessOutputEvent(output)
                    if output.process_id == "sigchld-parent" =>
                {
                    let chunk = String::from_utf8_lossy(&output.chunk);
                    saw_registered_output |= chunk.contains("sigchld-registered");
                    saw_sigchld_output |= chunk.contains("sigchld:1");
                    saw_final_output |= chunk.contains("sigchld-final:1");
                }
                EventPayload::ProcessExitedEvent(exited)
                    if exited.process_id == "sigchld-parent" =>
                {
                    exit_code = Some(exited.exit_code);
                }
                _ => {}
            }
        }

        assert!(
            Instant::now() < deadline,
            "timed out waiting for SIGCHLD registration/output"
        );
    }

    assert!(signal_registered, "SIGCHLD should be registered");
    assert!(
        saw_registered_output,
        "parent should report SIGCHLD registration"
    );
    assert!(saw_sigchld_output, "parent should receive SIGCHLD output");
    assert!(saw_final_output, "parent should report final SIGCHLD count");
    assert_eq!(exit_code, Some(0));
}

#[test]
fn embedded_runtime_signal_suite() {
    embedded_runtime_signal_routes_sigterm_and_process_kill();
    embedded_runtime_signal_stop_continue_updates_kernel_state_and_guest_handler();
    embedded_runtime_kill_process_rejects_invalid_signal_without_killing_process();
    embedded_runtime_process_kill_signal_zero_checks_child_liveness();
    embedded_runtime_process_group_kill_terminates_detached_tree();
    embedded_runtime_signal_delivers_sigchld_on_child_exit();
}
