//! VM-backed end-to-end test for the
//! `Op::Spawn(SpawnPlacement::Cgroup(""))` empty-string bail in
//! `src/scenario/ops/dispatch.rs::apply_spawn` (the
//! `SpawnPlacement::Cgroup` arm).
//!
//! No unit test drives this Op::Spawn bail; this e2e is its
//! sole pin. The analogous empty-cgroup bail on
//! `Op::CaptureCgroupProcs` is unit-pinned with mocked
//! `CgroupOps` (src/scenario/ops/tests.rs:
//! `op_capture_cgroup_procs_empty_cgroup_bails_before_read`) as
//! a mirror. This e2e boots a real VM and proves the bail fires
//! in the production scenario-engine path — before
//! `WorkloadHandle::spawn` burns clone(2) syscalls, before any
//! cgroup-fs write. A regression that moved the check after
//! spawn would fail this test because the bail returns from the
//! scenario engine with a specific diagnostic string AND a
//! clean teardown — workers never reach the cgroup-fs layer.
//!
//! Expected behavior:
//! - `execute_scenario` returns `Ok(AssertResult::fail(...))` —
//!   `run_scenario` converts a step bail into a failing result, not
//!   an `Err` (src/scenario/ops/mod.rs)
//! - A failure detail contains
//!   `"Op::Spawn(SpawnPlacement::Cgroup): cgroup name is empty"`
//!   plus the actionable redirect to `SpawnPlacement::runner_cgroup()`
//! - No cgroup-fs side effects (no cgroup dir created for the
//!   empty name — the scenario never reaches the cgroup-fs layer)
//!
//! The test runs under `auto_repro = false` because the bail
//! IS the expected outcome — re-running in an auto-repro VM
//! to capture the "failure" would just bail again with the
//! same message, wasting a kernel boot.

use anyhow::{Context, Result};
use ktstr::assert::{AssertDetail, AssertResult, DetailKind};
use ktstr::ktstr_test;
use ktstr::prelude::{SpawnPlacement, WorkSpec};
use ktstr::scenario::Ctx;
use ktstr::scenario::ops::{HoldSpec, Op, Step, execute_scenario};
use std::time::Duration;

#[ktstr_test(
    workload_root_cgroup = "/ktstr-spawn-empty-bail-e2e",
    llcs = 1,
    cores = 2,
    threads = 1,
    memory_mib = 256,
    duration_s = 2,
    watchdog_timeout_s = 20,
    auto_repro = false
)]
fn op_spawn_cgroup_empty_string_bails_with_actionable_diagnostic(
    ctx: &Ctx,
) -> Result<AssertResult> {
    let backdrop = ktstr::prelude::Backdrop::default();
    let steps = vec![Step::new(
        vec![Op::spawn(
            SpawnPlacement::cgroup(""),
            WorkSpec::default().workers(1),
        )],
        HoldSpec::fixed(Duration::ZERO),
    )];
    // run_scenario converts a step bail into Ok(AssertResult::fail(...))
    // carrying the bail message as a failure detail (see
    // src/scenario/ops/mod.rs) — it does NOT return Err. So a missing
    // bail surfaces as a passing result, and the diagnostic lives in
    // the failure details, not an error chain.
    let result = execute_scenario(ctx, backdrop, steps).context(
        "execute_scenario returned Err — unexpected; a step bail is \
         converted to Ok(AssertResult::fail)",
    )?;
    if result.is_pass() {
        return Ok(AssertResult::fail(AssertDetail::new(
            DetailKind::Other,
            "execute_scenario produced a passing result — empty-string \
             SpawnPlacement::Cgroup MUST bail at apply_ops entry. \
             Without the bail, the workers would have spawned, the \
             cgroup-fs move_tasks would have failed with a less \
             actionable kernel ENOENT, and the spawn-before-validate \
             hazard the diagnostic prevents would be live."
                .to_string(),
        )));
    }
    // Pin the diagnostic shape so a regression that weakened the bail
    // to a bare "cgroup '': not found" (kernel ENOENT class) surfaces
    // here. The bail message arrives as a failure detail.
    let combined = result
        .failure_details()
        .map(|d| d.message.as_str())
        .collect::<Vec<_>>()
        .join(" | ");
    if !combined.contains("Op::Spawn(SpawnPlacement::Cgroup)") {
        return Ok(AssertResult::fail(AssertDetail::new(
            DetailKind::Other,
            format!(
                "expected the failure detail to name \
                 `Op::Spawn(SpawnPlacement::Cgroup)`; got: {combined}"
            ),
        )));
    }
    if !combined.contains("cgroup name is empty") {
        return Ok(AssertResult::fail(AssertDetail::new(
            DetailKind::Other,
            format!(
                "expected the failure detail to cite `cgroup name is \
                 empty`; got: {combined}"
            ),
        )));
    }
    if !combined.contains("SpawnPlacement::runner_cgroup") {
        return Ok(AssertResult::fail(AssertDetail::new(
            DetailKind::Other,
            format!(
                "expected the failure detail to name the runner-cgroup \
                 recovery path; got: {combined}"
            ),
        )));
    }
    Ok(AssertResult::pass())
}
