// Cross-platform shared memory - each platform uses its optimal mechanism
//
// Linux: /dev/shm/horus (tmpfs - RAM-backed, fastest) via file mmap
// macOS: POSIX shm_open() (Mach shared memory - RAM-backed) - NOT file-based!
// Windows: CreateFileMappingW (pagefile-backed - optimized for IPC) - NOT temp files!
//
// Note: macOS and Windows no longer use filesystem paths for shared memory.
// The path functions below are kept for Linux and backward compatibility only.

use std::path::PathBuf;

/// Sanitize a namespace string: replace any character that is not ASCII
/// alphanumeric or underscore with an underscore.
fn sanitize_namespace(ns: &str) -> String {
    ns.chars()
        .map(|c| {
            if c.is_ascii_alphanumeric() || c == '_' {
                c
            } else {
                '_'
            }
        })
        .collect()
}

/// Generate the SHM namespace for this process without caching.
///
/// This is the uncached version, suitable for unit tests.  Production code
/// should call [`shm_namespace`] instead, which memoises the result.
///
/// Priority:
/// 1. `HORUS_NAMESPACE` — set by `horus_manager` when it launches a node graph.
/// 2. `HORUS_SHM_NAMESPACE` — legacy name, kept for backward compatibility.
/// 3. Auto-generated from process-group ID + user ID on Unix
///    (`pgid{PGID}_uid{UID}`), or from the process ID on non-Unix platforms
///    (`pid{PID}`).  This ensures that independent HORUS applications running
///    on the same machine automatically use different SHM regions without any
///    explicit configuration.
pub fn generate_namespace() -> String {
    // 1. HORUS_NAMESPACE — set by horus_manager (launch.rs sets this env var)
    if let Ok(ns) = std::env::var("HORUS_NAMESPACE") {
        if !ns.is_empty() {
            return sanitize_namespace(&ns);
        }
    }
    // 2. HORUS_SHM_NAMESPACE — legacy fallback
    if let Ok(ns) = std::env::var("HORUS_SHM_NAMESPACE") {
        if !ns.is_empty() {
            return sanitize_namespace(&ns);
        }
    }
    // 3. Auto-generate: PGID groups all processes launched from the same parent
    //    so they share SHM; different applications have different PGIDs.
    #[cfg(unix)]
    {
        // SAFETY: getpgrp() and getuid() are always-succeeding async-signal-safe syscalls.
        let pgid = unsafe { libc::getpgrp() };
        let uid = unsafe { libc::getuid() };
        format!("pgid{}_uid{}", pgid, uid)
    }
    #[cfg(not(unix))]
    {
        format!("pid{}", std::process::id())
    }
}

/// Return the SHM namespace for this process.
///
/// The namespace is always a non-empty string and is resolved once then
/// cached for the lifetime of the process.  See [`generate_namespace`] for
/// the resolution priority and format.
///
/// Examples (with `HORUS_NAMESPACE=robot1`):
/// - Linux:   `/dev/shm/horus_robot1/topics/`
/// - macOS:   POSIX shm names become `/horus_robot1_<topic>`
pub fn shm_namespace() -> String {
    static NAMESPACE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
    NAMESPACE.get_or_init(generate_namespace).clone()
}

/// Get the base directory for HORUS shared memory.
///
/// This returns a platform-appropriate path for shared memory:
/// - Linux: `/dev/shm/horus_<namespace>/` (tmpfs for maximum performance)
/// - macOS: `/tmp/horus_<namespace>/` (no /dev/shm, but /tmp is still fast)
/// - Windows: `%TEMP%\horus_<namespace>\` (system temp directory)
///
/// The namespace suffix comes from [`shm_namespace`] (see its documentation
/// for the resolution order) and ensures that independent HORUS applications
/// running on the same machine use completely separate SHM regions.
pub fn shm_base_dir() -> PathBuf {
    let dir_name = format!("horus_{}", shm_namespace());

    #[cfg(target_os = "linux")]
    {
        PathBuf::from("/dev/shm").join(&dir_name)
    }

    #[cfg(target_os = "macos")]
    {
        // macOS doesn't have /dev/shm, use /tmp instead
        PathBuf::from("/tmp").join(&dir_name)
    }

    #[cfg(target_os = "windows")]
    {
        // Windows uses temp directory
        std::env::temp_dir().join(&dir_name)
    }

    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
    {
        // Fallback for other Unix-like systems (BSD, etc.)
        PathBuf::from("/tmp").join(&dir_name)
    }
}

/// Get the topics directory for shared memory message passing
pub fn shm_topics_dir() -> PathBuf {
    shm_base_dir().join("topics")
}

/// Get the nodes directory for node presence files
///
/// Each running node creates a presence file here at startup and removes it at shutdown.
/// Monitor scans this directory to discover all active nodes.
///
/// Structure: `/dev/shm/horus/nodes/{node_name}.json`
pub fn shm_nodes_dir() -> PathBuf {
    shm_base_dir().join("nodes")
}

/// Get the network status directory for transport monitoring
pub fn shm_network_dir() -> PathBuf {
    shm_base_dir().join("network")
}

/// Get the control directory for node lifecycle commands
///
/// Control files allow external processes (like `horus node kill`) to
/// send commands to running nodes without killing the entire scheduler.
///
/// Structure: `/dev/shm/horus/control/{node_name}.cmd`
/// Commands: "stop", "restart", "pause", "resume"
pub fn shm_control_dir() -> PathBuf {
    shm_base_dir().join("control")
}

/// Get the logs shared memory path
pub fn shm_logs_path() -> PathBuf {
    // Logs are at the same level as horus dir, not inside it
    #[cfg(target_os = "linux")]
    {
        PathBuf::from("/dev/shm/horus_logs")
    }

    #[cfg(target_os = "macos")]
    {
        PathBuf::from("/tmp/horus_logs")
    }

    #[cfg(target_os = "windows")]
    {
        std::env::temp_dir().join("horus_logs")
    }

    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
    {
        PathBuf::from("/tmp/horus_logs")
    }
}

/// Returns true if the platform has native shared memory support (/dev/shm on Linux).
pub fn has_native_shm() -> bool {
    #[cfg(target_os = "linux")]
    {
        std::path::Path::new("/dev/shm").exists()
    }
    #[cfg(not(target_os = "linux"))]
    {
        false
    }
}

// ============================================================================
// Stale SHM namespace cleanup
// ============================================================================

/// Returns the parent directory where HORUS SHM namespace directories live.
///
/// - Linux: `/dev/shm/`
/// - macOS: `/tmp/`
/// - Windows: `%TEMP%`
pub fn shm_parent_dir() -> PathBuf {
    #[cfg(target_os = "linux")]
    {
        PathBuf::from("/dev/shm")
    }

    #[cfg(target_os = "macos")]
    {
        PathBuf::from("/tmp")
    }

    #[cfg(target_os = "windows")]
    {
        std::env::temp_dir()
    }

    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
    {
        PathBuf::from("/tmp")
    }
}

/// Parse a HORUS auto-generated namespace directory name.
///
/// Returns `Some((pgid, uid))` for names matching `horus_pgid{N}_uid{N}`.
/// Returns `None` for custom namespaces (e.g. `horus_my_robot`) which are
/// never auto-cleaned.
pub fn parse_namespace_pgid(dir_name: &str) -> Option<(i32, u32)> {
    let suffix = dir_name.strip_prefix("horus_pgid")?;
    let (pgid_str, rest) = suffix.split_once("_uid")?;
    let pgid: i32 = pgid_str.parse().ok()?;
    let uid: u32 = rest.parse().ok()?;
    Some((pgid, uid))
}

/// Check whether any process in the given process group is still alive.
///
/// Uses `kill(-pgid, 0)` which checks for existence without sending a signal.
/// Returns `false` if the process group doesn't exist or we lack permission to signal it.
#[cfg(unix)]
pub fn process_group_alive(pgid: i32) -> bool {
    if pgid <= 0 {
        return false;
    }
    // SAFETY: kill with signal 0 is a standard existence check.
    // Negative pid means "send to process group |pid|".
    let ret = unsafe { libc::kill(-pgid, 0) };
    if ret == 0 {
        return true;
    }
    // EPERM means the process group exists but we can't signal it — still alive.
    let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
    errno == libc::EPERM
}

#[cfg(not(unix))]
pub fn process_group_alive(_pgid: i32) -> bool {
    // On non-Unix platforms, we can't check process groups.
    // Conservatively assume alive to avoid deleting active namespaces.
    true
}

/// Result of a stale namespace cleanup operation.
#[derive(Debug, Clone)]
pub struct NamespaceCleanupResult {
    /// Number of stale namespace directories removed.
    pub removed: usize,
    /// Total bytes freed.
    pub bytes_freed: u64,
    /// Directories that were skipped (still alive, other user, etc.).
    pub skipped: usize,
    /// Errors encountered (non-fatal — we skip and continue).
    pub errors: Vec<String>,
}

/// Information about a single HORUS namespace directory.
#[derive(Debug, Clone)]
pub struct NamespaceInfo {
    /// Full path to the namespace directory.
    pub path: PathBuf,
    /// Directory name (e.g. `horus_pgid12345_uid1000`).
    pub dir_name: String,
    /// Parsed PGID, if this is an auto-generated namespace.
    pub pgid: Option<i32>,
    /// Parsed UID, if this is an auto-generated namespace.
    pub uid: Option<u32>,
    /// Whether the owning process group is still alive.
    pub alive: bool,
    /// Total size in bytes.
    pub size_bytes: u64,
    /// Number of files inside.
    pub file_count: usize,
}

/// Scan the SHM parent directory and remove stale HORUS namespace directories.
///
/// A namespace is considered stale when:
/// 1. Its name matches `horus_pgid{N}_uid{N}` (auto-generated, not custom)
/// 2. Its UID matches the current user (we never touch other users' dirs)
/// 3. Its process group is no longer alive
/// 4. It's not the current process's namespace
///
/// This is safe to call at any time. It only removes directories that belong
/// to dead process groups owned by the current user.
pub fn cleanup_stale_namespaces() -> NamespaceCleanupResult {
    let mut result = NamespaceCleanupResult {
        removed: 0,
        bytes_freed: 0,
        skipped: 0,
        errors: Vec::new(),
    };

    let parent = shm_parent_dir();
    if !parent.exists() {
        return result;
    }

    let current_ns = format!("horus_{}", shm_namespace());

    #[cfg(unix)]
    // SAFETY: getuid() is an always-succeeding, async-signal-safe POSIX syscall.
    let current_uid = unsafe { libc::getuid() };
    #[cfg(not(unix))]
    let current_uid: u32 = 0;

    let entries = match std::fs::read_dir(&parent) {
        Ok(e) => e,
        Err(e) => {
            result
                .errors
                .push(format!("Failed to read {}: {}", parent.display(), e));
            return result;
        }
    };

    for entry in entries.flatten() {
        let dir_name = entry.file_name().to_string_lossy().to_string();

        // Only look at horus_pgid* directories
        if !dir_name.starts_with("horus_pgid") {
            continue;
        }

        // Skip the current process's namespace
        if dir_name == current_ns {
            result.skipped += 1;
            continue;
        }

        // Parse PGID and UID from the directory name
        let (pgid, uid) = match parse_namespace_pgid(&dir_name) {
            Some(parsed) => parsed,
            None => {
                result.skipped += 1;
                continue;
            }
        };

        // Only clean our own user's directories
        if uid != current_uid {
            result.skipped += 1;
            continue;
        }

        // Check if the process group is still alive
        if process_group_alive(pgid) {
            result.skipped += 1;
            continue;
        }

        // Process group is dead — remove the stale namespace
        let dir_path = entry.path();
        let size = dir_size_bytes(&dir_path);

        match std::fs::remove_dir_all(&dir_path) {
            Ok(()) => {
                log::info!(
                    "Removed stale SHM namespace: {} (freed {})",
                    dir_name,
                    format_bytes_compact(size)
                );
                result.removed += 1;
                result.bytes_freed += size;
            }
            Err(e) => {
                result
                    .errors
                    .push(format!("Failed to remove {}: {}", dir_path.display(), e));
            }
        }
    }

    if result.removed > 0 {
        log::info!(
            "SHM cleanup: removed {} stale namespace(s), freed {}",
            result.removed,
            format_bytes_compact(result.bytes_freed)
        );
    }

    result
}

/// List all HORUS namespace directories in the SHM parent.
///
/// Returns info about every `horus_*` directory, including whether each is
/// alive or stale. Used by `horus clean --shm` for verbose display.
pub fn list_all_horus_namespaces() -> Vec<NamespaceInfo> {
    let parent = shm_parent_dir();
    if !parent.exists() {
        return Vec::new();
    }

    let entries = match std::fs::read_dir(&parent) {
        Ok(e) => e,
        Err(_) => return Vec::new(),
    };

    let mut namespaces = Vec::new();

    for entry in entries.flatten() {
        let dir_name = entry.file_name().to_string_lossy().to_string();

        // Only look at horus_* directories
        if !dir_name.starts_with("horus_") {
            continue;
        }

        let path = entry.path();
        if !path.is_dir() {
            continue;
        }

        let (pgid, uid, alive) = match parse_namespace_pgid(&dir_name) {
            Some((pgid, _uid)) => (Some(pgid), Some(_uid), process_group_alive(pgid)),
            None => (None, None, true), // Custom namespaces assumed alive
        };

        let size_bytes = dir_size_bytes(&path);
        let file_count = dir_file_count(&path);

        namespaces.push(NamespaceInfo {
            path,
            dir_name,
            pgid,
            uid,
            alive,
            size_bytes,
            file_count,
        });
    }

    namespaces
}

/// Recursively compute the total size of a directory in bytes.
fn dir_size_bytes(path: &std::path::Path) -> u64 {
    let mut size = 0u64;
    if let Ok(entries) = std::fs::read_dir(path) {
        for entry in entries.flatten() {
            let p = entry.path();
            if p.is_file() {
                if let Ok(meta) = p.metadata() {
                    size += meta.len();
                }
            } else if p.is_dir() {
                size += dir_size_bytes(&p);
            }
        }
    }
    size
}

/// Recursively count files in a directory.
fn dir_file_count(path: &std::path::Path) -> usize {
    let mut count = 0;
    if let Ok(entries) = std::fs::read_dir(path) {
        for entry in entries.flatten() {
            let p = entry.path();
            if p.is_file() {
                count += 1;
            } else if p.is_dir() {
                count += dir_file_count(&p);
            }
        }
    }
    count
}

/// Format bytes in a compact human-readable form.
fn format_bytes_compact(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = 1024 * KB;
    const GB: u64 = 1024 * MB;

    if bytes >= GB {
        format!("{:.1} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.1} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.1} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} B", bytes)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_shm_paths_are_valid() {
        let base = shm_base_dir();
        assert!(!base.as_os_str().is_empty());

        let topics = shm_topics_dir();
        assert!(topics.starts_with(&base));

        let nodes = shm_nodes_dir();
        assert!(nodes.starts_with(&base));
    }

    /// shm_base_dir() must always end with a component that starts with "horus_".
    /// The namespace is now always present (never bare "horus").
    #[test]
    fn test_shm_base_dir_starts_with_horus_underscore() {
        let base = shm_base_dir();
        let dir_name = base
            .file_name()
            .expect("base dir should have a file_name component")
            .to_string_lossy();
        assert!(
            dir_name.starts_with("horus_"),
            "shm_base_dir() last component must start with 'horus_', got '{}'",
            dir_name
        );
    }

    /// sanitize_namespace replaces non-alphanumeric/underscore chars with '_'.
    #[test]
    fn test_sanitize_namespace() {
        assert_eq!(sanitize_namespace("my-robot/1!"), "my_robot_1_");
        assert_eq!(sanitize_namespace("robot_01"), "robot_01");
        assert_eq!(sanitize_namespace("hello world"), "hello_world");
        assert_eq!(sanitize_namespace("abc123_XYZ"), "abc123_XYZ");
    }

    /// generate_namespace() returns a non-empty string in all cases.
    #[test]
    fn test_generate_namespace_is_non_empty() {
        // generate_namespace reads live env vars; unset both to exercise auto-gen path.
        // We can't safely unset env vars in a parallel test environment, so we only
        // verify the contract (non-empty) rather than the specific auto-gen format.
        let ns = generate_namespace();
        assert!(
            !ns.is_empty(),
            "generate_namespace() must never return an empty string"
        );
    }

    /// generate_namespace() with HORUS_NAMESPACE set returns the sanitized value.
    #[test]
    fn test_generate_namespace_uses_horus_namespace_env() {
        // Set both vars; HORUS_NAMESPACE must win.
        // SAFETY: setting env vars is process-wide. This test must not run in
        // parallel with other tests that read HORUS_NAMESPACE.  Since each Rust
        // test binary runs its tests sequentially by default within a single
        // thread pool, and the env changes are bracketed, this is acceptable.
        // In CI, use `--test-threads=1` if parallel env-var tests are added.
        let prev = std::env::var("HORUS_NAMESPACE").ok();
        let prev_legacy = std::env::var("HORUS_SHM_NAMESPACE").ok();

        unsafe {
            std::env::set_var("HORUS_NAMESPACE", "test_robot_A");
            std::env::remove_var("HORUS_SHM_NAMESPACE");
        }
        let ns = generate_namespace();

        // Restore
        // SAFETY: env var mutation is process-wide; test is single-threaded and
        // restores original values before returning.
        unsafe {
            match prev {
                Some(v) => std::env::set_var("HORUS_NAMESPACE", v),
                None => std::env::remove_var("HORUS_NAMESPACE"),
            }
            match prev_legacy {
                Some(v) => std::env::set_var("HORUS_SHM_NAMESPACE", v),
                None => std::env::remove_var("HORUS_SHM_NAMESPACE"),
            }
        }

        assert_eq!(ns, "test_robot_A");
    }

    /// generate_namespace() falls back to HORUS_SHM_NAMESPACE when HORUS_NAMESPACE unset.
    #[test]
    fn test_generate_namespace_legacy_fallback() {
        let prev = std::env::var("HORUS_NAMESPACE").ok();
        let prev_legacy = std::env::var("HORUS_SHM_NAMESPACE").ok();

        // SAFETY: env var mutation is process-wide; test is single-threaded and
        // restores original values before returning.
        unsafe {
            std::env::remove_var("HORUS_NAMESPACE");
            std::env::set_var("HORUS_SHM_NAMESPACE", "legacy_ns");
        }
        let ns = generate_namespace();

        // SAFETY: restoring env vars saved above; same single-threaded test context.
        unsafe {
            match prev {
                Some(v) => std::env::set_var("HORUS_NAMESPACE", v),
                None => std::env::remove_var("HORUS_NAMESPACE"),
            }
            match prev_legacy {
                Some(v) => std::env::set_var("HORUS_SHM_NAMESPACE", v),
                None => std::env::remove_var("HORUS_SHM_NAMESPACE"),
            }
        }

        assert_eq!(ns, "legacy_ns");
    }

    /// generate_namespace() auto-generates a non-empty namespace when both env vars are absent.
    #[test]
    fn test_generate_namespace_auto_when_no_env() {
        let prev = std::env::var("HORUS_NAMESPACE").ok();
        let prev_legacy = std::env::var("HORUS_SHM_NAMESPACE").ok();

        // SAFETY: env var mutation is process-wide; test is single-threaded and
        // restores original values before returning.
        unsafe {
            std::env::remove_var("HORUS_NAMESPACE");
            std::env::remove_var("HORUS_SHM_NAMESPACE");
        }
        let ns = generate_namespace();

        // SAFETY: restoring env vars saved above; same single-threaded test context.
        unsafe {
            match prev {
                Some(v) => std::env::set_var("HORUS_NAMESPACE", v),
                None => std::env::remove_var("HORUS_NAMESPACE"),
            }
            match prev_legacy {
                Some(v) => std::env::set_var("HORUS_SHM_NAMESPACE", v),
                None => std::env::remove_var("HORUS_SHM_NAMESPACE"),
            }
        }

        assert!(!ns.is_empty(), "auto-generated namespace must not be empty");
        // On Unix the format is "pgid<N>_uid<N>"; on non-Unix it's "pid<N>".
        #[cfg(unix)]
        assert!(
            ns.starts_with("pgid"),
            "auto-generated Unix namespace must start with 'pgid', got '{}'",
            ns
        );
        #[cfg(not(unix))]
        assert!(
            ns.starts_with("pid"),
            "auto-generated non-Unix namespace must start with 'pid', got '{}'",
            ns
        );
    }

    /// Two different env var values produce different shm_base_dir paths.
    #[test]
    fn test_different_namespaces_give_different_paths() {
        let ns_a = format!("horus_{}", sanitize_namespace("app_a"));
        let ns_b = format!("horus_{}", sanitize_namespace("app_b"));
        assert_ne!(
            ns_a, ns_b,
            "different app names must produce different SHM directory names"
        );
    }

    // ========================================================================
    // Stale namespace cleanup tests
    // ========================================================================

    #[test]
    fn test_parse_namespace_pgid_valid() {
        assert_eq!(
            parse_namespace_pgid("horus_pgid12345_uid1000"),
            Some((12345, 1000))
        );
        assert_eq!(parse_namespace_pgid("horus_pgid1_uid0"), Some((1, 0)));
        assert_eq!(
            parse_namespace_pgid("horus_pgid999999999_uid65534"),
            Some((999999999, 65534))
        );
    }

    #[test]
    fn test_parse_namespace_pgid_custom_namespaces() {
        // Custom namespaces should return None — never auto-cleaned
        assert_eq!(parse_namespace_pgid("horus_my_robot"), None);
        assert_eq!(parse_namespace_pgid("horus_test"), None);
        assert_eq!(parse_namespace_pgid("horus_"), None);
        assert_eq!(parse_namespace_pgid("not_horus_pgid1_uid1"), None);
    }

    #[test]
    fn test_parse_namespace_pgid_malformed() {
        assert_eq!(parse_namespace_pgid("horus_pgidabc_uid1000"), None);
        assert_eq!(parse_namespace_pgid("horus_pgid123_uidabc"), None);
        assert_eq!(parse_namespace_pgid("horus_pgid_uid"), None);
        assert_eq!(parse_namespace_pgid("horus_pgid123"), None);
        assert_eq!(parse_namespace_pgid(""), None);
    }

    #[cfg(unix)]
    #[test]
    fn test_process_group_alive_current() {
        // Our own process group should be alive
        // SAFETY: getpgrp() is an always-succeeding, async-signal-safe POSIX syscall.
        let pgid = unsafe { libc::getpgrp() };
        assert!(
            process_group_alive(pgid),
            "current process group should be alive"
        );
    }

    #[cfg(unix)]
    #[test]
    fn test_process_group_alive_nonexistent() {
        // PGID 999999999 is extremely unlikely to exist
        assert!(
            !process_group_alive(999999999),
            "nonexistent process group should not be alive"
        );
    }

    #[test]
    fn test_process_group_alive_invalid_pgid() {
        assert!(!process_group_alive(0));
        assert!(!process_group_alive(-1));
    }

    #[test]
    fn test_format_bytes_compact() {
        assert_eq!(format_bytes_compact(0), "0 B");
        assert_eq!(format_bytes_compact(512), "512 B");
        assert_eq!(format_bytes_compact(1024), "1.0 KB");
        assert_eq!(format_bytes_compact(1536), "1.5 KB");
        assert_eq!(format_bytes_compact(1048576), "1.0 MB");
        assert_eq!(format_bytes_compact(1073741824), "1.0 GB");
    }

    #[test]
    fn test_shm_parent_dir_exists() {
        let parent = shm_parent_dir();
        // On any normal system, /dev/shm or /tmp should exist
        assert!(
            parent.exists(),
            "shm_parent_dir() should point to an existing directory: {}",
            parent.display()
        );
    }

    #[test]
    fn test_cleanup_stale_namespaces_with_simulated_stale_dir() {
        // Create a fake stale namespace directory with a dead PGID
        let parent = shm_parent_dir();
        // Use a unique dead PGID based on timestamp to avoid interference from parallel tests
        let unique_suffix = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
            % 100_000_000;
        let dead_pgid = 900_000_000 + unique_suffix as i32;
        #[cfg(unix)]
        // SAFETY: getuid() is an always-succeeding, async-signal-safe POSIX syscall.
        let uid = unsafe { libc::getuid() };
        #[cfg(not(unix))]
        let uid: u32 = 0;

        let stale_dir_name = format!("horus_pgid{}_uid{}", dead_pgid, uid);
        let stale_path = parent.join(&stale_dir_name);

        // Ensure we start clean
        let _ = std::fs::remove_dir_all(&stale_path);

        // Create the fake stale directory with a file inside
        std::fs::create_dir_all(&stale_path).expect("create stale dir");
        std::fs::write(stale_path.join("test_topic"), b"stale data").expect("write stale file");
        assert!(stale_path.exists());

        // Run cleanup — it should remove this stale directory
        let result = cleanup_stale_namespaces();
        assert!(
            result.removed >= 1,
            "Should remove at least our stale directory, removed={}",
            result.removed
        );
        assert!(
            !stale_path.exists(),
            "Stale directory should have been removed"
        );
        assert!(result.bytes_freed > 0, "Should have freed some bytes");
    }

    #[test]
    fn test_cleanup_does_not_remove_live_namespaces() {
        // Create a directory matching the current process's PGID — should NOT be removed
        let parent = shm_parent_dir();
        #[cfg(unix)]
        // SAFETY: getpgrp() is an always-succeeding, async-signal-safe POSIX syscall.
        let pgid = unsafe { libc::getpgrp() };
        #[cfg(not(unix))]
        let pgid: i32 = std::process::id() as i32;
        #[cfg(unix)]
        // SAFETY: getuid() is an always-succeeding, async-signal-safe POSIX syscall.
        let uid = unsafe { libc::getuid() };
        #[cfg(not(unix))]
        let uid: u32 = 0;

        let live_dir_name = format!("horus_pgid{}_uid{}", pgid, uid);
        let live_path = parent.join(&live_dir_name);

        // Might already exist from the current process's namespace
        let created = if !live_path.exists() {
            std::fs::create_dir_all(&live_path).expect("create live dir");
            true
        } else {
            false
        };

        let result = cleanup_stale_namespaces();

        // The live directory should NOT have been removed
        assert!(
            live_path.exists(),
            "Live process group directory should not be removed"
        );

        // Clean up if we created it
        if created {
            let _ = std::fs::remove_dir_all(&live_path);
        }

        // No errors should have occurred for this directory
        let _ = result; // result is valid
    }

    #[test]
    fn test_list_all_horus_namespaces() {
        let namespaces = list_all_horus_namespaces();
        // Should return a list (possibly empty, but shouldn't panic)
        for ns in &namespaces {
            assert!(ns.dir_name.starts_with("horus_"));
            // Note: path may not exist if another test cleaned it up concurrently
        }
    }

    #[test]
    fn test_dir_size_bytes_and_file_count() {
        use std::io::Write;
        let tmp = std::env::temp_dir().join("horus_test_dir_size");
        let _ = std::fs::remove_dir_all(&tmp);
        std::fs::create_dir_all(&tmp).unwrap();

        // Create a file with known size
        let mut f = std::fs::File::create(tmp.join("file1.dat")).unwrap();
        f.write_all(&[0u8; 100]).unwrap();
        drop(f);

        // Create a subdirectory with another file
        std::fs::create_dir_all(tmp.join("sub")).unwrap();
        let mut f2 = std::fs::File::create(tmp.join("sub/file2.dat")).unwrap();
        f2.write_all(&[0u8; 200]).unwrap();
        drop(f2);

        let size = dir_size_bytes(&tmp);
        assert_eq!(size, 300, "Total size should be 300 bytes");

        let count = dir_file_count(&tmp);
        assert_eq!(count, 2, "Should have 2 files");

        let _ = std::fs::remove_dir_all(&tmp);
    }
}
