use super::tests_helpers::make_lock;
use super::*;
use crate::core::types::{ResourceLock, ResourceStatus, ResourceType};
use std::collections::HashMap;
use std::path::Path;
#[test]
fn test_fj131_update_global_lock_overwrites_machine_stats() {
let dir = tempfile::tempdir().unwrap();
let results1 = vec![("web".to_string(), 10_usize, 8_usize, 2_usize)];
update_global_lock(dir.path(), "infra", &results1).unwrap();
let results2 = vec![("web".to_string(), 10_usize, 10_usize, 0_usize)];
update_global_lock(dir.path(), "infra", &results2).unwrap();
let loaded = load_global_lock(dir.path()).unwrap().unwrap();
assert_eq!(loaded.machines["web"].converged, 10);
assert_eq!(loaded.machines["web"].failed, 0);
}
#[test]
fn test_fj131_new_lock_generator_format() {
let lock = new_lock("m", "h");
assert!(lock.generator.starts_with("forjar "));
let version_part = lock.generator.strip_prefix("forjar ").unwrap();
assert!(
!version_part.is_empty(),
"should have version after 'forjar '"
);
}
#[test]
fn test_fj131_new_global_lock_generator_format() {
let lock = new_global_lock("test");
assert!(lock.generator.starts_with("forjar "));
assert_eq!(lock.schema, "1.0");
}
#[test]
fn test_fj131_save_lock_deep_state_dir() {
let dir = tempfile::tempdir().unwrap();
let deep = dir.path().join("a").join("b").join("c");
let lock = make_lock();
std::fs::create_dir_all(&deep).unwrap();
save_lock(&deep, &lock).unwrap();
let loaded = load_lock(&deep, "test").unwrap().unwrap();
assert_eq!(loaded.machine, "test");
}
#[test]
fn test_fj131_update_global_lock_changes_name() {
let dir = tempfile::tempdir().unwrap();
let results = vec![("web".to_string(), 3_usize, 3_usize, 0_usize)];
update_global_lock(dir.path(), "old-name", &results).unwrap();
let results2: Vec<(String, usize, usize, usize)> = vec![];
update_global_lock(dir.path(), "new-name", &results2).unwrap();
let loaded = load_global_lock(dir.path()).unwrap().unwrap();
assert_eq!(loaded.name, "new-name");
assert!(loaded.machines.contains_key("web"));
}
#[test]
fn test_fj131_lock_roundtrip_preserves_all_status_types() {
let dir = tempfile::tempdir().unwrap();
let mut lock = make_lock();
lock.resources.insert(
"drifted-res".to_string(),
ResourceLock {
resource_type: ResourceType::Package,
status: ResourceStatus::Drifted,
applied_at: None,
duration_seconds: None,
hash: "blake3:xxx".to_string(),
details: HashMap::new(),
},
);
lock.resources.insert(
"unknown-res".to_string(),
ResourceLock {
resource_type: ResourceType::Service,
status: ResourceStatus::Unknown,
applied_at: None,
duration_seconds: None,
hash: "".to_string(),
details: HashMap::new(),
},
);
save_lock(dir.path(), &lock).unwrap();
let loaded = load_lock(dir.path(), "test").unwrap().unwrap();
assert_eq!(
loaded.resources["drifted-res"].status,
ResourceStatus::Drifted
);
assert_eq!(
loaded.resources["unknown-res"].status,
ResourceStatus::Unknown
);
assert_eq!(
loaded.resources["test-pkg"].status,
ResourceStatus::Converged
);
}
#[test]
fn test_fj132_save_lock_with_duration() {
let dir = tempfile::tempdir().unwrap();
let mut lock = new_lock("m1", "host1");
lock.resources.insert(
"timed-res".to_string(),
ResourceLock {
resource_type: ResourceType::File,
status: ResourceStatus::Converged,
applied_at: Some("2026-01-01T00:00:00Z".to_string()),
duration_seconds: Some(1.234),
hash: "blake3:abc123".to_string(),
details: HashMap::new(),
},
);
save_lock(dir.path(), &lock).unwrap();
let loaded = load_lock(dir.path(), "m1").unwrap().unwrap();
assert_eq!(loaded.resources["timed-res"].duration_seconds, Some(1.234));
}
#[test]
fn test_fj132_save_lock_unicode_hostname() {
let dir = tempfile::tempdir().unwrap();
let lock = new_lock("edge-node-01", "räck-ünit");
save_lock(dir.path(), &lock).unwrap();
let loaded = load_lock(dir.path(), "edge-node-01").unwrap().unwrap();
assert_eq!(loaded.hostname, "räck-ünit");
}
#[test]
fn test_fj132_lock_file_path_consistency() {
let dir = Path::new("/tmp/forjar-state");
let p1 = lock_file_path(dir, "web");
let p2 = lock_file_path(dir, "web");
assert_eq!(p1, p2);
assert!(p1.ends_with("web/state.lock.yaml"));
}
#[test]
fn test_fj132_update_global_lock_preserves_existing_machines() {
let dir = tempfile::tempdir().unwrap();
let results1 = vec![("web".to_string(), 5usize, 5usize, 0usize)];
update_global_lock(dir.path(), "test-config", &results1).unwrap();
let results2 = vec![("db".to_string(), 3usize, 2usize, 1usize)];
update_global_lock(dir.path(), "test-config", &results2).unwrap();
let lock = load_global_lock(dir.path()).unwrap().unwrap();
assert!(lock.machines.contains_key("web"), "web should be preserved");
assert!(lock.machines.contains_key("db"), "db should be added");
assert_eq!(lock.machines["web"].converged, 5);
assert_eq!(lock.machines["db"].failed, 1);
}
#[test]
fn test_fj132_new_lock_fields_populated() {
let lock = new_lock("prod-1", "prod-host");
assert_eq!(lock.schema, "1.0");
assert_eq!(lock.machine, "prod-1");
assert_eq!(lock.hostname, "prod-host");
assert_eq!(lock.blake3_version, "1.8");
assert!(lock.generator.starts_with("forjar "));
assert!(!lock.generated_at.is_empty());
assert!(lock.resources.is_empty());
}
#[test]
fn test_fj132_global_lock_schema_version() {
let lock = new_global_lock("my-infra");
assert_eq!(lock.schema, "1.0");
assert_eq!(lock.name, "my-infra");
assert!(lock.machines.is_empty());
}
#[test]
fn test_fj132_save_lock_with_many_details() {
let dir = tempfile::tempdir().unwrap();
let mut details = HashMap::new();
details.insert(
"path".to_string(),
serde_yaml_ng::Value::String("/etc/app.conf".to_string()),
);
details.insert(
"content_hash".to_string(),
serde_yaml_ng::Value::String("blake3:deadbeef".to_string()),
);
details.insert(
"owner".to_string(),
serde_yaml_ng::Value::String("root".to_string()),
);
details.insert(
"mode".to_string(),
serde_yaml_ng::Value::String("0644".to_string()),
);
let mut lock = new_lock("m1", "h1");
lock.resources.insert(
"config".to_string(),
ResourceLock {
resource_type: ResourceType::File,
status: ResourceStatus::Converged,
applied_at: None,
duration_seconds: None,
hash: "blake3:abc".to_string(),
details,
},
);
save_lock(dir.path(), &lock).unwrap();
let loaded = load_lock(dir.path(), "m1").unwrap().unwrap();
let d = &loaded.resources["config"].details;
assert_eq!(d.len(), 4);
assert_eq!(
d["path"],
serde_yaml_ng::Value::String("/etc/app.conf".to_string())
);
}
#[test]
fn test_fj132_concurrent_save_different_machines() {
let dir = tempfile::tempdir().unwrap();
let lock_a = new_lock("machine-a", "host-a");
let lock_b = new_lock("machine-b", "host-b");
save_lock(dir.path(), &lock_a).unwrap();
save_lock(dir.path(), &lock_b).unwrap();
let loaded_a = load_lock(dir.path(), "machine-a").unwrap().unwrap();
let loaded_b = load_lock(dir.path(), "machine-b").unwrap().unwrap();
assert_eq!(loaded_a.hostname, "host-a");
assert_eq!(loaded_b.hostname, "host-b");
}
#[test]
fn test_fj036_save_and_load_lock_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let mut lock = make_lock();
lock.machine = "roundtrip-box".to_string();
lock.hostname = "rt-host".to_string();
lock.schema = "1.0".to_string();
lock.blake3_version = "1.8".to_string();
lock.resources.insert(
"extra-svc".to_string(),
ResourceLock {
resource_type: ResourceType::Service,
status: ResourceStatus::Failed,
applied_at: Some("2026-02-25T12:00:00Z".to_string()),
duration_seconds: Some(2.75),
hash: "blake3:roundtrip".to_string(),
details: HashMap::new(),
},
);
save_lock(dir.path(), &lock).unwrap();
let loaded = load_lock(dir.path(), "roundtrip-box").unwrap().unwrap();
assert_eq!(loaded.machine, "roundtrip-box");
assert_eq!(loaded.hostname, "rt-host");
assert_eq!(loaded.schema, "1.0");
assert_eq!(loaded.blake3_version, "1.8");
assert_eq!(loaded.resources.len(), lock.resources.len());
assert_eq!(loaded.resources["extra-svc"].status, ResourceStatus::Failed);
assert_eq!(loaded.resources["extra-svc"].duration_seconds, Some(2.75));
assert_eq!(loaded.resources["extra-svc"].hash, "blake3:roundtrip");
}
#[test]
fn test_fj036_load_lock_nonexistent_returns_none() {
let dir = tempfile::tempdir().unwrap();
let result = load_lock(dir.path(), "no-such-machine").unwrap();
assert!(
result.is_none(),
"loading a lock for a nonexistent machine must return None"
);
}
#[test]
fn test_fj036_lock_path_construction() {
let state_dir = Path::new("/var/lib/forjar/state");
let machine = "web-prod";
let p = lock_file_path(state_dir, machine);
assert_eq!(
p,
std::path::PathBuf::from("/var/lib/forjar/state/web-prod/state.lock.yaml"),
"lock path must be state_dir/machine/state.lock.yaml"
);
}
#[test]
fn test_fj036_save_lock_creates_state_dir() {
let dir = tempfile::tempdir().unwrap();
let deep_state = dir.path().join("nonexistent").join("deep").join("state");
assert!(!deep_state.exists());
let lock = make_lock();
save_lock(&deep_state, &lock).unwrap();
let expected_dir = deep_state.join("test");
assert!(
expected_dir.exists(),
"save_lock must create the state directory hierarchy"
);
let expected_file = lock_file_path(&deep_state, "test");
assert!(
expected_file.exists(),
"lock file must exist after save_lock"
);
}