#![allow(unsafe_code)]

use karapace_core::{Engine, StoreLock};
use karapace_store::{EnvState, StoreLayout};
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::sync::{Arc, Barrier};
use std::thread;

/// Skip test if running as root — root bypasses filesystem permission checks,
/// so read-only directory tests are meaningless in containers running as uid 0.
fn skip_if_root() -> bool {
    #[allow(unsafe_code)]
    unsafe {
        libc::getuid() == 0
    }
}

fn write_manifest(dir: &Path, content: &str) -> std::path::PathBuf {
    let path = dir.join("karapace.toml");
    fs::write(&path, content).unwrap();
    path
}

fn mock_manifest(packages: &[&str]) -> String {
    format!(
        r#"
manifest_version = 1
[base]
image = "rolling"
[system]
packages = [{}]
[runtime]
backend = "mock"
"#,
        packages
            .iter()
            .map(|p| format!("\"{p}\""))
            .collect::<Vec<_>>()
            .join(", ")
    )
}

// §11.2: Build → Destroy → Rebuild equality
#[test]
fn build_destroy_rebuild_produces_identical_env_id() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git", "clang"]));

    let r1 = engine.build(&manifest).unwrap();
    let id1 = r1.identity.env_id.clone();

    engine.destroy(&id1).unwrap();

    let meta_store = karapace_store::MetadataStore::new(StoreLayout::new(store.path()));
    meta_store.remove(&id1).unwrap();

    let r2 = engine.build(&manifest).unwrap();
    assert_eq!(
        id1, r2.identity.env_id,
        "rebuild after destroy must produce identical env_id"
    );
}

// §11.2: Multi-environment isolation
#[test]
fn multi_environment_isolation() {
    let store = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let p1 = tempfile::tempdir().unwrap();
    let p2 = tempfile::tempdir().unwrap();

    let m1 = write_manifest(p1.path(), &mock_manifest(&["git"]));
    let m2 = write_manifest(p2.path(), &mock_manifest(&["cmake"]));

    let r1 = engine.build(&m1).unwrap();
    let r2 = engine.build(&m2).unwrap();

    assert_ne!(r1.identity.env_id, r2.identity.env_id);

    let envs = engine.list().unwrap();
    assert_eq!(envs.len(), 2);

    engine.destroy(&r1.identity.env_id).unwrap();
    let envs = engine.list().unwrap();
    assert_eq!(envs.len(), 1);

    let meta = engine.inspect(&r2.identity.env_id).unwrap();
    assert_eq!(meta.state, EnvState::Built);
}

// §11.2: GC safety under load
#[test]
fn gc_safety_with_multiple_environments() {
    let store = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let mut env_ids = Vec::new();
    for i in 0..10 {
        let p = tempfile::tempdir().unwrap();
        let pkgs: Vec<String> = (0..=i).map(|j| format!("pkg{j}")).collect();
        let pkg_refs: Vec<&str> = pkgs.iter().map(String::as_str).collect();
        let m = write_manifest(p.path(), &mock_manifest(&pkg_refs));
        let r = engine.build(&m).unwrap();
        env_ids.push(r.identity.env_id);
    }

    assert_eq!(engine.list().unwrap().len(), 10);

    for id in &env_ids[..5] {
        engine.destroy(id).unwrap();
    }

    let layout = StoreLayout::new(store.path());
    let lock = StoreLock::acquire(&layout.lock_file()).unwrap();
    let _report = engine.gc(&lock, false).unwrap();

    for id in &env_ids[5..] {
        let meta = engine.inspect(id).unwrap();
        assert_eq!(meta.state, EnvState::Built, "active env should survive GC");
    }
}

// §11.2: Concurrent build safety
#[test]
fn concurrent_builds_are_safe() {
    let store = tempfile::tempdir().unwrap();
    let store_path = store.path().to_path_buf();

    let barrier = Arc::new(Barrier::new(4));
    let mut handles = Vec::new();

    for i in 0..4 {
        let sp = store_path.clone();
        let b = Arc::clone(&barrier);
        handles.push(thread::spawn(move || {
            let engine = Engine::new(&sp);
            let p = tempfile::tempdir().unwrap();
            let pkgs = [format!("thread-pkg-{i}")];
            let pkg_refs: Vec<&str> = pkgs.iter().map(String::as_str).collect();
            let m = write_manifest(p.path(), &mock_manifest(&pkg_refs));

            b.wait();
            engine.build(&m).unwrap()
        }));
    }

    let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();

    let engine = Engine::new(&store_path);
    let envs = engine.list().unwrap();
    assert_eq!(envs.len(), 4, "all 4 concurrent builds should succeed");

    let ids: std::collections::HashSet<_> =
        results.iter().map(|r| r.identity.env_id.clone()).collect();
    assert_eq!(ids.len(), 4, "all env IDs should be unique");
}

// §11.3: Reproducibility test
#[test]
fn same_manifest_produces_identical_env_id_across_engines() {
    let manifest_content = mock_manifest(&["git", "clang", "cmake"]);

    let store1 = tempfile::tempdir().unwrap();
    let store2 = tempfile::tempdir().unwrap();
    let p1 = tempfile::tempdir().unwrap();
    let p2 = tempfile::tempdir().unwrap();

    let m1 = write_manifest(p1.path(), &manifest_content);
    let m2 = write_manifest(p2.path(), &manifest_content);

    let engine1 = Engine::new(store1.path());
    let engine2 = Engine::new(store2.path());

    let r1 = engine1.build(&m1).unwrap();
    let r2 = engine2.build(&m2).unwrap();

    assert_eq!(
        r1.identity.env_id, r2.identity.env_id,
        "same manifest on different stores must produce identical env_id"
    );
    assert_eq!(r1.lock_file.env_id, r2.lock_file.env_id);
    assert_eq!(
        r1.lock_file.base_image_digest,
        r2.lock_file.base_image_digest
    );
}

// §16 Core invariant: host system remains untouched
#[test]
fn host_system_untouched_after_lifecycle() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let home_before: Vec<_> = fs::read_dir(std::env::var("HOME").unwrap_or("/tmp".to_owned()))
        .unwrap()
        .filter_map(Result::ok)
        .map(|e| e.path())
        .collect();

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();
    engine.freeze(&r.identity.env_id).unwrap();
    engine.destroy(&r.identity.env_id).unwrap();

    let home_after: Vec<_> = fs::read_dir(std::env::var("HOME").unwrap_or("/tmp".to_owned()))
        .unwrap()
        .filter_map(Result::ok)
        .map(|e| e.path())
        .collect();

    assert_eq!(home_before, home_after, "host HOME must not be modified");
}

// §16 Core invariant: no silent drift
#[test]
fn drift_always_observable() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();

    // Clear the upper dir to simulate a clean post-build state.
    // The mock backend creates files there during build; a real backend's
    // overlay upper would also have content, but drift is measured from
    // the post-build snapshot baseline.
    let upper = engine.store_layout().upper_dir(&r.identity.env_id);
    if upper.exists() {
        fs::remove_dir_all(&upper).unwrap();
        fs::create_dir_all(&upper).unwrap();
    }

    let report = karapace_core::diff_overlay(engine.store_layout(), &r.identity.env_id).unwrap();
    assert!(!report.has_drift, "fresh build should have no drift");

    fs::write(upper.join("injected.txt"), "mutation").unwrap();

    let report = karapace_core::diff_overlay(engine.store_layout(), &r.identity.env_id).unwrap();
    assert!(report.has_drift, "mutation must be detected");
    assert!(report.added.contains(&"injected.txt".to_owned()));
}

// §15: Crash safety — interrupted write leaves no partial objects
#[test]
fn store_integrity_after_partial_operations() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let obj_store = karapace_store::ObjectStore::new(layout.clone());
    obj_store.put(b"valid1").unwrap();
    obj_store.put(b"valid2").unwrap();

    let stray_path = layout.objects_dir().join("not_a_real_hash");
    fs::write(&stray_path, b"corrupted data").unwrap();

    let report = karapace_store::verify_store_integrity(&layout).unwrap();
    assert_eq!(report.failed.len(), 1, "corrupted object must be detected");
    assert_eq!(report.passed, 2);
}

// §5.2: commit persists overlay into store
#[test]
fn commit_persists_overlay_drift() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();

    // Clear mock backend artifacts from upper, then add user files
    let upper = engine.store_layout().upper_dir(&r.identity.env_id);
    if upper.exists() {
        fs::remove_dir_all(&upper).unwrap();
    }
    fs::create_dir_all(&upper).unwrap();
    fs::write(upper.join("user_file.txt"), "user data").unwrap();
    fs::create_dir_all(upper.join("subdir")).unwrap();
    fs::write(upper.join("subdir").join("nested.txt"), "nested data").unwrap();

    let snapshot_hash = engine.commit(&r.identity.env_id).unwrap();
    assert!(
        !snapshot_hash.is_empty(),
        "commit should return a snapshot hash"
    );

    // Look up the snapshot layer to get the tar_hash
    let layer_store = karapace_store::LayerStore::new(StoreLayout::new(store.path()));
    let layer = layer_store.get(&snapshot_hash).unwrap();
    assert_eq!(layer.kind, karapace_store::LayerKind::Snapshot);
    assert!(!layer.tar_hash.is_empty());

    let obj_store = karapace_store::ObjectStore::new(StoreLayout::new(store.path()));
    assert!(
        obj_store.exists(&layer.tar_hash),
        "committed tar object must exist in store"
    );

    // Verify the committed tar can be unpacked and contains the user files
    let tar_data = obj_store.get(&layer.tar_hash).unwrap();
    let unpack_dir = tempfile::tempdir().unwrap();
    karapace_store::unpack_layer(&tar_data, unpack_dir.path()).unwrap();
    assert!(unpack_dir.path().join("user_file.txt").exists());
    assert!(unpack_dir.path().join("subdir").join("nested.txt").exists());
}

// INV-S1: build → modify → commit → restore → diff = zero drift
#[test]
fn restore_roundtrip() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();

    // Clear mock artifacts and add user files
    let upper = engine.store_layout().upper_dir(&r.identity.env_id);
    if upper.exists() {
        fs::remove_dir_all(&upper).unwrap();
    }
    fs::create_dir_all(&upper).unwrap();
    fs::write(upper.join("user_file.txt"), "snapshot content").unwrap();
    fs::create_dir_all(upper.join("data")).unwrap();
    fs::write(upper.join("data").join("config.json"), r#"{"key":"val"}"#).unwrap();

    // Commit the snapshot
    let snapshot_hash = engine.commit(&r.identity.env_id).unwrap();

    // Mutate the upper dir (simulating user modifications after snapshot)
    fs::write(upper.join("user_file.txt"), "MODIFIED AFTER SNAPSHOT").unwrap();
    fs::write(upper.join("extra.txt"), "extra file").unwrap();

    // Restore from the snapshot
    engine.restore(&r.identity.env_id, &snapshot_hash).unwrap();

    // Verify the upper dir matches the snapshot content exactly
    assert_eq!(
        fs::read_to_string(upper.join("user_file.txt")).unwrap(),
        "snapshot content"
    );
    assert_eq!(
        fs::read_to_string(upper.join("data").join("config.json")).unwrap(),
        r#"{"key":"val"}"#
    );
    // The extra file should NOT exist after restore
    assert!(
        !upper.join("extra.txt").exists(),
        "extra file must be gone after restore"
    );
}

// INV-S2: restore nonexistent snapshot → error
#[test]
fn restore_nonexistent_snapshot_fails() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();

    let result = engine.restore(&r.identity.env_id, "nonexistent_hash");
    assert!(result.is_err());
}

// list_snapshots returns committed snapshots
#[test]
fn list_snapshots_after_commit() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();

    // No snapshots initially
    let snaps = engine.list_snapshots(&r.identity.env_id).unwrap();
    assert!(snaps.is_empty());

    // Commit a snapshot
    let _hash = engine.commit(&r.identity.env_id).unwrap();

    let snaps = engine.list_snapshots(&r.identity.env_id).unwrap();
    assert_eq!(snaps.len(), 1);
    assert_eq!(snaps[0].kind, karapace_store::LayerKind::Snapshot);
    assert!(!snaps[0].tar_hash.is_empty());
}

// §12: GC scales to at least 100 environments
#[test]
fn gc_scales_to_100_environments() {
    let store = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    for i in 0..100 {
        let p = tempfile::tempdir().unwrap();
        let pkgs: Vec<String> = (0..=i).map(|j| format!("scale-pkg-{j}")).collect();
        let pkg_refs: Vec<&str> = pkgs.iter().map(String::as_str).collect();
        let m = write_manifest(p.path(), &mock_manifest(&pkg_refs));
        engine.build(&m).unwrap();
    }

    assert_eq!(engine.list().unwrap().len(), 100);

    let layout = StoreLayout::new(store.path());
    let lock = StoreLock::acquire(&layout.lock_file()).unwrap();
    let start = std::time::Instant::now();
    let report = engine.gc(&lock, true).unwrap();
    let elapsed = start.elapsed();

    assert!(
        elapsed.as_secs() < 10,
        "GC dry-run on 100 envs took {elapsed:?}, must be under 10s"
    );
    assert_eq!(report.orphaned_envs.len(), 0, "all envs have ref_count > 0");
}

// §12: Warm cache build under 10 seconds
#[test]
fn warm_build_under_10_seconds() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git", "clang", "cmake"]));

    // Cold build
    engine.build(&manifest).unwrap();

    // Warm rebuild (store already populated)
    let start = std::time::Instant::now();
    engine.rebuild(&manifest).unwrap();
    let elapsed = start.elapsed();

    assert!(
        elapsed.as_secs() < 10,
        "warm rebuild took {elapsed:?}, must be under 10s"
    );
}

// §3.1: Build fails deterministically — different manifests get different IDs
#[test]
fn different_manifests_different_ids() {
    let store = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let p1 = tempfile::tempdir().unwrap();
    let p2 = tempfile::tempdir().unwrap();
    let m1 = write_manifest(p1.path(), &mock_manifest(&["git"]));
    let m2 = write_manifest(p2.path(), &mock_manifest(&["git", "cmake"]));

    let r1 = engine.build(&m1).unwrap();
    let r2 = engine.build(&m2).unwrap();
    assert_ne!(r1.identity.env_id, r2.identity.env_id);
}

// §5.1: Whiteout files detected as removed
#[test]
fn whiteout_files_detected_as_removals() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();

    // Clear mock backend artifacts, then add only the whiteout
    let upper = engine.store_layout().upper_dir(&r.identity.env_id);
    if upper.exists() {
        fs::remove_dir_all(&upper).unwrap();
    }
    fs::create_dir_all(&upper).unwrap();
    // Overlayfs whiteout: .wh.filename means filename was deleted
    fs::write(upper.join(".wh.deleted_config"), "").unwrap();

    let report = karapace_core::diff_overlay(engine.store_layout(), &r.identity.env_id).unwrap();
    assert!(report.has_drift);
    assert!(report.removed.contains(&"deleted_config".to_owned()));
    assert!(report.added.is_empty());
}

// §5.1: Modified files classified correctly when lower layer exists
#[test]
fn modified_files_detected_against_lower_layer() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();

    // Clear mock backend artifacts from upper
    let upper = engine.store_layout().upper_dir(&r.identity.env_id);
    if upper.exists() {
        fs::remove_dir_all(&upper).unwrap();
    }
    fs::create_dir_all(&upper).unwrap();

    // Simulate a lower layer file
    let env_dir = engine.store_layout().env_path(&r.identity.env_id);
    let lower = env_dir.join("lower");
    fs::create_dir_all(&lower).unwrap();
    fs::write(lower.join("config.txt"), "original").unwrap();

    // Same file in upper = modification
    fs::write(upper.join("config.txt"), "modified content").unwrap();
    // New file = added
    fs::write(upper.join("new_script.sh"), "#!/bin/sh").unwrap();

    let report = karapace_core::diff_overlay(engine.store_layout(), &r.identity.env_id).unwrap();
    assert!(report.has_drift);
    assert_eq!(report.modified, vec!["config.txt"]);
    assert_eq!(report.added, vec!["new_script.sh"]);
    assert!(report.removed.is_empty());
}

// §6.1: exec works via mock backend
#[test]
fn exec_via_mock_backend() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();

    let result = engine.exec(&r.identity.env_id, &["echo".to_owned(), "hello".to_owned()]);
    assert!(result.is_ok());
}

// §3.2: Lock file integrity verifiable after build
#[test]
fn lock_file_integrity_after_build() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git", "clang"]));
    let r = engine.build(&manifest).unwrap();

    // Lock file was written
    let lock_path = project.path().join("karapace.lock");
    assert!(lock_path.exists());

    // Verify integrity
    assert!(r.lock_file.verify_integrity().is_ok());

    // Verify manifest intent
    let parsed = karapace_schema::parse_manifest_file(&manifest).unwrap();
    let normalized = parsed.normalize().unwrap();
    assert!(r.lock_file.verify_manifest_intent(&normalized).is_ok());
}

// §5.2: Frozen environment cannot be entered
#[test]
fn frozen_env_cannot_be_entered() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();
    engine.freeze(&r.identity.env_id).unwrap();

    let result = engine.enter(&r.identity.env_id);
    assert!(result.is_err(), "entering a frozen env must fail");
}

// §15: Crash injection — partial write must not corrupt store
#[test]
fn crash_injection_partial_write_detected() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let obj_store = karapace_store::ObjectStore::new(layout.clone());

    // Write valid objects
    let h1 = obj_store.put(b"valid-object-1").unwrap();
    let h2 = obj_store.put(b"valid-object-2").unwrap();

    // Simulate a crash during write: create a file with a valid hash name
    // but corrupted content (as if the process died mid-write and the
    // atomic rename somehow partially completed — or manual tampering)
    let fake_hash = blake3::hash(b"original-content").to_hex().to_string();
    let fake_path = layout.objects_dir().join(&fake_hash);
    fs::write(&fake_path, b"truncated-garbage").unwrap();

    // Store integrity check must detect all corruption
    let report = karapace_store::verify_store_integrity(&layout).unwrap();
    assert_eq!(report.checked, 3);
    assert_eq!(report.passed, 2, "two valid objects should pass");
    assert_eq!(
        report.failed.len(),
        1,
        "corrupted object should be detected"
    );
    assert_eq!(report.failed[0].hash, fake_hash);

    // Valid objects must still be readable
    assert!(obj_store.get(&h1).is_ok());
    assert!(obj_store.get(&h2).is_ok());

    // Corrupted object must fail on read
    assert!(obj_store.get(&fake_hash).is_err());
}

// §15: Crash injection — version file corruption detected
#[test]
fn crash_injection_version_corruption_detected() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    // Corrupt the version file
    let version_path = store.path().join("store").join("version");
    fs::write(&version_path, r#"{"format_version": 999}"#).unwrap();

    // Re-initializing should detect version mismatch
    let result = layout.initialize();
    assert!(result.is_err(), "mismatched version must be rejected");
}

// §12: Environment entry under 1 second (mock backend)
#[test]
fn environment_entry_under_1_second() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();

    let start = std::time::Instant::now();
    // Mock enter is effectively instant — this tests the overhead of
    // metadata lookup, state transition, backend dispatch, and cleanup.
    engine.enter(&r.identity.env_id).unwrap();
    let elapsed = start.elapsed();

    assert!(
        elapsed.as_secs() < 1,
        "environment entry took {elapsed:?}, must be under 1s"
    );
}

// §4.1: Archive lifecycle — archive preserves but prevents entry
#[test]
fn archive_lifecycle() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();

    // Archive from Built state
    engine.archive(&r.identity.env_id).unwrap();
    let meta = engine.inspect(&r.identity.env_id).unwrap();
    assert_eq!(meta.state, EnvState::Archived);

    // Archived env cannot be entered
    let result = engine.enter(&r.identity.env_id);
    assert!(result.is_err(), "entering an archived env must fail");

    // Archived env can be rebuilt
    let r2 = engine.rebuild(&manifest).unwrap();
    let meta2 = engine.inspect(&r2.identity.env_id).unwrap();
    assert_eq!(meta2.state, EnvState::Built);
}

// §4.1: Freeze then archive lifecycle
#[test]
fn freeze_then_archive() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();

    engine.freeze(&r.identity.env_id).unwrap();
    engine.archive(&r.identity.env_id).unwrap();

    let meta = engine.inspect(&r.identity.env_id).unwrap();
    assert_eq!(meta.state, EnvState::Archived);
}

// §4.1: Destroy of non-existent env fails gracefully
#[test]
fn destroy_nonexistent_env_fails_gracefully() {
    let store = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());
    let result = engine.destroy("0000000000000000000000000000000000000000000000000000000000000000");
    assert!(result.is_err());
}

// §3.2: Lock file v2 contains resolved package versions (not just names)
#[test]
fn lock_file_v2_has_resolved_versions() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git", "clang"]));
    let r = engine.build(&manifest).unwrap();

    assert_eq!(r.lock_file.lock_version, 2);
    assert_eq!(r.lock_file.resolved_packages.len(), 2);
    for pkg in &r.lock_file.resolved_packages {
        assert!(!pkg.name.is_empty());
        assert!(!pkg.version.is_empty());
        assert_ne!(pkg.version, "unresolved");
    }
}

// §6.2: Cannot destroy a running environment (must stop first)
#[test]
fn destroy_running_env_is_rejected() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();

    // Simulate entering (mock backend sets state to Running)
    engine.enter(&r.identity.env_id).unwrap();

    // Now try to destroy — should fail because mock leaves it in Running
    // Note: mock enter() sets internal state but engine resets to Built on success,
    // so we manually set it to Running to test the guard
    let meta_store = karapace_store::MetadataStore::new(engine.store_layout().clone());
    meta_store
        .update_state(&r.identity.env_id, EnvState::Running)
        .unwrap();

    let result = engine.destroy(&r.identity.env_id);
    assert!(result.is_err(), "destroy must reject Running environments");
    let err_msg = result.unwrap_err().to_string();
    assert!(
        err_msg.contains("running") || err_msg.contains("Running"),
        "error should mention Running state: {err_msg}"
    );
}

// Quick-style manifest generation + build (tests the flow used by `karapace quick`)
#[test]
fn quick_style_generated_manifest_builds_correctly() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    // Simulate what cmd_quick does: generate a manifest from CLI flags
    let image = "rolling";
    let packages = ["git", "curl"];
    let mut manifest = String::new();
    manifest.push_str("manifest_version = 1\n\n");
    manifest.push_str("[base]\n");
    {
        use std::fmt::Write as _;
        let _ = write!(manifest, "image = \"{image}\"\n\n");
    }
    manifest.push_str("[system]\npackages = [");
    let pkg_list: Vec<String> = packages.iter().map(|p| format!("\"{p}\"")).collect();
    manifest.push_str(&pkg_list.join(", "));
    manifest.push_str("]\n\n");
    manifest.push_str("[runtime]\nbackend = \"mock\"\n");

    let manifest_path = write_manifest(project.path(), &manifest);
    let result = engine.build(&manifest_path).unwrap();

    assert!(!result.identity.env_id.is_empty());
    assert!(!result.identity.short_id.is_empty());

    let meta = engine.inspect(&result.identity.env_id).unwrap();
    assert_eq!(meta.state, EnvState::Built);
}

// Quick-style minimal manifest (no packages, no hardware)
#[test]
fn quick_style_minimal_manifest_builds() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest =
        "manifest_version = 1\n\n[base]\nimage = \"rolling\"\n\n[runtime]\nbackend = \"mock\"\n";
    let manifest_path = write_manifest(project.path(), manifest);
    let result = engine.build(&manifest_path).unwrap();

    assert!(!result.identity.env_id.is_empty());
}

// §16: No hidden mutable state — init + build produce consistent lock
#[test]
fn init_then_build_produces_consistent_state() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let init_result = engine.init(&manifest).unwrap();

    // Init creates metadata in Defined state
    let meta = engine.inspect(&init_result.identity.env_id).unwrap();
    assert_eq!(meta.state, EnvState::Defined);

    // Build resolves and creates a new identity (different from init's preliminary)
    let build_result = engine.build(&manifest).unwrap();

    // The lock file from build is verifiable
    assert!(build_result.lock_file.verify_integrity().is_ok());
    assert_eq!(build_result.lock_file.lock_version, 2);
}

// INV-S2: Restore atomicity — original upper dir preserved if snapshot invalid
#[test]
fn restore_preserves_upper_on_invalid_snapshot() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();
    let env_id = r.identity.env_id.to_string();

    // Create a file in upper that we want to verify survives a failed restore
    let upper = store.path().join("env").join(&env_id).join("upper");
    fs::create_dir_all(&upper).unwrap();
    fs::write(upper.join("sentinel.txt"), "must survive").unwrap();

    // Attempt restore with a nonexistent snapshot — should fail
    let result = engine.restore(&env_id, "nonexistent_hash_abc123");
    assert!(result.is_err());

    // Original upper content must still exist
    assert!(upper.join("sentinel.txt").exists());
    assert_eq!(
        fs::read_to_string(upper.join("sentinel.txt")).unwrap(),
        "must survive"
    );
}

// INV-S3: Multiple snapshots listed in deterministic hash order
#[test]
fn snapshot_ordering_is_deterministic() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();
    let env_id = r.identity.env_id.to_string();

    // Create upper dir with different content for each commit
    let upper = store.path().join("env").join(&env_id).join("upper");

    fs::create_dir_all(&upper).unwrap();
    fs::write(upper.join("v1.txt"), "version 1").unwrap();
    let h1 = engine.commit(&env_id).unwrap();

    fs::write(upper.join("v2.txt"), "version 2").unwrap();
    let h2 = engine.commit(&env_id).unwrap();

    fs::write(upper.join("v3.txt"), "version 3").unwrap();
    let h3 = engine.commit(&env_id).unwrap();

    // All hashes must be different
    assert_ne!(h1, h2);
    assert_ne!(h2, h3);
    assert_ne!(h1, h3);

    // list_snapshots returns sorted by hash — verify determinism
    let snaps = engine.list_snapshots(&env_id).unwrap();
    assert_eq!(snaps.len(), 3);
    let hashes: Vec<&str> = snaps.iter().map(|s| s.hash.as_str()).collect();
    let mut sorted = hashes.clone();
    sorted.sort_unstable();
    assert_eq!(hashes, sorted, "snapshots must be sorted by hash");

    // Calling again must return same order
    let snaps2 = engine.list_snapshots(&env_id).unwrap();
    let hashes2: Vec<&str> = snaps2.iter().map(|s| s.hash.as_str()).collect();
    assert_eq!(hashes, hashes2, "snapshot ordering must be deterministic");
}

// INV-W1: WAL recovery cleans orphaned env_dir after simulated build crash
#[test]
fn wal_recovery_cleans_orphaned_env_dir() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    // Simulate a crash during build:
    // 1. Create a WAL entry for a build operation
    // 2. Create an orphaned env_dir (as if build started but crashed)
    // 3. Verify that Engine::new() recovery cleans it up
    let wal = karapace_store::WriteAheadLog::new(&layout);
    wal.initialize().unwrap();

    let fake_env_id = "crash_test_env_abc123";
    let orphan_dir = store.path().join("env").join(fake_env_id);
    fs::create_dir_all(&orphan_dir).unwrap();
    fs::write(orphan_dir.join("partial_build_artifact"), "data").unwrap();

    let op_id = wal
        .begin(karapace_store::WalOpKind::Build, fake_env_id)
        .unwrap();
    wal.add_rollback_step(
        &op_id,
        karapace_store::RollbackStep::RemoveDir(orphan_dir.clone()),
    )
    .unwrap();

    // Do NOT commit the WAL — simulates a crash

    // Now create a new Engine, which should trigger WAL recovery
    let _engine = Engine::new(store.path());

    // The orphaned env_dir must be cleaned up
    assert!(
        !orphan_dir.exists(),
        "WAL recovery must remove orphaned env_dir"
    );

    // WAL must be empty after recovery
    let wal2 = karapace_store::WriteAheadLog::new(&layout);
    assert!(wal2.list_incomplete().unwrap().is_empty());
}

// --- A3: WAL & Crash Safety Hardening ---

// Simulate crash during commit: WAL entry exists, staging dir left behind
#[test]
fn wal_crash_during_commit_leaves_recoverable_state() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let wal = karapace_store::WriteAheadLog::new(&layout);
    wal.initialize().unwrap();

    // Simulate a commit that created a staging dir but crashed before completion
    let staging_dir = layout.staging_dir().join("restore-crash_test");
    fs::create_dir_all(&staging_dir).unwrap();
    fs::write(staging_dir.join("partial_data.txt"), "partial").unwrap();

    let op_id = wal
        .begin(karapace_store::WalOpKind::Commit, "crash_env")
        .unwrap();
    wal.add_rollback_step(
        &op_id,
        karapace_store::RollbackStep::RemoveDir(staging_dir.clone()),
    )
    .unwrap();

    // Do NOT commit — simulates crash

    // Recovery via new Engine must clean up
    let _engine = Engine::new(store.path());

    assert!(
        !staging_dir.exists(),
        "WAL recovery must remove orphaned staging dir"
    );
    let wal2 = karapace_store::WriteAheadLog::new(&layout);
    assert!(wal2.list_incomplete().unwrap().is_empty());
}

// Simulate crash during restore: old upper removed but new upper not yet renamed
#[test]
fn wal_crash_during_restore_rollback() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let wal = karapace_store::WriteAheadLog::new(&layout);
    wal.initialize().unwrap();

    let fake_env_id = "restore_crash_env";

    // Create orphaned staging dir (as if restore was in progress)
    let staging = layout.staging_dir().join(format!("restore-{fake_env_id}"));
    fs::create_dir_all(&staging).unwrap();
    fs::write(staging.join("snapshot_file.txt"), "snapshot data").unwrap();

    let op_id = wal
        .begin(karapace_store::WalOpKind::Restore, fake_env_id)
        .unwrap();
    wal.add_rollback_step(
        &op_id,
        karapace_store::RollbackStep::RemoveDir(staging.clone()),
    )
    .unwrap();

    // Crash — WAL entry remains

    // Recovery should clean up staging
    let count = wal.recover().unwrap();
    assert_eq!(count, 1);
    assert!(!staging.exists());
}

// Multiple incomplete WAL entries all rolled back in order
#[test]
fn wal_multi_entry_recovery() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let wal = karapace_store::WriteAheadLog::new(&layout);
    wal.initialize().unwrap();

    // Create 3 orphaned directories with WAL entries
    let mut dirs = Vec::new();
    for i in 0..3 {
        let dir = store.path().join(format!("orphan_{i}"));
        fs::create_dir_all(&dir).unwrap();
        fs::write(dir.join("data"), format!("data_{i}")).unwrap();

        let op_id = wal
            .begin(karapace_store::WalOpKind::Build, &format!("env_{i}"))
            .unwrap();
        wal.add_rollback_step(&op_id, karapace_store::RollbackStep::RemoveDir(dir.clone()))
            .unwrap();
        dirs.push(dir);
    }

    assert_eq!(wal.list_incomplete().unwrap().len(), 3);

    let count = wal.recover().unwrap();
    assert_eq!(count, 3);

    for dir in &dirs {
        assert!(!dir.exists(), "all orphaned dirs must be cleaned up");
    }
    assert!(wal.list_incomplete().unwrap().is_empty());
}

// Incomplete temp file in objects dir should not be visible
#[test]
fn incomplete_temp_file_invisible_to_store() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let obj_store = karapace_store::ObjectStore::new(layout.clone());

    // Write a valid object
    let hash = obj_store.put(b"valid data").unwrap();

    // Create a temp file (simulating interrupted atomic write)
    let temp_path = layout.objects_dir().join(".tmp_partial_write");
    fs::write(&temp_path, b"incomplete").unwrap();

    // list() should only return the valid object (skips dotfiles)
    let list = obj_store.list().unwrap();
    assert_eq!(list.len(), 1);
    assert_eq!(list[0], hash);

    // Integrity check should only check the valid object
    let report = karapace_store::verify_store_integrity(&layout).unwrap();
    assert_eq!(report.checked, 1);
    assert_eq!(report.passed, 1);
    assert!(report.failed.is_empty());
}

// Verify atomic rename is used: concurrent writes to same hash don't corrupt
#[test]
fn concurrent_object_writes_are_safe() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let store_path = store.path().to_path_buf();
    let barrier = Arc::new(Barrier::new(8));
    let mut handles = Vec::new();

    for _ in 0..8 {
        let sp = store_path.clone();
        let b = Arc::clone(&barrier);
        handles.push(thread::spawn(move || {
            let layout = StoreLayout::new(&sp);
            let obj_store = karapace_store::ObjectStore::new(layout);
            b.wait();
            // All threads write the same data — should deduplicate safely
            obj_store.put(b"concurrent-data").unwrap()
        }));
    }

    let hashes: Vec<String> = handles.into_iter().map(|h| h.join().unwrap()).collect();
    // All must produce the same hash
    let first = &hashes[0];
    for h in &hashes {
        assert_eq!(h, first);
    }

    // Data must be readable and intact
    let obj_store = karapace_store::ObjectStore::new(StoreLayout::new(store.path()));
    let data = obj_store.get(first).unwrap();
    assert_eq!(data, b"concurrent-data");
}

// --- M1: WAL Crash Safety Hardening (2.0) ---

// M1.1: Verify rollback step is registered before side-effect in build
#[test]
fn wal_rollback_registered_before_build_side_effect() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let wal = karapace_store::WriteAheadLog::new(&layout);
    wal.initialize().unwrap();

    // Simulate the new build pattern: register rollback step, then create dir
    let fake_env_id = "m1_test_build_order";
    let op_id = wal
        .begin(karapace_store::WalOpKind::Build, fake_env_id)
        .unwrap();

    let env_dir = store.path().join("env").join(fake_env_id);
    // Rollback step registered BEFORE dir creation
    wal.add_rollback_step(
        &op_id,
        karapace_store::RollbackStep::RemoveDir(env_dir.clone()),
    )
    .unwrap();

    // Verify WAL entry has the rollback step before dir even exists
    let entries = wal.list_incomplete().unwrap();
    assert_eq!(entries.len(), 1);
    assert!(!entries[0].rollback_steps.is_empty());
    assert!(!env_dir.exists(), "dir should not exist yet");

    // Now create the dir (simulating actual build)
    fs::create_dir_all(&env_dir).unwrap();

    // Simulate crash: don't commit. Recovery should clean up.
    let _engine = Engine::new(store.path());
    assert!(
        !env_dir.exists(),
        "WAL recovery must remove orphaned dir even when rollback was registered before creation"
    );
}

// M1.1: Verify rollback of nonexistent path is a safe no-op
#[test]
fn wal_rollback_nonexistent_path_is_noop() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let wal = karapace_store::WriteAheadLog::new(&layout);
    wal.initialize().unwrap();

    let op_id = wal
        .begin(karapace_store::WalOpKind::Build, "noop_test")
        .unwrap();

    let nonexistent = store.path().join("env").join("does_not_exist");
    wal.add_rollback_step(
        &op_id,
        karapace_store::RollbackStep::RemoveDir(nonexistent.clone()),
    )
    .unwrap();

    // Recovery should succeed without error even though the path doesn't exist
    let wal2 = karapace_store::WriteAheadLog::new(&layout);
    let recovered = wal2.recover().unwrap();
    assert_eq!(recovered, 1);

    // WAL should be clean
    assert!(wal2.list_incomplete().unwrap().is_empty());
}

// M1.2: Destroy with WAL protection — normal path
#[test]
fn destroy_with_wal_commits_cleanly() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();
    let env_id = r.identity.env_id.to_string();

    // Destroy should succeed and leave no WAL entries
    engine.destroy(&env_id).unwrap();

    let layout = StoreLayout::new(store.path());
    let wal = karapace_store::WriteAheadLog::new(&layout);
    assert!(
        wal.list_incomplete().unwrap().is_empty(),
        "WAL must be clean after successful destroy"
    );
}

// M1.2: Destroy WAL crash recovery — crash between env_dir removal and metadata removal
#[test]
fn wal_crash_during_destroy_is_recoverable() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let wal = karapace_store::WriteAheadLog::new(&layout);
    wal.initialize().unwrap();

    let fake_env_id = "destroy_crash_env";

    // Create env dir and metadata to simulate a built environment
    let env_dir = store.path().join("env").join(fake_env_id);
    fs::create_dir_all(&env_dir).unwrap();
    fs::write(env_dir.join("some_file"), "data").unwrap();

    let meta_dir = layout.metadata_dir();
    fs::create_dir_all(&meta_dir).unwrap();
    fs::write(
        meta_dir.join(fake_env_id),
        r#"{"env_id":"destroy_crash_env"}"#,
    )
    .unwrap();

    // Simulate a destroy that crashed: WAL entry with rollback steps, not committed
    let op_id = wal
        .begin(karapace_store::WalOpKind::Destroy, fake_env_id)
        .unwrap();
    wal.add_rollback_step(
        &op_id,
        karapace_store::RollbackStep::RemoveDir(env_dir.clone()),
    )
    .unwrap();
    wal.add_rollback_step(
        &op_id,
        karapace_store::RollbackStep::RemoveFile(meta_dir.join(fake_env_id)),
    )
    .unwrap();

    // Recovery via Engine::new should clean up
    let _engine = Engine::new(store.path());

    assert!(
        !env_dir.exists(),
        "WAL recovery must remove orphaned env_dir from incomplete destroy"
    );

    let wal2 = karapace_store::WriteAheadLog::new(&layout);
    assert!(wal2.list_incomplete().unwrap().is_empty());
}

// M1.3: Commit layer rollback — crash after layer write, before WAL commit
#[test]
fn wal_crash_during_commit_cleans_orphaned_snapshot_layer() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let wal = karapace_store::WriteAheadLog::new(&layout);
    wal.initialize().unwrap();

    // Create a fake snapshot layer manifest file
    let layers_dir = layout.layers_dir();
    fs::create_dir_all(&layers_dir).unwrap();
    let fake_hash = "orphaned_snapshot_hash_abc123";
    let layer_path = layers_dir.join(fake_hash);
    fs::write(
        &layer_path,
        r#"{"hash":"orphaned_snapshot_hash_abc123","kind":"Snapshot"}"#,
    )
    .unwrap();

    // Simulate commit crash: WAL entry with layer rollback, not committed
    let op_id = wal
        .begin(karapace_store::WalOpKind::Commit, "commit_crash_env")
        .unwrap();
    wal.add_rollback_step(
        &op_id,
        karapace_store::RollbackStep::RemoveFile(layer_path.clone()),
    )
    .unwrap();

    // Recovery should remove the orphaned snapshot layer
    let _engine = Engine::new(store.path());

    assert!(
        !layer_path.exists(),
        "WAL recovery must remove orphaned snapshot layer from incomplete commit"
    );
}

// M1.4: GC with WAL marker — normal path
#[test]
fn gc_with_wal_commits_cleanly() {
    let store = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    // Run GC (nothing to collect, but WAL should be clean after)
    let lock = StoreLock::acquire(&layout.lock_file()).unwrap();
    let report = engine.gc(&lock, true).unwrap();
    assert_eq!(report.orphaned_envs.len(), 0);

    let wal = karapace_store::WriteAheadLog::new(&layout);
    assert!(
        wal.list_incomplete().unwrap().is_empty(),
        "WAL must be clean after successful GC"
    );
}

// M1.4: GC incomplete WAL entry recovered safely
#[test]
fn gc_incomplete_wal_entry_recovered_safely() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let wal = karapace_store::WriteAheadLog::new(&layout);
    wal.initialize().unwrap();

    // Simulate an incomplete GC: WAL entry exists, GC didn't finish
    let _op_id = wal.begin(karapace_store::WalOpKind::Gc, "gc").unwrap();

    // Recovery via Engine::new should clean up the WAL entry (no rollback steps needed)
    let _engine = Engine::new(store.path());

    let wal2 = karapace_store::WriteAheadLog::new(&layout);
    assert!(
        wal2.list_incomplete().unwrap().is_empty(),
        "incomplete GC WAL entry must be cleaned up on recovery"
    );
}

// M1.4: GC is idempotent after partial run
#[test]
fn gc_is_idempotent_after_partial_run() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();
    let env_id = r.identity.env_id.to_string();

    // Destroy to create orphaned objects/layers
    engine.destroy(&env_id).unwrap();

    // First GC run
    let layout = StoreLayout::new(store.path());
    let lock = StoreLock::acquire(&layout.lock_file()).unwrap();
    let report1 = engine.gc(&lock, false).unwrap();
    // Second GC run — should be a no-op (idempotent)
    let report2 = engine.gc(&lock, false).unwrap();
    assert_eq!(
        report2.removed_envs + report2.removed_layers + report2.removed_objects,
        0,
        "second GC run should find nothing to collect: {report2:?}"
    );

    // First run should have found something to collect
    assert!(
        report1.removed_envs + report1.removed_layers + report1.removed_objects > 0,
        "first GC run should have collected orphans: {report1:?}"
    );
}

// --- M6: Failure Mode Testing (2.0) ---

// M6.2: Object read fails gracefully on permission denied
#[test]
fn object_get_fails_on_permission_denied() {
    if skip_if_root() {
        return;
    }
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let obj_store = karapace_store::ObjectStore::new(layout.clone());
    let hash = obj_store.put(b"test data").unwrap();

    // Remove read permission on the object file
    let obj_path = layout.objects_dir().join(&hash);
    fs::set_permissions(&obj_path, fs::Permissions::from_mode(0o000)).unwrap();

    let result = obj_store.get(&hash);
    assert!(result.is_err(), "get must fail on permission denied");

    // Restore permissions for cleanup
    fs::set_permissions(&obj_path, fs::Permissions::from_mode(0o644)).unwrap();
}

// M6.2: Metadata write fails gracefully on read-only store
#[test]
fn metadata_put_fails_on_read_only_dir() {
    if skip_if_root() {
        return;
    }
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let meta_store = karapace_store::MetadataStore::new(layout.clone());

    // Make metadata dir read-only
    let meta_dir = layout.metadata_dir();
    fs::set_permissions(&meta_dir, fs::Permissions::from_mode(0o555)).unwrap();

    let meta = karapace_store::EnvMetadata {
        env_id: "test_ro".into(),
        short_id: "test_ro".into(),
        name: None,
        state: EnvState::Defined,
        manifest_hash: "mhash".into(),
        base_layer: "base".into(),
        dependency_layers: vec![],
        policy_layer: None,
        created_at: "2025-01-01T00:00:00Z".to_owned(),
        updated_at: "2025-01-01T00:00:00Z".to_owned(),
        ref_count: 1,
        checksum: None,
    };
    let result = meta_store.put(&meta);
    assert!(result.is_err(), "put must fail on read-only metadata dir");

    // Restore permissions for cleanup
    fs::set_permissions(&meta_dir, fs::Permissions::from_mode(0o755)).unwrap();
}

// M6.3: Concurrent GC blocked by store lock
#[test]
fn concurrent_gc_blocked_by_lock() {
    let store = tempfile::tempdir().unwrap();
    let _engine = Engine::new(store.path());
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    // Acquire lock in main thread
    let lock = StoreLock::acquire(&layout.lock_file()).unwrap();

    // Try to acquire lock again — should return None (non-blocking try)
    let result = StoreLock::try_acquire(&layout.lock_file()).unwrap();
    assert!(
        result.is_none(),
        "second lock acquisition must return None while first is held"
    );

    drop(lock);

    // After drop, lock should be available again
    let lock2 = StoreLock::try_acquire(&layout.lock_file()).unwrap();
    assert!(lock2.is_some(), "lock must be available after drop");
}

// M6: Layer corruption detected on get
#[test]
fn layer_corruption_detected_on_get() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let layer_store = karapace_store::LayerStore::new(layout.clone());
    let layer = karapace_store::LayerManifest {
        hash: "test_layer".to_owned(),
        kind: karapace_store::LayerKind::Base,
        parent: None,
        object_refs: vec![],
        read_only: true,
        tar_hash: String::new(),
    };
    let content_hash = layer_store.put(&layer).unwrap();

    // Corrupt the layer file (flip bytes but keep it valid-ish)
    let layer_path = layout.layers_dir().join(&content_hash);
    fs::write(&layer_path, b"corrupted data that is not the original").unwrap();

    let result = layer_store.get(&content_hash);
    assert!(
        result.is_err(),
        "corrupted layer must be detected by hash verification"
    );
}

// M6: Metadata corruption detected on get
#[test]
fn metadata_corruption_detected_on_get() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let meta_store = karapace_store::MetadataStore::new(layout.clone());
    let meta = karapace_store::EnvMetadata {
        env_id: "corrupt_test".into(),
        short_id: "corrupt_test".into(),
        name: None,
        state: EnvState::Built,
        manifest_hash: "mhash".into(),
        base_layer: "base".into(),
        dependency_layers: vec![],
        policy_layer: None,
        created_at: "2025-01-01T00:00:00Z".to_owned(),
        updated_at: "2025-01-01T00:00:00Z".to_owned(),
        ref_count: 1,
        checksum: None,
    };
    meta_store.put(&meta).unwrap();

    // Corrupt the metadata file while keeping valid JSON with wrong checksum
    let meta_path = layout.metadata_dir().join("corrupt_test");
    let mut content = fs::read_to_string(&meta_path).unwrap();
    content = content.replace("corrupt_test", "tampered_val");
    fs::write(&meta_path, &content).unwrap();

    let result = meta_store.get("corrupt_test");
    assert!(
        result.is_err(),
        "tampered metadata must be detected by checksum verification"
    );
}

// M6: Destroy non-existent environment returns clean error
#[test]
fn destroy_nonexistent_env_returns_error() {
    let store = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let result = engine.destroy("does_not_exist_abc123");
    assert!(result.is_err());
    let err = result.unwrap_err().to_string();
    assert!(
        err.contains("not found") || err.contains("NotFound") || err.contains("does_not_exist"),
        "error should indicate env not found: {err}"
    );
}

// M6: Build with invalid manifest returns clean error
#[test]
fn build_invalid_manifest_returns_error() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = project.path().join("karapace.toml");
    fs::write(&manifest, "this is not valid toml [[[").unwrap();

    let result = engine.build(&manifest);
    assert!(result.is_err(), "build with invalid manifest must fail");
}

// stop() on non-Running env returns correct error
#[test]
fn stop_non_running_env_returns_error() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();

    // Environment is in Built state, not Running
    let result = engine.stop(&r.identity.env_id);
    assert!(result.is_err(), "stop on Built env must fail");
    let err_msg = result.unwrap_err().to_string();
    assert!(
        err_msg.to_lowercase().contains("running")
            || err_msg.to_lowercase().contains("not running"),
        "error should mention running state: {err_msg}"
    );
}

#[test]
fn stale_running_marker_cleaned_on_engine_new() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();

    // Build an environment so we have an env_dir
    let engine = Engine::new(store.path());
    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let result = engine.build(&manifest);
    assert!(result.is_ok());
    let env_id = result.unwrap().identity.env_id;

    // Manually create a stale .running marker (simulates a crash)
    let env_dir = store.path().join("env").join(&*env_id);
    fs::create_dir_all(&env_dir).unwrap();
    let running_marker = env_dir.join(".running");
    fs::write(&running_marker, "stale").unwrap();
    assert!(running_marker.exists());

    // Creating a new Engine should clean up the stale marker
    let _engine2 = Engine::new(store.path());
    assert!(
        !running_marker.exists(),
        "stale .running marker must be removed by Engine::new()"
    );
}

// --- §7: Disk-full / write-failure simulation ---

#[test]
fn build_on_readonly_objects_dir_returns_error() {
    if skip_if_root() {
        return;
    }
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();

    // Initialize the store, then make objects dir read-only
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();
    let objects_dir = layout.objects_dir();
    fs::set_permissions(&objects_dir, fs::Permissions::from_mode(0o444)).unwrap();

    let engine = Engine::new(store.path());
    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let result = engine.build(&manifest);

    // Restore permissions for cleanup
    fs::set_permissions(&objects_dir, fs::Permissions::from_mode(0o755)).unwrap();

    assert!(
        result.is_err(),
        "build must fail when objects dir is read-only"
    );
}

#[test]
fn build_on_readonly_metadata_dir_returns_error() {
    if skip_if_root() {
        return;
    }
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();

    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();
    let meta_dir = layout.metadata_dir();
    fs::set_permissions(&meta_dir, fs::Permissions::from_mode(0o444)).unwrap();

    let engine = Engine::new(store.path());
    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let result = engine.build(&manifest);

    fs::set_permissions(&meta_dir, fs::Permissions::from_mode(0o755)).unwrap();

    assert!(
        result.is_err(),
        "build must fail when metadata dir is read-only"
    );
}

#[test]
fn commit_on_readonly_layers_dir_returns_error() {
    if skip_if_root() {
        return;
    }
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();
    let env_id = r.identity.env_id.to_string();

    // Create upper dir with content
    let upper = store.path().join("env").join(&env_id).join("upper");
    fs::create_dir_all(&upper).unwrap();
    fs::write(upper.join("file.txt"), "data").unwrap();

    // Make layers dir read-only
    let layout = StoreLayout::new(store.path());
    let layers_dir = layout.layers_dir();
    fs::set_permissions(&layers_dir, fs::Permissions::from_mode(0o444)).unwrap();

    let result = engine.commit(&env_id);

    fs::set_permissions(&layers_dir, fs::Permissions::from_mode(0o755)).unwrap();

    assert!(
        result.is_err(),
        "commit must fail when layers dir is read-only"
    );

    // Store should still be usable after the error
    let meta = engine.inspect(&env_id).unwrap();
    assert_eq!(
        meta.state,
        EnvState::Built,
        "env must still be in Built state after failed commit"
    );
}

#[test]
fn write_failure_never_panics() {
    if skip_if_root() {
        return;
    }
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    // Test ObjectStore::put on read-only dir
    let objects_dir = layout.objects_dir();
    fs::set_permissions(&objects_dir, fs::Permissions::from_mode(0o444)).unwrap();
    let obj_store = karapace_store::ObjectStore::new(layout.clone());
    let result = obj_store.put(b"test data");
    fs::set_permissions(&objects_dir, fs::Permissions::from_mode(0o755)).unwrap();
    assert!(
        result.is_err(),
        "ObjectStore::put must return Err, not panic"
    );

    // Test LayerStore::put on read-only dir
    let layers_dir = layout.layers_dir();
    fs::set_permissions(&layers_dir, fs::Permissions::from_mode(0o444)).unwrap();
    let layer_store = karapace_store::LayerStore::new(layout.clone());
    let layer = karapace_store::LayerManifest {
        hash: "test".into(),
        kind: karapace_store::LayerKind::Base,
        parent: None,
        object_refs: vec![],
        read_only: true,
        tar_hash: "test".into(),
    };
    let result = layer_store.put(&layer);
    fs::set_permissions(&layers_dir, fs::Permissions::from_mode(0o755)).unwrap();
    assert!(
        result.is_err(),
        "LayerStore::put must return Err, not panic"
    );

    // Test MetadataStore::put on read-only dir
    let meta_dir = layout.metadata_dir();
    fs::set_permissions(&meta_dir, fs::Permissions::from_mode(0o444)).unwrap();
    let meta_store = karapace_store::MetadataStore::new(layout.clone());
    let meta = karapace_store::EnvMetadata {
        env_id: "test123".into(),
        short_id: "test123".into(),
        name: None,
        state: EnvState::Defined,
        manifest_hash: "mh".into(),
        base_layer: "bl".into(),
        dependency_layers: vec![],
        policy_layer: None,
        created_at: "2025-01-01T00:00:00Z".to_owned(),
        updated_at: "2025-01-01T00:00:00Z".to_owned(),
        ref_count: 1,
        checksum: None,
    };
    let result = meta_store.put(&meta);
    fs::set_permissions(&meta_dir, fs::Permissions::from_mode(0o755)).unwrap();
    assert!(
        result.is_err(),
        "MetadataStore::put must return Err, not panic"
    );
}

// --- §2: Bit-flip corruption detection ---

#[test]
fn bitflip_corruption_detected_on_objects() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();
    let obj_store = karapace_store::ObjectStore::new(layout.clone());

    // Write several objects
    let h1 = obj_store.put(b"object-alpha").unwrap();
    let h2 = obj_store.put(b"object-beta-longer-content-here").unwrap();
    let h3 = obj_store.put(b"object-gamma").unwrap();

    // Flip a random byte in object 2
    let path = layout.objects_dir().join(&h2);
    let mut data = fs::read(&path).unwrap();
    let flip_idx = data.len() / 2;
    data[flip_idx] ^= 0xFF;
    fs::write(&path, &data).unwrap();

    // Intact objects must still be readable
    assert!(obj_store.get(&h1).is_ok());
    assert!(obj_store.get(&h3).is_ok());

    // Corrupted object must fail with typed error
    let result = obj_store.get(&h2);
    assert!(result.is_err(), "bit-flipped object must be detected");

    // verify_store_integrity must report exactly 1 failure
    let report = karapace_store::verify_store_integrity(&layout).unwrap();
    assert_eq!(report.checked, 3);
    assert_eq!(report.passed, 2);
    assert_eq!(report.failed.len(), 1);
    assert_eq!(report.failed[0].hash, h2);
}

// --- §6: GC dry-run equivalence ---

#[test]
fn gc_dry_run_matches_real_gc() {
    let store = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    // Build 5 environments, destroy 3
    let mut env_ids = Vec::new();
    for i in 0..5 {
        let p = tempfile::tempdir().unwrap();
        let pkgs: Vec<String> = (0..=i).map(|j| format!("pkg{j}")).collect();
        let pkg_refs: Vec<&str> = pkgs.iter().map(String::as_str).collect();
        let m = write_manifest(p.path(), &mock_manifest(&pkg_refs));
        let r = engine.build(&m).unwrap();
        env_ids.push(r.identity.env_id.to_string());
    }
    for id in &env_ids[..3] {
        engine.destroy(id).unwrap();
    }

    let layout = StoreLayout::new(store.path());
    let lock = StoreLock::acquire(&layout.lock_file()).unwrap();

    // Dry run — populates orphaned_* but doesn't remove
    let dry_report = engine.gc(&lock, true).unwrap();
    assert_eq!(
        dry_report.removed_envs, 0,
        "dry-run must not remove anything"
    );

    // Real GC — actually removes
    let real_report = engine.gc(&lock, false).unwrap();

    // Dry-run orphaned counts must match real removed counts
    assert_eq!(
        dry_report.orphaned_envs.len(),
        real_report.removed_envs,
        "dry-run orphaned_envs must match real removed_envs"
    );
    assert_eq!(
        dry_report.orphaned_layers.len(),
        real_report.removed_layers,
        "dry-run orphaned_layers must match real removed_layers"
    );
    assert_eq!(
        dry_report.orphaned_objects.len(),
        real_report.removed_objects,
        "dry-run orphaned_objects must match real removed_objects"
    );
}

// --- §15: Soak test — 100 build/destroy/GC cycles ---

#[test]
fn soak_100_build_destroy_gc_cycles() {
    let store = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());
    let layout = StoreLayout::new(store.path());

    for cycle in 0..100 {
        let p = tempfile::tempdir().unwrap();
        let pkgs: Vec<String> = (0..=(cycle % 5)).map(|j| format!("pkg{j}")).collect();
        let pkg_refs: Vec<&str> = pkgs.iter().map(String::as_str).collect();
        let m = write_manifest(p.path(), &mock_manifest(&pkg_refs));

        let r = engine.build(&m).unwrap();
        let env_id = r.identity.env_id.to_string();

        // Verify it's inspectable
        let meta = engine.inspect(&env_id).unwrap();
        assert_eq!(meta.state, EnvState::Built);

        // Destroy
        engine.destroy(&env_id).unwrap();

        // GC every 10 cycles
        if cycle % 10 == 9 {
            let lock = StoreLock::acquire(&layout.lock_file()).unwrap();
            let _report = engine.gc(&lock, false).unwrap();
        }
    }

    // Final state: no environments should remain
    let envs = engine.list().unwrap();
    assert_eq!(
        envs.len(),
        0,
        "all environments must be destroyed after soak"
    );

    // Final GC — store should be clean
    let lock = StoreLock::acquire(&layout.lock_file()).unwrap();
    let _report = engine.gc(&lock, false).unwrap();

    // WAL must be clean
    let wal = karapace_store::WriteAheadLog::new(&layout);
    assert!(wal.list_incomplete().unwrap().is_empty());

    // Store integrity must pass
    let integrity = karapace_store::verify_store_integrity(&layout).unwrap();
    assert!(
        integrity.failed.is_empty(),
        "store integrity must be clean after 100 cycles"
    );
}

// --- §11: Store v1 rejection test ---

#[test]
fn old_store_version_rejected_with_clear_error() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    // Overwrite the version file with v1 (old format)
    let version_path = store.path().join("store").join("version");
    fs::write(&version_path, r#"{"format_version": 1}"#).unwrap();

    // Re-initializing must reject v1
    let result = layout.initialize();
    assert!(
        result.is_err(),
        "store v1 must be rejected by the current binary"
    );

    // The error message should mention the version
    let err_msg = result.unwrap_err().to_string();
    assert!(
        err_msg.contains("version") || err_msg.contains("format"),
        "error should mention version mismatch: {err_msg}"
    );
}

// --- §4: Resolver / lockfile determinism ---

#[test]
fn build_with_invalid_manifest_returns_manifest_error() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(
        project.path(),
        r"
manifest_version = 1
[base]
",
    );
    let result = engine.build(&manifest);
    assert!(result.is_err(), "build with missing image must fail");
}

#[test]
fn build_with_invalid_backend_returns_error() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(
        project.path(),
        r#"
manifest_version = 1
[base]
image = "rolling"
[runtime]
backend = "nonexistent_backend"
"#,
    );
    let result = engine.build(&manifest);
    assert!(result.is_err(), "build with unknown backend must fail");
}

#[test]
fn lockfile_determinism_same_inputs_same_env_id() {
    let store = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    // Build twice with identical manifests in different project dirs
    let p1 = tempfile::tempdir().unwrap();
    let m1 = write_manifest(p1.path(), &mock_manifest(&["git", "vim"]));
    let r1 = engine.build(&m1).unwrap();

    // Destroy first, then rebuild with same manifest
    engine.destroy(&r1.identity.env_id).unwrap();

    let p2 = tempfile::tempdir().unwrap();
    let m2 = write_manifest(p2.path(), &mock_manifest(&["git", "vim"]));
    let r2 = engine.build(&m2).unwrap();

    assert_eq!(
        r1.identity.env_id, r2.identity.env_id,
        "same manifest must produce same env_id"
    );
}

#[test]
fn different_packages_produce_different_env_id() {
    let store = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let p1 = tempfile::tempdir().unwrap();
    let m1 = write_manifest(p1.path(), &mock_manifest(&["git"]));
    let r1 = engine.build(&m1).unwrap();

    let p2 = tempfile::tempdir().unwrap();
    let m2 = write_manifest(p2.path(), &mock_manifest(&["vim"]));
    let r2 = engine.build(&m2).unwrap();

    assert_ne!(
        r1.identity.env_id, r2.identity.env_id,
        "different packages must produce different env_id"
    );
}

// --- §6: GC 1000-object stress test ---

#[test]
fn gc_stress_1000_objects() {
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();
    let obj_store = karapace_store::ObjectStore::new(layout.clone());

    // Create 1000 orphaned objects (no layer or metadata references them)
    for i in 0..1000 {
        obj_store
            .put(format!("orphaned-object-{i}").as_bytes())
            .unwrap();
    }

    // Also create a live environment so GC has reachable refs to trace
    let engine = Engine::new(store.path());
    let p = tempfile::tempdir().unwrap();
    let m = write_manifest(p.path(), &mock_manifest(&["git"]));
    let _r = engine.build(&m).unwrap();

    // Run GC
    let lock = StoreLock::acquire(&layout.lock_file()).unwrap();
    let report = engine.gc(&lock, false).unwrap();

    // All 1000 orphans must be collected
    assert_eq!(
        report.removed_objects, 1000,
        "GC must collect all 1000 orphaned objects, got {}",
        report.removed_objects
    );

    // Live env must survive
    assert_eq!(engine.list().unwrap().len(), 1);

    // Store integrity must still pass
    let integrity = karapace_store::verify_store_integrity(&layout).unwrap();
    assert!(
        integrity.failed.is_empty(),
        "store integrity must pass after GC stress"
    );
}

// --- §5: Concurrent build operations ---

#[test]
fn concurrent_builds_do_not_corrupt_store() {
    let store = tempfile::tempdir().unwrap();
    let store_path = store.path().to_owned();

    let barrier = Arc::new(Barrier::new(4));
    let mut handles = Vec::new();

    for thread_idx in 0..4 {
        let path = store_path.clone();
        let b = Arc::clone(&barrier);
        handles.push(thread::spawn(move || {
            let engine = Engine::new(&path);
            b.wait();

            let p = tempfile::tempdir().unwrap();
            let pkgs: Vec<String> = (0..=(thread_idx % 3))
                .map(|j| format!("t{thread_idx}pkg{j}"))
                .collect();
            let pkg_refs: Vec<&str> = pkgs.iter().map(String::as_str).collect();
            let m_content = format!(
                r#"
manifest_version = 1
[base]
image = "rolling"
[system]
packages = [{}]
[runtime]
backend = "mock"
"#,
                pkg_refs
                    .iter()
                    .map(|p| format!("\"{p}\""))
                    .collect::<Vec<_>>()
                    .join(", ")
            );
            let manifest_path = p.path().join("karapace.toml");
            fs::write(&manifest_path, &m_content).unwrap();
            engine.build(&manifest_path).unwrap();
        }));
    }

    for h in handles {
        h.join().unwrap();
    }

    // Verify store is healthy after concurrent builds
    let engine = Engine::new(&store_path);
    let envs = engine.list().unwrap();
    assert_eq!(envs.len(), 4, "all 4 concurrent builds must succeed");

    let layout = StoreLayout::new(&store_path);
    let integrity = karapace_store::verify_store_integrity(&layout).unwrap();
    assert!(
        integrity.failed.is_empty(),
        "store must be intact after concurrent builds"
    );
}

// --- M6.1: WAL write failure simulation ---

#[test]
fn wal_write_fails_on_readonly_dir() {
    if skip_if_root() {
        return;
    }
    let store = tempfile::tempdir().unwrap();
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();

    let wal = karapace_store::WriteAheadLog::new(&layout);
    wal.initialize().unwrap();

    // Make WAL dir read-only
    let wal_dir = store.path().join("store").join("wal");
    fs::set_permissions(&wal_dir, fs::Permissions::from_mode(0o444)).unwrap();

    let result = wal.begin(karapace_store::WalOpKind::Build, "test-env");

    fs::set_permissions(&wal_dir, fs::Permissions::from_mode(0o755)).unwrap();

    assert!(
        result.is_err(),
        "WAL begin must fail when WAL dir is read-only"
    );
}

#[test]
fn build_fails_cleanly_when_wal_dir_is_readonly() {
    if skip_if_root() {
        return;
    }
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();

    // Initialize layout and WAL so the dir exists
    let layout = StoreLayout::new(store.path());
    layout.initialize().unwrap();
    let wal = karapace_store::WriteAheadLog::new(&layout);
    wal.initialize().unwrap();

    let engine = Engine::new(store.path());

    // Now make WAL dir read-only to simulate disk full
    let wal_dir = layout.root().join("store").join("wal");
    fs::set_permissions(&wal_dir, fs::Permissions::from_mode(0o444)).unwrap();

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let result = engine.build(&manifest);

    fs::set_permissions(&wal_dir, fs::Permissions::from_mode(0o755)).unwrap();

    assert!(
        result.is_err(),
        "build must fail cleanly when WAL dir is read-only (simulates disk full)"
    );
}

// --- M6.4: stop() SIGTERM/SIGKILL path tests ---

#[test]
fn stop_sends_sigterm_to_real_process() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();
    let env_id = r.identity.env_id.to_string();

    // Spawn a real sleep process and wait on it later to avoid zombie
    let mut child = std::process::Command::new("sleep")
        .arg("60")
        .spawn()
        .expect("spawn sleep");
    let pid = child.id();
    let pid_i32 = i32::try_from(pid).expect("pid fits in i32");

    // Set metadata to Running state and write .running marker with real PID
    let layout = StoreLayout::new(store.path());
    let meta_store = karapace_store::MetadataStore::new(layout.clone());
    meta_store.update_state(&env_id, EnvState::Running).unwrap();

    let env_dir = store.path().join("env").join(&env_id);
    fs::create_dir_all(&env_dir).unwrap();
    fs::write(env_dir.join(".running"), pid.to_string()).unwrap();

    // Verify the process is alive
    assert!(
        Path::new(&format!("/proc/{pid}")).exists(),
        "sleep process must be alive before stop"
    );

    // Stop should send SIGTERM (the mock backend returns pid=99999 which won't exist,
    // but the real process PID was written to .running). The mock backend's status()
    // returns pid=99999 for running envs, so stop() will try to kill 99999 (ESRCH).
    // This tests the ESRCH handling path.
    let result = engine.stop(&env_id);

    // Clean up the real process regardless of result
    unsafe {
        libc::kill(pid_i32, libc::SIGKILL);
    }
    let _ = child.wait();

    assert!(
        result.is_ok(),
        "stop must succeed even when backend PID no longer exists (ESRCH path): {:?}",
        result.err()
    );

    // Verify state was reset to Built
    let meta = engine.inspect(&env_id).unwrap();
    assert_eq!(
        meta.state,
        EnvState::Built,
        "state must be reset to Built after stop"
    );
}

#[test]
fn stop_invalid_pid_handled_gracefully() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();
    let env_id = r.identity.env_id.to_string();

    // Manually set state to Running (the mock backend will report pid=99999)
    let layout = StoreLayout::new(store.path());
    let meta_store = karapace_store::MetadataStore::new(layout.clone());
    meta_store.update_state(&env_id, EnvState::Running).unwrap();

    // stop() will try to kill pid 99999 which doesn't exist → ESRCH
    let result = engine.stop(&env_id);

    // Must handle gracefully (ESRCH = process already exited)
    assert!(
        result.is_ok(),
        "stop with non-existent PID must succeed (ESRCH handled): {:?}",
        result.err()
    );

    let meta = engine.inspect(&env_id).unwrap();
    assert_eq!(meta.state, EnvState::Built);
}

// --- M7: Coverage expansion ---

#[test]
fn freeze_and_archive_state_transitions() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();
    let env_id = r.identity.env_id.to_string();

    // Built → Frozen
    engine.freeze(&env_id).unwrap();
    let meta = engine.inspect(&env_id).unwrap();
    assert_eq!(meta.state, EnvState::Frozen);

    // Frozen → Archived
    engine.archive(&env_id).unwrap();
    let meta = engine.inspect(&env_id).unwrap();
    assert_eq!(meta.state, EnvState::Archived);
}

#[test]
fn rename_environment_works() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r = engine.build(&manifest).unwrap();
    let env_id = r.identity.env_id.to_string();

    // Rename
    engine.rename(&env_id, "my-dev-env").unwrap();
    let meta = engine.inspect(&env_id).unwrap();
    assert_eq!(meta.name, Some("my-dev-env".to_owned()));

    // Rename again
    engine.rename(&env_id, "new-name").unwrap();
    let meta = engine.inspect(&env_id).unwrap();
    assert_eq!(meta.name, Some("new-name".to_owned()));
}

#[test]
fn verify_store_reports_all_clean_after_fresh_build() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    let manifest = write_manifest(project.path(), &mock_manifest(&["git", "vim"]));
    let _r = engine.build(&manifest).unwrap();

    let layout = StoreLayout::new(store.path());
    let report = karapace_store::verify_store_integrity(&layout).unwrap();

    assert!(report.checked > 0, "must check at least some objects");
    assert_eq!(report.passed, report.checked);
    assert!(report.failed.is_empty());
    assert!(report.layers_checked > 0, "must check at least some layers");
    assert_eq!(report.layers_passed, report.layers_checked);
    assert!(
        report.metadata_checked > 0,
        "must check at least some metadata"
    );
    assert_eq!(report.metadata_passed, report.metadata_checked);
}

#[test]
fn rebuild_preserves_new_and_cleans_old() {
    let store = tempfile::tempdir().unwrap();
    let project = tempfile::tempdir().unwrap();
    let engine = Engine::new(store.path());

    // Initial build
    let manifest = write_manifest(project.path(), &mock_manifest(&["git"]));
    let r1 = engine.build(&manifest).unwrap();
    let old_id = r1.identity.env_id.to_string();

    // Rebuild with different packages → different env_id
    let manifest2 = write_manifest(project.path(), &mock_manifest(&["git", "vim"]));
    let r2 = engine.rebuild(&manifest2).unwrap();
    let new_id = r2.identity.env_id.to_string();

    assert_ne!(
        old_id, new_id,
        "different packages must produce different env_id"
    );

    // New env must be inspectable
    let meta = engine.inspect(&new_id).unwrap();
    assert_eq!(meta.state, EnvState::Built);

    // Old env must be gone (destroyed by rebuild)
    assert!(engine.inspect(&old_id).is_err());
}
