//! Running-process enumeration for the non-Steam detection fallback (D6/D7):
//! native adapters only — `/proc` on Linux, Toolhelp on Windows — so there is no
//! `sysinfo` dependency and the audit surface stays small.
//!
//! This module is *just the OS edge*: it returns the list of running executable
//! paths/names. The trustworthy part — turning that list into a game via the
//! user's explicit mappings and the launcher denylist — is the pure
//! [`match_processes`](super::match_processes), unit-tested in the parent module.

/// Enumerate the executables of currently-running processes as paths/basenames.
/// Best-effort: processes we can't introspect (other users') are skipped rather
/// than erroring. The result is fed to [`match_processes`](super::match_processes),
/// which normalizes each entry to a basename before matching.
pub fn running_executables() -> Vec<String> {
    #[cfg(target_os = "linux")]
    {
        linux_proc_executables()
    }
    #[cfg(windows)]
    {
        windows_toolhelp_executables()
    }
    #[cfg(not(any(target_os = "linux", windows)))]
    {
        Vec::new()
    }
}

#[cfg(target_os = "linux")]
fn linux_proc_executables() -> Vec<String> {
    let mut out = Vec::new();
    let Ok(entries) = std::fs::read_dir("/proc") else {
        return out;
    };
    for entry in entries.flatten() {
        let name = entry.file_name();
        let Some(name) = name.to_str() else { continue };
        // Only numeric entries are processes.
        if !name.bytes().all(|b| b.is_ascii_digit()) {
            continue;
        }
        let proc_dir = entry.path();
        // Prefer the real exe path (full, untruncated); fall back to `comm`, which
        // is readable for all processes but truncated to 15 bytes.
        if let Ok(exe) = std::fs::read_link(proc_dir.join("exe"))
            && let Some(s) = exe.to_str()
        {
            out.push(s.to_string());
            continue;
        }
        if let Ok(comm) = std::fs::read_to_string(proc_dir.join("comm")) {
            let trimmed = comm.trim();
            if !trimmed.is_empty() {
                out.push(trimmed.to_string());
            }
        }
    }
    out
}

#[cfg(windows)]
fn windows_toolhelp_executables() -> Vec<String> {
    use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE};
    use windows_sys::Win32::System::Diagnostics::ToolHelp::{
        CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
        TH32CS_SNAPPROCESS,
    };

    let mut out = Vec::new();
    // SAFETY: standard Toolhelp snapshot of all processes; handle checked below.
    let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
    if snapshot == INVALID_HANDLE_VALUE {
        return out;
    }
    let mut entry: PROCESSENTRY32W = unsafe { std::mem::zeroed() };
    entry.dwSize = std::mem::size_of::<PROCESSENTRY32W>() as u32;
    // SAFETY: entry is zeroed with dwSize set, as Process32FirstW requires.
    let mut ok = unsafe { Process32FirstW(snapshot, &mut entry) };
    while ok != 0 {
        // szExeFile is a NUL-terminated UTF-16 array (the basename, e.g. game.exe).
        let end = entry
            .szExeFile
            .iter()
            .position(|&c| c == 0)
            .unwrap_or(entry.szExeFile.len());
        let name = String::from_utf16_lossy(&entry.szExeFile[..end]);
        if !name.is_empty() {
            out.push(name);
        }
        // SAFETY: same valid snapshot + entry struct.
        ok = unsafe { Process32NextW(snapshot, &mut entry) };
    }
    // SAFETY: snapshot handle came from CreateToolhelp32Snapshot above.
    unsafe { CloseHandle(snapshot) };
    out
}

#[cfg(test)]
mod tests {
    #[cfg(target_os = "linux")]
    #[test]
    fn enumerates_at_least_this_process() {
        // The test runner itself is a process, so /proc enumeration must be
        // non-empty and include something that normalizes to our own exe basename.
        let exes = super::running_executables();
        assert!(
            !exes.is_empty(),
            "expected to see running processes via /proc"
        );
        // Our own /proc/self/exe basename should appear among them.
        let me = std::fs::read_link("/proc/self/exe")
            .ok()
            .and_then(|p| p.file_name().map(|f| f.to_string_lossy().into_owned()));
        if let Some(me) = me {
            let me_norm = super::super::normalize_exe(&me);
            assert!(
                exes.iter()
                    .any(|e| super::super::normalize_exe(e) == me_norm),
                "running list should include our own executable {me_norm:?}"
            );
        }
    }
}
