//! Background system resource sampler for replay job monitoring.

use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use crate::process_watcher::sample_system_gpu_mem;

/// A timestamped data point in a monitoring series.
#[derive(Clone)]
pub struct SeriesPoint<T> {
    pub timestamp_ms: u64,
    pub data: T,
}

/// A typed time series of monitoring data.
#[derive(Clone, Default)]
pub struct SeriesData<T> {
    pub points: Vec<SeriesPoint<T>>,
}

impl<T> SeriesData<T> {
    pub fn new() -> Self {
        SeriesData { points: Vec::new() }
    }

    pub fn push(&mut self, timestamp_ms: u64, data: T) {
        self.points.push(SeriesPoint { timestamp_ms, data });
    }

    pub fn is_empty(&self) -> bool {
        self.points.is_empty()
    }
}

impl SeriesData<f64> {
    /// Iterate over points as (time_seconds, value) pairs.
    pub fn iter_secs(&self) -> impl Iterator<Item = (f64, f64)> + '_ {
        self.points
            .iter()
            .map(|p| (p.timestamp_ms as f64 / 1000.0, p.data))
    }

    /// Maximum data value, or 0.0 if the series is empty.
    pub fn max_value(&self) -> f64 {
        self.points.iter().map(|p| p.data).fold(0.0f64, f64::max)
    }
}

/// All monitoring data collected during a replay job.
pub struct MonitorData {
    /// CPU usage, 0–100%.
    pub cpu_pct: SeriesData<f64>,
    /// Available system memory in MiB.
    pub mem_available_mb: SeriesData<f64>,
    /// Swap currently in use, in MiB.
    pub swap_used_mb: SeriesData<f64>,
    /// Number of traces currently being replayed.
    pub active_replays: SeriesData<f64>,
    /// Number of traces currently being downloaded.
    pub active_downloads: SeriesData<f64>,
    /// GPU memory in use, in MiB.
    pub gpu_mem_mb: SeriesData<f64>,
    /// GPU actual clock frequency in MHz.
    pub gpu_freq_mhz: SeriesData<f64>,
    /// GPU maximum clock frequency in MHz, read once at startup for chart scaling.
    pub gpu_freq_max_mhz: Option<u32>,
    /// GPU memory clock frequency in MHz.
    pub gpu_mem_freq_mhz: SeriesData<f64>,
    /// GPU busy percentage, from the freedreno perf reader thread.
    pub gpu_busy_pct: SeriesData<f64>,
    /// CPU temperature in °C.
    pub cpu_temp_c: SeriesData<f64>,
    /// GPU temperature in °C.
    pub gpu_temp_c: SeriesData<f64>,
}

impl MonitorData {
    pub fn new(gpu_freq_max_mhz: Option<u32>) -> Self {
        MonitorData {
            cpu_pct: SeriesData::new(),
            mem_available_mb: SeriesData::new(),
            swap_used_mb: SeriesData::new(),
            active_replays: SeriesData::new(),
            active_downloads: SeriesData::new(),
            gpu_mem_mb: SeriesData::new(),
            gpu_freq_mhz: SeriesData::new(),
            gpu_freq_max_mhz,
            gpu_mem_freq_mhz: SeriesData::new(),
            gpu_busy_pct: SeriesData::new(),
            cpu_temp_c: SeriesData::new(),
            gpu_temp_c: SeriesData::new(),
        }
    }

    /// Total elapsed time of the monitoring session in milliseconds.
    pub fn duration_ms(&self) -> u64 {
        self.cpu_pct
            .points
            .iter()
            .chain(self.gpu_busy_pct.points.iter())
            .map(|p| p.timestamp_ms)
            .max()
            .unwrap_or(0)
    }
}

impl Default for MonitorData {
    fn default() -> Self {
        MonitorData::new(None)
    }
}

pub struct MonitorTimeRange {
    pub start: u64,
    pub end: u64,
}
impl MonitorTimeRange {
    pub fn new(start: Duration, end: Duration) -> Self {
        Self {
            start: start.as_millis() as u64,
            end: end.as_millis() as u64,
        }
    }
}

/// One trace's timeline: when its download and replay phases started and ended.
#[derive(Clone, Debug)]
pub struct TraceEvent {
    pub trace_name: String,
    pub download_start_ms: u64,
    pub download_end_ms: u64,
    pub replay_start_ms: u64,
    pub replay_end_ms: u64,
    /// Whether the trace passed (checksum matched).
    pub passed: bool,
}

/// Detected GPU sysfs paths, filled in at construction time.
struct GpuPaths {
    /// For freedreno: path to the `gem` debugfs file.
    freedreno_gem: Option<PathBuf>,
    /// For freedreno: path to the `perf` debugfs file (streaming %BUSY blocks).
    freedreno_perf: Option<PathBuf>,
    /// For freedreno: path to `/sys/class/devfreq/NAME/cur_freq` (Hz, decimal).
    freedreno_cur_freq: Option<PathBuf>,
    /// For freedreno: max frequency in MHz, read once at detection time.
    freedreno_max_freq_mhz: Option<u32>,
    /// For amdgpu: path to hwmon `freq1_input` (GFX clock, Hz).
    amdgpu_freq1_input: Option<PathBuf>,
    /// For amdgpu: path to hwmon `freq2_input` (memory clock, Hz).
    amdgpu_freq2_input: Option<PathBuf>,
    /// For amdgpu: max GFX frequency in MHz, read once at detection time.
    amdgpu_max_freq_mhz: Option<u32>,
    /// For i915: path to `i915_gem_objects`.
    i915_gem_objects: Option<PathBuf>,
    /// For i915: path to `i915_frequency_info`.
    i915_frequency_info: Option<PathBuf>,
    /// Path to CPU thermal zone `temp` file (millidegrees Celsius).
    cpu_temp_path: Option<PathBuf>,
    /// Path to GPU thermal zone `temp` file (millidegrees Celsius).
    gpu_temp_path: Option<PathBuf>,
}

impl GpuPaths {
    fn detect() -> Self {
        let dri = std::path::Path::new("/sys/kernel/debug/dri");
        let mut freedreno_gem = None;
        let mut freedreno_perf = None;
        let mut i915_gem_objects = None;
        let mut i915_frequency_info = None;

        if let Ok(entries) = std::fs::read_dir(dri) {
            for entry in entries.flatten() {
                let path = entry.path();

                // freedreno: look for a `name` file starting with "msm "
                let name_path = path.join("name");
                if let Ok(name) = std::fs::read_to_string(&name_path) {
                    if name.starts_with("msm ") {
                        let gem = path.join("gem");
                        if gem.exists() {
                            freedreno_gem = Some(gem);
                        }
                        let perf = path.join("perf");
                        if perf.exists() {
                            freedreno_perf = Some(perf);
                        }
                    }
                }

                // i915: look for i915_gem_objects
                let gem_obj = path.join("i915_gem_objects");
                if gem_obj.exists() {
                    i915_gem_objects = Some(gem_obj);
                }

                // i915 frequency
                let freq = path.join("i915_frequency_info");
                if freq.exists() {
                    i915_frequency_info = Some(freq);
                }
            }
        }

        // freedreno devfreq frequency: walk /sys/class/devfreq and find the
        // entry whose driver/of_node/compatible contains "qcom,adreno".
        let mut freedreno_cur_freq = None;
        let mut freedreno_max_freq_mhz = None;
        if let Ok(entries) = std::fs::read_dir("/sys/class/devfreq") {
            for entry in entries.flatten() {
                let path = entry.path();
                if let Ok(bytes) = std::fs::read(path.join("device/of_node/compatible")) {
                    // compatible is a null-separated list of strings.
                    let is_adreno = bytes
                        .split(|&b| b == 0)
                        .filter_map(|s| std::str::from_utf8(s).ok())
                        .any(|s| s.contains("qcom,adreno"));
                    if is_adreno {
                        let cur = path.join("cur_freq");
                        if cur.exists() {
                            freedreno_cur_freq = Some(cur);
                        }
                        freedreno_max_freq_mhz = std::fs::read_to_string(path.join("max_freq"))
                            .ok()
                            .and_then(|s| parse_devfreq_hz(s.trim()));
                        break;
                    }
                }
            }
        }

        // amdgpu hwmon frequency: walk /sys/class/hwmon and find the entry
        // whose `name` file contains "amdgpu".
        let mut amdgpu_freq1_input = None;
        let mut amdgpu_freq2_input = None;
        let mut amdgpu_max_freq_mhz = None;
        if let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") {
            for entry in entries.flatten() {
                let path = entry.path();
                if std::fs::read_to_string(path.join("name"))
                    .map(|n| n.trim() == "amdgpu")
                    .unwrap_or(false)
                {
                    let f1 = path.join("freq1_input");
                    if f1.exists() {
                        amdgpu_freq1_input = Some(f1);
                    }
                    let f2 = path.join("freq2_input");
                    if f2.exists() {
                        amdgpu_freq2_input = Some(f2);
                    }
                    amdgpu_max_freq_mhz = std::fs::read_to_string(path.join("freq1_max"))
                        .ok()
                        .and_then(|s| parse_devfreq_hz(s.trim()));
                    break;
                }
            }
        }

        let (cpu_temp_path, gpu_temp_path) = detect_thermal_zones();

        GpuPaths {
            freedreno_gem,
            freedreno_perf,
            freedreno_cur_freq,
            freedreno_max_freq_mhz,
            amdgpu_freq1_input,
            amdgpu_freq2_input,
            amdgpu_max_freq_mhz,
            i915_gem_objects,
            i915_frequency_info,
            cpu_temp_path,
            gpu_temp_path,
        }
    }

    /// Sample GPU memory usage in bytes.
    fn sample_gpu_mem(&self) -> Option<u64> {
        if let Some(ref path) = self.freedreno_gem {
            if let Ok(content) = std::fs::read_to_string(path) {
                if let Some(v) = parse_freedreno_gem(&content) {
                    return Some(v);
                }
            }
        }
        if let Some(ref path) = self.i915_gem_objects {
            if let Ok(content) = std::fs::read_to_string(path) {
                if let Some(v) = parse_i915_gem_objects(&content) {
                    return Some(v);
                }
            }
        }
        None
    }

    /// Sample CPU and GPU temperatures in °C.
    fn sample_temps(&self) -> (Option<f32>, Option<f32>) {
        let cpu = self.cpu_temp_path.as_deref().and_then(read_millicelsius);
        let gpu = self.gpu_temp_path.as_deref().and_then(read_millicelsius);
        (cpu, gpu)
    }

    /// Sample GPU actual frequency, max frequency, and memory clock frequency, all in MHz.
    ///
    /// Returns `(gfx_mhz, max_mhz, mem_mhz)`.
    fn sample_gpu_freq(&self) -> (Option<u32>, Option<u32>, Option<u32>) {
        // Freedreno devfreq (cur_freq is in Hz)
        if let Some(ref path) = self.freedreno_cur_freq {
            if let Ok(content) = std::fs::read_to_string(path) {
                if let Some(mhz) = parse_devfreq_hz(content.trim()) {
                    return (Some(mhz), self.freedreno_max_freq_mhz, None);
                }
            }
        }
        // amdgpu hwmon (freq1_input = GFX, freq2_input = memory, both in Hz)
        if self.amdgpu_freq1_input.is_some() || self.amdgpu_freq2_input.is_some() {
            let gfx = self
                .amdgpu_freq1_input
                .as_deref()
                .and_then(|p| std::fs::read_to_string(p).ok())
                .and_then(|s| parse_devfreq_hz(s.trim()));
            let mem = self
                .amdgpu_freq2_input
                .as_deref()
                .and_then(|p| std::fs::read_to_string(p).ok())
                .and_then(|s| parse_devfreq_hz(s.trim()));
            return (gfx, self.amdgpu_max_freq_mhz, mem);
        }
        // i915
        let path = match &self.i915_frequency_info {
            Some(p) => p,
            None => return (None, None, None),
        };
        match std::fs::read_to_string(path) {
            Ok(content) => {
                let (actual, max) = parse_i915_frequency_info(&content);
                (actual, max, None)
            }
            Err(_) => (None, None, None),
        }
    }
}

/// Scan thermal zones for a CPU zone and a GPU zone.
///
/// Recognises two conventions:
/// - `type` == `"x86_pkg_temp"` → CPU; `type` containing `"gpu"` → GPU.
/// - `type` == `"acpitz"` with `device/path` == `"\_TZ_.CPUZ"` → CPU;
///   `device/path` == `"\_TZ_.GFXZ"` → GPU.
///
/// Returns paths to the `temp` files (values are in millidegrees Celsius).
fn detect_thermal_zones() -> (Option<PathBuf>, Option<PathBuf>) {
    let Ok(entries) = std::fs::read_dir("/sys/class/thermal") else {
        return (None, None);
    };
    let mut zones: Vec<_> = entries.flatten().collect();
    zones.sort_by_key(|e| e.file_name());
    let mut cpu = None;
    let mut gpu = None;
    for entry in zones {
        let path = entry.path();
        let Ok(type_str) = std::fs::read_to_string(path.join("type")) else {
            continue;
        };
        let t = type_str.trim();
        if cpu.is_none() && t == "x86_pkg_temp" {
            cpu = Some(path.join("temp"));
        } else if cpu.is_none() && t == "acpitz" {
            let acpi_path = std::fs::read_to_string(path.join("device/path")).unwrap_or_default();
            if acpi_path.trim() == r"\_TZ_.CPUZ" {
                cpu = Some(path.join("temp"));
            }
        }
        if gpu.is_none() && t.contains("gpu") {
            gpu = Some(path.join("temp"));
        } else if gpu.is_none() && t == "acpitz" {
            let acpi_path = std::fs::read_to_string(path.join("device/path")).unwrap_or_default();
            if acpi_path.trim() == r"\_TZ_.GFXZ" {
                gpu = Some(path.join("temp"));
            }
        }
    }
    (cpu, gpu)
}

/// Read a thermal zone `temp` file (millidegrees Celsius) and return degrees Celsius.
fn read_millicelsius(path: &std::path::Path) -> Option<f32> {
    let s = std::fs::read_to_string(path).ok()?;
    let mc: i32 = s.trim().parse().ok()?;
    Some(mc as f32 / 1000.0)
}

/// Parse a devfreq `cur_freq` or `max_freq` value (Hz as a decimal string) into MHz.
fn parse_devfreq_hz(s: &str) -> Option<u32> {
    s.parse::<u64>().ok().map(|hz| (hz / 1_000_000) as u32)
}

/// Parse freedreno `gem` debugfs content for total GPU memory in bytes.
/// Looks for a line like: "Total:       23 objects,  43466752 bytes"
fn parse_freedreno_gem(content: &str) -> Option<u64> {
    for line in content.lines() {
        if let Some(rest) = line.strip_prefix("Total:") {
            if let Some(bytes_part) = rest.split(',').nth(1) {
                let s = bytes_part.trim().trim_end_matches(" bytes");
                if let Ok(v) = s.parse::<u64>() {
                    return Some(v);
                }
            }
        }
    }
    None
}

/// Parse i915 `i915_gem_objects` debugfs content for total system memory in bytes.
/// Looks for a line like: "system: total:0x0000000faa710000 bytes"
fn parse_i915_gem_objects(content: &str) -> Option<u64> {
    for line in content.lines() {
        if let Some(rest) = line.strip_prefix("system: total:") {
            let hex = rest.split_whitespace().next().unwrap_or("");
            let hex = hex.trim_start_matches("0x");
            if let Ok(v) = u64::from_str_radix(hex, 16) {
                return Some(v);
            }
        }
    }
    None
}

/// Parse i915 `i915_frequency_info` debugfs content.
/// Returns (actual_mhz, max_overclocked_mhz).
/// Looks for lines like:
///   "Actual freq: 350 MHz"
///   "Max overclocked frequency: 1200MHz"
fn parse_i915_frequency_info(content: &str) -> (Option<u32>, Option<u32>) {
    let mut actual = None;
    let mut max = None;
    for line in content.lines() {
        if let Some(rest) = line.strip_prefix("Actual freq:") {
            actual = rest.trim().trim_end_matches(" MHz").parse().ok();
        }
        if let Some(rest) = line.strip_prefix("Max overclocked frequency:") {
            max = rest.trim().trim_end_matches("MHz").parse().ok();
        }
    }
    (actual, max)
}

/// Previous `/proc/stat` reading, for delta CPU usage computation.
#[derive(Default, Clone)]
struct CpuStat {
    total: u64,
    idle: u64,
}

fn read_cpu_stat() -> Option<CpuStat> {
    let content = std::fs::read_to_string("/proc/stat").ok()?;
    // First line: "cpu  user nice system idle iowait irq softirq steal guest guest_nice"
    let line = content.lines().next()?;
    let mut parts = line.split_whitespace().skip(1); // skip "cpu"
    let user: u64 = parts.next()?.parse().ok()?;
    let nice: u64 = parts.next()?.parse().ok()?;
    let system: u64 = parts.next()?.parse().ok()?;
    let idle: u64 = parts.next()?.parse().ok()?;
    let iowait: u64 = parts.next()?.parse().ok()?;
    let irq: u64 = parts.next()?.parse().ok()?;
    let softirq: u64 = parts.next()?.parse().ok()?;
    let total = user + nice + system + idle + iowait + irq + softirq;
    Some(CpuStat { total, idle })
}

fn read_meminfo() -> (u64, u64) {
    match std::fs::read_to_string("/proc/meminfo") {
        Ok(content) => parse_meminfo(&content),
        Err(_) => (0, 0),
    }
}

/// Parse `/proc/meminfo` content. Returns (available_mb, swap_used_mb).
fn parse_meminfo(content: &str) -> (u64, u64) {
    let mut available_kb: u64 = 0;
    let mut swap_total_kb: u64 = 0;
    let mut swap_free_kb: u64 = 0;
    for line in content.lines() {
        if let Some(rest) = line.strip_prefix("MemAvailable:") {
            available_kb = rest.trim().trim_end_matches(" kB").parse().unwrap_or(0);
        } else if let Some(rest) = line.strip_prefix("SwapTotal:") {
            swap_total_kb = rest.trim().trim_end_matches(" kB").parse().unwrap_or(0);
        } else if let Some(rest) = line.strip_prefix("SwapFree:") {
            swap_free_kb = rest.trim().trim_end_matches(" kB").parse().unwrap_or(0);
        }
    }
    let swap_used_kb = swap_total_kb.saturating_sub(swap_free_kb);
    (available_kb / 1024, swap_used_kb / 1024)
}

/// Shared counters incremented/decremented by replay tasks.
#[derive(Default)]
pub struct ActivityCounters {
    pub active_replays: AtomicUsize,
    pub active_downloads: AtomicUsize,
}

pub struct SystemMonitor {
    pub counters: Arc<ActivityCounters>,
    pub events: Arc<Mutex<Vec<TraceEvent>>>,
    data: Arc<Mutex<MonitorData>>,
    stop: Arc<std::sync::atomic::AtomicBool>,
    thread: Option<std::thread::JoinHandle<()>>,
}

impl SystemMonitor {
    pub fn new() -> Self {
        let counters = Arc::new(ActivityCounters::default());
        let events = Arc::new(Mutex::new(Vec::new()));
        let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
        let start = Instant::now();

        // Detect GPU paths before spawning threads so we can extract the perf
        // path for the dedicated reader thread without cloning all of GpuPaths.
        let gpu = GpuPaths::detect();
        let freedreno_perf_path = gpu.freedreno_perf.clone();
        let gpu_freq_max_mhz = gpu.freedreno_max_freq_mhz.or(gpu.amdgpu_max_freq_mhz);
        let data = Arc::new(Mutex::new(MonitorData::new(gpu_freq_max_mhz)));

        // Spawn a dedicated thread that blocks on the streaming `perf` debugfs
        // file. Each percentage line is a ~1 second sample; timestamp it at
        // the moment it is read and push it directly to data.gpu_busy_pct.
        if let Some(perf_path) = freedreno_perf_path {
            log::debug!("freedreno GPU busy: reading from {perf_path:?}");
            let data_t = data.clone();
            let stop_t = stop.clone();
            let start_t = start;
            std::thread::spawn(move || {
                use std::io::BufRead;
                let file = match std::fs::File::open(&perf_path) {
                    Ok(f) => f,
                    Err(e) => {
                        log::warn!("Failed to open GPU perf file {perf_path:?}: {e}");
                        return;
                    }
                };
                let reader = std::io::BufReader::new(file);
                for line in reader.lines() {
                    if stop_t.load(Ordering::Relaxed) {
                        break;
                    }
                    let line = match line {
                        Ok(l) => l,
                        Err(e) => {
                            log::warn!("Error reading GPU perf file: {e}");
                            break;
                        }
                    };
                    let trimmed = line.trim();
                    if let Some(pct) = trimmed
                        .strip_suffix('%')
                        .and_then(|s| s.parse::<f64>().ok())
                    {
                        let ts = start_t.elapsed().as_millis() as u64;
                        data_t.lock().unwrap().gpu_busy_pct.push(ts, pct);
                    }
                    // "%BUSY" header lines are skipped (no else branch needed)
                }
            });
        } else {
            log::debug!("freedreno GPU busy: no perf file found under /sys/kernel/debug/dri/");
        }

        let counters_t = counters.clone();
        let data_t = data.clone();
        let stop_t = stop.clone();
        let start_t = start;

        let thread = std::thread::spawn(move || {
            let mut prev_cpu = read_cpu_stat().unwrap_or_default();

            loop {
                std::thread::sleep(Duration::from_secs(1));
                if stop_t.load(Ordering::Relaxed) {
                    break;
                }

                let timestamp_ms = start_t.elapsed().as_millis() as u64;
                let cur_cpu = read_cpu_stat().unwrap_or_default();
                let cpu_pct = {
                    let dtotal = cur_cpu.total.saturating_sub(prev_cpu.total);
                    let didle = cur_cpu.idle.saturating_sub(prev_cpu.idle);
                    if dtotal > 0 {
                        (100.0 * (dtotal - didle) as f64) / dtotal as f64
                    } else {
                        0.0
                    }
                };
                prev_cpu = cur_cpu;

                let (mem_available_mb, swap_used_mb) = read_meminfo();
                let gpu_mem_bytes =
                    gpu.sample_gpu_mem()
                        .or_else(|| match sample_system_gpu_mem() {
                            Some(peak) => {
                                if peak.vram_bytes.is_none() && peak.sys_bytes.is_none() {
                                    None
                                } else {
                                    Some(peak.vram_bytes.unwrap_or(0) + peak.sys_bytes.unwrap_or(0))
                                }
                            }
                            None => None,
                        });
                let (gpu_freq_mhz, _, gpu_mem_freq_mhz) = gpu.sample_gpu_freq();
                let (cpu_temp_c, gpu_temp_c) = gpu.sample_temps();

                let mut data = data_t.lock().unwrap();
                data.cpu_pct.push(timestamp_ms, cpu_pct);
                data.mem_available_mb
                    .push(timestamp_ms, mem_available_mb as f64);
                data.swap_used_mb.push(timestamp_ms, swap_used_mb as f64);
                data.active_replays.push(
                    timestamp_ms,
                    counters_t.active_replays.load(Ordering::Relaxed) as f64,
                );
                data.active_downloads.push(
                    timestamp_ms,
                    counters_t.active_downloads.load(Ordering::Relaxed) as f64,
                );
                if let Some(bytes) = gpu_mem_bytes {
                    data.gpu_mem_mb
                        .push(timestamp_ms, bytes as f64 / (1024.0 * 1024.0));
                }
                if let Some(mhz) = gpu_freq_mhz {
                    data.gpu_freq_mhz.push(timestamp_ms, mhz as f64);
                }
                if let Some(mhz) = gpu_mem_freq_mhz {
                    data.gpu_mem_freq_mhz.push(timestamp_ms, mhz as f64);
                }
                if let Some(c) = cpu_temp_c {
                    data.cpu_temp_c.push(timestamp_ms, c as f64);
                }
                if let Some(c) = gpu_temp_c {
                    data.gpu_temp_c.push(timestamp_ms, c as f64);
                }
            }
        });

        SystemMonitor {
            counters,
            events,
            data,
            stop,
            thread: Some(thread),
        }
    }

    /// Stop the background sampler and return all collected data.
    pub fn stop(mut self) -> (MonitorData, Vec<TraceEvent>) {
        self.stop.store(true, Ordering::Relaxed);
        if let Some(t) = self.thread.take() {
            let _ = t.join();
        }
        let data = std::mem::take(&mut *self.data.lock().unwrap());
        let events = std::mem::take(&mut *self.events.lock().unwrap());
        (data, events)
    }
}

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

    #[test]
    fn test_freedreno_gem_parse() {
        let content = "Pinned: 0 objects, 0 bytes\nTotal:       23 objects,  43466752 bytes\n";
        assert_eq!(parse_freedreno_gem(content), Some(43466752));
    }

    #[test]
    fn test_i915_gem_objects_parse() {
        let content = "system: total:0x0000000faa710000 bytes, shrinkable:0x0 bytes, ...\n";
        assert_eq!(parse_i915_gem_objects(content), Some(0x0000000faa710000u64));
    }

    #[test]
    fn test_i915_frequency_parse() {
        let content = "Actual freq: 350 MHz\nMax overclocked frequency: 1200MHz\n";
        assert_eq!(parse_i915_frequency_info(content), (Some(350), Some(1200)));
    }

    #[test]
    fn test_meminfo_parse() {
        let content =
            "MemAvailable:    2048000 kB\nSwapTotal:    1048576 kB\nSwapFree:      524288 kB\n";
        assert_eq!(parse_meminfo(content), (2000, 512));
    }

    #[test]
    fn test_devfreq_hz_parse() {
        assert_eq!(parse_devfreq_hz("700000000"), Some(700));
        assert_eq!(parse_devfreq_hz("1000000000"), Some(1000));
        assert_eq!(parse_devfreq_hz(""), None);
    }
}
