//! Regression for FC-21: `spec.run.limits.gracefulShutdownSeconds` is the
//! per-spec override the host backend honours when tearing down a VM.
//!
//! # What this test guards
//!
//! The firecracker host backend uses a const fallback
//! (`GRACEFUL_SHUTDOWN_TIMEOUT`) when waiting for a VM to exit before
//! SIGKILL. FC-21 makes that window per-spec so:
//!
//! 1. Cells with shutdown hooks (flushing a WAL, closing connections) can
//!    raise the window without rebuilding the host.
//! 2. Latency-sensitive cells can drop it to fail fast.
//!
//! These tests live in `cellos-core` (not `cellos-host-firecracker`) because
//! the contract surface — the field on `RunLimits`, its serde shape, the
//! `Option<u64>` semantics — is what every backend must agree on. The
//! firecracker crate wires its const fallback through; other backends will
//! gain their own resolvers as they grow VM teardown paths.

use std::time::Duration;

use cellos_core::{ExecutionCellSpec, RunLimits, RunSpec};

/// Mirror of the firecracker backend's compiled-in fallback. Kept here as
/// the contract test's expected default; the firecracker crate documents
/// the same value at `GRACEFUL_SHUTDOWN_TIMEOUT`. If the host backend ever
/// changes its fallback, that crate's own tests will catch it — this one
/// guards the contract layer.
const HOST_FALLBACK_GRACEFUL_TIMEOUT: Duration = Duration::from_secs(5);

/// Resolver that mirrors the host backend's logic on the contract side: read
/// `spec.run.limits.graceful_shutdown_seconds`, fall back to the host's
/// compiled-in default. Lives in the test so we can exercise the contract
/// without depending on the host crate (which is Linux-only).
fn resolve_timeout(spec: &ExecutionCellSpec) -> Duration {
    spec.run
        .as_ref()
        .and_then(|r| r.limits.as_ref())
        .and_then(|l| l.graceful_shutdown_seconds)
        .map(Duration::from_secs)
        .unwrap_or(HOST_FALLBACK_GRACEFUL_TIMEOUT)
}

#[test]
fn explicit_value_yields_duration_from_secs() {
    let json = r#"{
        "id": "cell-fc21-explicit",
        "run": {
            "argv": ["/usr/bin/true"],
            "limits": {
                "gracefulShutdownSeconds": 10
            }
        },
        "authority": {},
        "lifetime": {"ttlSeconds": 60}
    }"#;
    let spec: ExecutionCellSpec = serde_json::from_str(json).expect("valid spec");
    assert_eq!(
        resolve_timeout(&spec),
        Duration::from_secs(10),
        "FC-21: gracefulShutdownSeconds=10 must produce Duration::from_secs(10)"
    );
}

#[test]
fn omitting_field_falls_back_to_host_default() {
    let json = r#"{
        "id": "cell-fc21-default",
        "run": {
            "argv": ["/usr/bin/true"]
        },
        "authority": {},
        "lifetime": {"ttlSeconds": 60}
    }"#;
    let spec: ExecutionCellSpec = serde_json::from_str(json).expect("valid spec");
    assert_eq!(
        resolve_timeout(&spec),
        HOST_FALLBACK_GRACEFUL_TIMEOUT,
        "FC-21: omitting limits.gracefulShutdownSeconds must fall back to the host const"
    );
}

#[test]
fn limits_present_but_field_absent_also_falls_back() {
    // Cells that pin memory but not graceful shutdown still get the const
    // default — the field is optional inside an otherwise-set RunLimits.
    let json = r#"{
        "id": "cell-fc21-partial-limits",
        "run": {
            "argv": ["/usr/bin/true"],
            "limits": {
                "memoryMaxBytes": 134217728
            }
        },
        "authority": {},
        "lifetime": {"ttlSeconds": 60}
    }"#;
    let spec: ExecutionCellSpec = serde_json::from_str(json).expect("valid spec");
    assert_eq!(resolve_timeout(&spec), HOST_FALLBACK_GRACEFUL_TIMEOUT);
}

#[test]
fn field_round_trips_through_serde() {
    let limits = RunLimits {
        memory_max_bytes: None,
        cpu_max: None,
        graceful_shutdown_seconds: Some(42),
    };
    let run = RunSpec {
        argv: vec!["/usr/bin/true".into()],
        working_directory: None,
        timeout_ms: None,
        limits: Some(limits),
        secret_delivery: Default::default(),
    };
    let json = serde_json::to_string(&run).expect("serialize");
    assert!(
        json.contains("gracefulShutdownSeconds"),
        "FC-21: field must serialize as camelCase"
    );
    let back: RunSpec = serde_json::from_str(&json).expect("deserialize");
    assert_eq!(
        back.limits.unwrap().graceful_shutdown_seconds,
        Some(42),
        "FC-21: round-trip preserves gracefulShutdownSeconds"
    );
}
