//! FC-23 — Forced terminal state never claims a workload exit code.
//!
//! Acceptance (`Plans/firecracker-release-readiness.md` line 70):
//!
//! > unit test asserts `complete-forced` events have `exit_code = null`
//! > (not `0`, not `137`); the field is reserved for vsock-authenticated
//! > exits only.
//!
//! The invariant is structural in `cellos-core`: the
//! `lifecycle_destroyed_data_v1` constructor takes no `exit_code` parameter
//! at all, so the destroyed event payload cannot leak a synthetic exit code
//! when the supervisor SIGKILL'd the VMM (`LifecycleTerminalState::Forced`).
//! These tests pin that contract so a future "convenience" parameter cannot
//! quietly regress FC-23 — a producer wishing to surface an authenticated
//! exit code MUST emit `cell.command.v1.completed` via
//! `command_completed_data_v1` instead, which is only reachable on the
//! in-VM-bridge clean-exit path.

use cellos_core::{
    lifecycle_destroyed_data_v1, ExecutionCellSpec, LifecycleDestroyOutcome,
    LifecycleTerminalState, Lifetime,
};

fn forced_destroyed_spec() -> ExecutionCellSpec {
    ExecutionCellSpec {
        id: "fc23-forced".into(),
        correlation: None,
        ingress: None,
        environment: None,
        placement: None,
        policy: None,
        identity: None,
        run: None,
        authority: Default::default(),
        lifetime: Lifetime { ttl_seconds: 30 },
        export: None,
        telemetry: None,
    }
}

/// `complete-forced` events carry no `exitCode` field.
///
/// The supervisor reaches `Forced` only when the in-VM bridge died before
/// delivering an authenticated exit code, so any code we *could* embed here
/// (e.g. `-1`, `137`) would be synthetic and must not leak into audit. The
/// constructor enforces this by simply not accepting an `exit_code`
/// argument; this test guards that the field is absent from the JSON for
/// the Forced case.
#[test]
fn forced_terminal_state_payload_has_no_exit_code_field() {
    let spec = forced_destroyed_spec();
    let data = lifecycle_destroyed_data_v1(
        &spec,
        "cell-fc23",
        Some("run-fc23"),
        LifecycleDestroyOutcome::Failed,
        Some("in-VM exit bridge: vsock closed before exit code"),
        Some(LifecycleTerminalState::Forced),
        None,
        None,
    )
    .expect("constructor builds destroyed payload");

    let obj = data.as_object().expect("destroyed data is a JSON object");
    assert_eq!(
        obj.get("terminalState").and_then(|v| v.as_str()),
        Some("forced")
    );
    assert!(
        !obj.contains_key("exitCode"),
        "FC-23: forced terminal state must not carry an exitCode field; got {data}"
    );
    // Defensive: the field is also not present under the snake_case spelling
    // some legacy producers used. Both must be absent.
    assert!(
        !obj.contains_key("exit_code"),
        "FC-23: snake_case exit_code must also be absent on forced events"
    );
}

/// Even when the *outcome* succeeded but teardown was Forced, no exit code
/// leaks. (Outcome = phase-error answer; terminalState = bridge-trust answer.
/// FC-23 keys on the bridge-trust answer.)
#[test]
fn forced_terminal_normalizes_exit_code_to_null_even_on_success_outcome() {
    let spec = forced_destroyed_spec();
    let data = lifecycle_destroyed_data_v1(
        &spec,
        "cell-fc23-ok",
        None,
        // Hypothetical: phase-success path that nevertheless ended in
        // SIGKILL teardown after the bridge died.
        LifecycleDestroyOutcome::Succeeded,
        None,
        Some(LifecycleTerminalState::Forced),
        None,
        None,
    )
    .expect("constructor builds destroyed payload");

    let obj = data.as_object().expect("destroyed data is a JSON object");
    assert_eq!(
        obj.get("terminalState").and_then(|v| v.as_str()),
        Some("forced")
    );
    assert!(
        !obj.contains_key("exitCode"),
        "FC-23: forced terminalState must never carry exitCode regardless of outcome"
    );
}

/// Belt-and-suspenders: the Clean path also has no exit code on this
/// payload — the authenticated code rides on the separate
/// `cell.command.v1.completed` event (see `command_completed_data_v1`).
/// This pins that the destroyed payload never grew an `exitCode` field on
/// any path, so a future regression for either Clean or Forced is caught.
#[test]
fn clean_terminal_state_payload_also_has_no_exit_code_field() {
    let spec = forced_destroyed_spec();
    let data = lifecycle_destroyed_data_v1(
        &spec,
        "cell-fc23-clean",
        None,
        LifecycleDestroyOutcome::Succeeded,
        None,
        Some(LifecycleTerminalState::Clean),
        None,
        None,
    )
    .expect("constructor builds destroyed payload");

    let obj = data.as_object().expect("destroyed data is a JSON object");
    assert_eq!(
        obj.get("terminalState").and_then(|v| v.as_str()),
        Some("clean")
    );
    assert!(
        !obj.contains_key("exitCode"),
        "destroyed payload must not carry exitCode on any path; \
         authenticated codes ride on cell.command.v1.completed"
    );
}
