//! FC-23 — Forced terminal state never claims a workload exit code.
//!
//! Per `Plans/firecracker-release-readiness.md` FC-23:
//!
//! > Forced terminal state never claims a workload exit code. Acceptance:
//! > unit test asserts `complete-forced` events have `exit_code = null` (not
//! > `0`, not `137`); the field is reserved for vsock-authenticated exits
//! > only.
//!
//! Audit invariant: when `LifecycleTerminalState::Forced` appears on a
//! `cell.lifecycle.v1.destroyed` event, the supervisor never received an
//! authenticated exit code over the vsock bridge — teardown reached the
//! SIGKILL fallback. Any `exitCode` value on that path would be synthetic
//! (e.g. `0` from a default, or `137 = 128 + SIGKILL` from the wait status)
//! and must not be confused with a real workload exit. The shipping
//! `lifecycle_destroyed_data_v1` constructor enforces this by *omitting* the
//! `exitCode` field entirely from the destroyed payload — the field is
//! carried only on `cell.command.v1.completed`, whose construction path
//! requires an `i32` argument and is fed exclusively by the vsock receiver.
//!
//! The test:
//!
//! 1. Constructs a `Forced` destroyed payload with the most adversarial
//!    `outcome` shape (Failed) and asserts:
//!    - `terminalState == "forced"`,
//!    - the JSON object MUST NOT contain a non-null `exitCode` field —
//!      either the key is absent, or it serializes as `null`.
//!    Re-runs the same assertion under `LifecycleDestroyOutcome::Succeeded`
//!    to cover the both-outcomes lattice (a forced kill of a workload that
//!    happened to "succeed" by some other measure must still not stamp an
//!    exit code).
//!
//! 2. Demonstrates the assertion is non-trivial by exercising the legitimate
//!    exit-code path — `command_completed_data_v1` — and asserting `exitCode`
//!    *is* present and *is* a JSON number. If `exitCode` were unreachable
//!    everywhere, step (1) would pass vacuously.

use cellos_core::{
    command_completed_data_v1, lifecycle_destroyed_data_v1, AuthorityBundle, ExecutionCellSpec,
    LifecycleDestroyOutcome, LifecycleTerminalState, Lifetime,
};
use serde_json::Value;

/// Build a minimal `ExecutionCellSpec` fixture suitable for event-constructor
/// tests. Mirrors the in-tree pattern used by
/// `events::tests::lifecycle_destroyed_terminal_state_*_serializes`.
fn fixture_spec(spec_id: &str) -> ExecutionCellSpec {
    ExecutionCellSpec {
        id: spec_id.into(),
        correlation: None,
        ingress: None,
        environment: None,
        placement: None,
        policy: None,
        identity: None,
        run: None,
        authority: AuthorityBundle::default(),
        lifetime: Lifetime { ttl_seconds: 60 },
        export: None,
        telemetry: None,
    }
}

/// Assert that `data` does NOT carry a non-null `exitCode` field.
///
/// "Non-null" is the wire-level contract: a `Value::Null` at the `exitCode`
/// key is treated the same as the field being absent (auditors keying on
/// `exitCode != null` get the same answer either way). What MUST NOT happen
/// is a synthetic numeric stamp (`0`, `-1`, `137`, …) leaking into the
/// destroyed payload on a forced-teardown path.
fn assert_no_authentic_exit_code(data: &Value, ctx: &str) {
    let obj = data
        .as_object()
        .unwrap_or_else(|| panic!("{ctx}: destroyed data must be a JSON object"));
    match obj.get("exitCode") {
        None => {}
        Some(Value::Null) => {}
        Some(other) => panic!(
            "{ctx}: destroyed payload carried a non-null exitCode ({other}); the \
             exitCode field is reserved for vsock-authenticated exits and MUST NOT \
             appear on a Forced terminal-state event (FC-23)"
        ),
    }
}

#[test]
fn forced_terminal_state_failed_outcome_omits_exit_code() {
    let spec = fixture_spec("fc23-forced-failed");
    let data = lifecycle_destroyed_data_v1(
        &spec,
        "cell-fc23-forced-failed",
        Some("run-fc23-forced-failed"),
        LifecycleDestroyOutcome::Failed,
        Some("in-VM exit bridge: vsock closed before exit code"),
        Some(LifecycleTerminalState::Forced),
        None,
        None,
    )
    .expect("destroyed payload must serialize");

    assert_eq!(
        data["terminalState"], "forced",
        "FC-23 fixture must actually exercise the Forced branch"
    );
    assert_no_authentic_exit_code(&data, "Forced + Failed");
}

#[test]
fn forced_terminal_state_succeeded_outcome_omits_exit_code() {
    // The Forced terminal-state guarantee is independent of the `outcome`
    // value: even if the run was classified Succeeded by some other rule,
    // the supervisor still never received an authenticated exit code, so
    // no exitCode may be stamped.
    let spec = fixture_spec("fc23-forced-succeeded");
    let data = lifecycle_destroyed_data_v1(
        &spec,
        "cell-fc23-forced-succeeded",
        Some("run-fc23-forced-succeeded"),
        LifecycleDestroyOutcome::Succeeded,
        None,
        Some(LifecycleTerminalState::Forced),
        None,
        None,
    )
    .expect("destroyed payload must serialize");

    assert_eq!(data["terminalState"], "forced");
    assert_no_authentic_exit_code(&data, "Forced + Succeeded");
}

#[test]
fn clean_terminal_state_with_command_completed_carries_numeric_exit_code() {
    // Non-triviality witness for FC-23: prove the `exitCode` field is
    // reachable in the system — the only legitimate path is
    // `command_completed_data_v1`, which is fed by the vsock-authenticated
    // exit-code bridge. The Clean destroyed event itself does not carry
    // exitCode (the schema places that field on cell.command.v1.completed),
    // so the pair {clean destroyed, command completed} together demonstrate
    // that exit codes are reachable when authenticated, even though the
    // forced-path assertions above pass.
    let spec = fixture_spec("fc23-clean");

    let destroyed = lifecycle_destroyed_data_v1(
        &spec,
        "cell-fc23-clean",
        Some("run-fc23-clean"),
        LifecycleDestroyOutcome::Succeeded,
        None,
        Some(LifecycleTerminalState::Clean),
        None,
        None,
    )
    .expect("destroyed payload must serialize");

    assert_eq!(destroyed["terminalState"], "clean");
    // The destroyed event still does not carry exitCode — the schema places
    // that field on cell.command.v1.completed. Document the invariant here
    // so the absence on Clean is not silently weakened in a future change.
    assert_no_authentic_exit_code(&destroyed, "Clean destroyed");

    // The vsock-authenticated exit code surfaces on cell.command.v1.completed.
    let completed = command_completed_data_v1(
        &spec,
        "cell-fc23-clean",
        Some("run-fc23-clean"),
        &[
            "/bin/sh".to_string(),
            "-c".to_string(),
            "exit 42".to_string(),
        ],
        42,
        128,
        None,
    )
    .expect("command-completed payload must serialize");

    let exit_code = completed
        .get("exitCode")
        .expect("Clean / vsock-authenticated path must carry exitCode");
    assert!(
        exit_code.is_number(),
        "exitCode must serialize as a JSON number on the authenticated path; \
         got {exit_code}"
    );
    assert_eq!(
        exit_code.as_i64(),
        Some(42),
        "command_completed_data_v1 must round-trip the supplied exit code so \
         the Forced-path no-exitCode assertion is non-trivial"
    );
}
