//! FC-50 — typed [`LifecycleReason`] coverage.
//!
//! Asserts that every typed variant round-trips to its documented
//! `snake_case` wire form, that the `Other(...)` escape hatch passes its
//! inner string through verbatim, and that the typed constructor produces a
//! payload identical to the string-based constructor when given equivalent
//! inputs (so JSON consumers cannot tell which surface a producer used).

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

fn fixture_spec() -> ExecutionCellSpec {
    ExecutionCellSpec {
        id: "fc50-typed-reason".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,
    }
}

/// Each typed variant, paired with its expected snake_case wire form.
fn typed_variants() -> Vec<(LifecycleReason, &'static str)> {
    vec![
        (LifecycleReason::Oom, "oom"),
        (LifecycleReason::TtlExceeded, "ttl_exceeded"),
        (LifecycleReason::VmmCrashed, "vmm_crashed"),
        (LifecycleReason::BootFailed, "boot_failed"),
        (LifecycleReason::SignalKilled, "signal_killed"),
        (LifecycleReason::InitCrashed, "init_crashed"),
        (
            LifecycleReason::KernelCannotMountRoot,
            "kernel_cannot_mount_root",
        ),
    ]
}

#[test]
fn every_typed_variant_serializes_to_snake_case_in_payload() {
    let spec = fixture_spec();
    for (variant, expected) in typed_variants() {
        // Display impl matches wire form.
        assert_eq!(
            format!("{}", variant),
            expected,
            "Display for {:?} should be {}",
            variant,
            expected
        );

        // Direct serde serialization matches wire form (string scalar).
        let direct = serde_json::to_value(&variant).unwrap();
        assert_eq!(
            direct,
            serde_json::Value::String(expected.to_string()),
            "serde for {:?} should be {}",
            variant,
            expected
        );

        // Round-trip through the typed constructor.
        let data = lifecycle_destroyed_data_v1_typed(
            &spec,
            "cell-typed",
            Some("run-typed"),
            LifecycleDestroyOutcome::Failed,
            Some(variant.clone()),
            None,
            None,
            None,
        )
        .unwrap();
        assert_eq!(
            data["reason"], expected,
            "payload reason for {:?} should be {}",
            variant, expected
        );
        assert_eq!(data["outcome"], "failed");
    }
}

#[test]
fn other_variant_round_trips_verbatim() {
    let spec = fixture_spec();
    let custom = "custom_thing";
    let data = lifecycle_destroyed_data_v1_typed(
        &spec,
        "cell-other",
        None,
        LifecycleDestroyOutcome::Failed,
        Some(LifecycleReason::Other(custom.to_string())),
        None,
        None,
        None,
    )
    .unwrap();
    assert_eq!(data["reason"], custom);

    // Display + serde also pass through verbatim.
    let r = LifecycleReason::Other(custom.to_string());
    assert_eq!(format!("{}", r), custom);
    assert_eq!(
        serde_json::to_value(&r).unwrap(),
        serde_json::Value::String(custom.to_string())
    );
}

#[test]
fn typed_and_string_constructors_produce_identical_payloads() {
    let spec = fixture_spec();
    // Pair each typed variant with the string the typed surface would emit.
    for (variant, wire) in typed_variants() {
        let typed = lifecycle_destroyed_data_v1_typed(
            &spec,
            "cell-equiv",
            Some("run-equiv"),
            LifecycleDestroyOutcome::Failed,
            Some(variant.clone()),
            Some(LifecycleTerminalState::Forced),
            None,
            None,
        )
        .unwrap();
        let stringy = lifecycle_destroyed_data_v1(
            &spec,
            "cell-equiv",
            Some("run-equiv"),
            LifecycleDestroyOutcome::Failed,
            Some(wire),
            Some(LifecycleTerminalState::Forced),
            None,
            None,
        )
        .unwrap();
        assert_eq!(
            typed, stringy,
            "typed vs string payloads must match for {:?}",
            variant
        );
    }
}

#[test]
fn typed_constructor_emits_evidence_bundle_and_residue_class_when_set() {
    let spec = fixture_spec();
    let data = lifecycle_destroyed_data_v1_typed(
        &spec,
        "cell-f5",
        Some("run-f5"),
        LifecycleDestroyOutcome::Failed,
        Some(LifecycleReason::Oom),
        Some(LifecycleTerminalState::Forced),
        None,
        None,
    )
    .unwrap();
    assert_eq!(data["reason"], "oom");
    // F5 evidenceBundleRef + residueClass omitted in this test variant — the
    // SubjectUrn / ResidueClass typed-API path is exercised by tests/forced_terminal_*
    // and tests/lifecycle_terminal_state_naming.rs.
}

#[test]
fn typed_constructor_omits_optional_extensions_when_unset() {
    // When the F5 fields are None, the typed constructor must produce the
    // same payload as the string-based constructor — no leakage of new keys.
    let spec = fixture_spec();
    let typed = lifecycle_destroyed_data_v1_typed(
        &spec,
        "cell-back-compat",
        None,
        LifecycleDestroyOutcome::Succeeded,
        None,
        None,
        None,
        None,
    )
    .unwrap();
    let stringy = lifecycle_destroyed_data_v1(
        &spec,
        "cell-back-compat",
        None,
        LifecycleDestroyOutcome::Succeeded,
        None,
        None,
        None,
        None,
    )
    .unwrap();
    assert_eq!(typed, stringy);
    let obj = typed.as_object().unwrap();
    assert!(!obj.contains_key("reason"));
    assert!(!obj.contains_key("terminalState"));
    assert!(!obj.contains_key("evidenceBundleRef"));
    assert!(!obj.contains_key("residueClass"));
}
