use std::collections::BTreeMap;
use sysinfo::System;

/// The acceleration backend for inference speed estimation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum GpuBackend {
    Cuda,
    Metal,
    Rocm,
    Vulkan, // AMD/other GPUs without ROCm (e.g. Windows AMD, older AMD)
    Sycl,   // Intel oneAPI
    CpuArm,
    CpuX86,
    Ascend,
}

impl GpuBackend {
    pub fn label(&self) -> &'static str {
        match self {
            GpuBackend::Cuda => "CUDA",
            GpuBackend::Metal => "Metal",
            GpuBackend::Rocm => "ROCm",
            GpuBackend::Vulkan => "Vulkan",
            GpuBackend::Sycl => "SYCL",
            GpuBackend::CpuArm => "CPU (ARM)",
            GpuBackend::CpuX86 => "CPU (x86)",
            GpuBackend::Ascend => "NPU (Ascend)",
        }
    }
}

/// Information about a single detected GPU.
#[derive(Debug, Clone, serde::Serialize)]
pub struct GpuInfo {
    pub name: String,
    pub vram_gb: Option<f64>,
    pub backend: GpuBackend,
    pub count: u32, // >1 for same-model multi-GPU (e.g. 2x RTX 4090)
    pub unified_memory: bool,
}

#[derive(Debug, Clone, serde::Serialize)]
pub struct SystemSpecs {
    pub total_ram_gb: f64,
    pub available_ram_gb: f64,
    pub total_cpu_cores: usize,
    pub cpu_name: String,
    pub has_gpu: bool,
    pub gpu_vram_gb: Option<f64>,
    /// Total VRAM across all same-model GPUs (e.g., 48GB for 2x RTX 3090).
    /// For multi-GPU inference backends (llama.cpp, vLLM), models can be split
    /// across cards, so we use total VRAM for fit scoring.
    pub total_gpu_vram_gb: Option<f64>,
    /// On Apple Silicon (unified memory), how much of the shared pool the GPU
    /// may actually wire — from Metal's `recommendedMaxWorkingSetSize`. macOS
    /// caps this well below total RAM. `None` on other platforms or when the
    /// query is unavailable. Distinct from `gpu_vram_gb`, which for unified
    /// memory reports the *total* pool.
    pub gpu_available_gb: Option<f64>,
    pub gpu_name: Option<String>,
    pub gpu_count: u32,
    pub unified_memory: bool,
    pub backend: GpuBackend,
    /// All detected GPUs (may span different vendors/backends).
    pub gpus: Vec<GpuInfo>,
    /// True when running in multi-node cluster mode (e.g. DGX Spark cluster).
    pub cluster_mode: bool,
    /// Number of nodes in the cluster (0 or 1 = single machine).
    pub cluster_node_count: u32,
}

impl SystemSpecs {
    pub fn detect() -> Self {
        let mut sys = System::new_all();
        sys.refresh_all();

        let total_ram_bytes = sys.total_memory();
        let available_ram_bytes = sys.available_memory();
        let total_ram_gb = total_ram_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
        let available_ram_gb = if available_ram_bytes == 0 && total_ram_bytes > 0 {
            // sysinfo may fail to report available memory on some platforms
            // (e.g. macOS Tahoe / newer macOS versions). Try fallbacks.
            Self::available_ram_fallback(&sys, total_ram_bytes, total_ram_gb)
        } else {
            available_ram_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
        };

        let total_cpu_cores = sys.cpus().len();
        let cpu_name = Self::detect_cpu_name(&sys);

        // On Windows, a BIOS GPU UMA carveout hides the carved-out portion from
        // the OS view of RAM (issue #810): a 32 GB Hawk Point machine with an
        // 8 GB UMA frame buffer reports ~24 GB via sysinfo. Prefer installed
        // DIMM capacity for AMD APUs when the gap is clearly a carveout rather
        // than ordinary firmware reservation.
        let total_ram_gb = Self::windows_apu_total_ram_gb(&cpu_name, total_ram_gb);

        let gpus = Self::detect_all_gpus(total_ram_gb, &cpu_name);

        // Primary GPU = the one with the most VRAM (best for inference).
        // Per-card display values come from the primary; the fit-scoring pool
        // and GPU count are aggregated across every detected GPU so that
        // multi-GPU systems (including mixed models, e.g. RX 7600 + R9700)
        // contribute their full combined VRAM, not just the primary's.
        let primary = gpus.first();
        let has_gpu = !gpus.is_empty();
        let gpu_vram_gb = primary.and_then(|g| g.vram_gb);
        let gpu_name = primary.map(|g| g.name.clone());
        let unified_memory = primary.map(|g| g.unified_memory).unwrap_or(false);
        // Total VRAM = sum of per-card VRAM * count across all GPUs (for
        // multi-GPU tensor splitting). Unified-memory GPUs report the shared
        // system pool as their VRAM; with a single such GPU this is correct.
        let total_gpu_vram_gb = {
            let sum: f64 = gpus
                .iter()
                .filter_map(|g| g.vram_gb.map(|vram| vram * g.count as f64))
                .sum();
            if sum > 0.0 { Some(sum) } else { None }
        };
        let gpu_count: u32 = gpus.iter().map(|g| g.count).sum();

        let cpu_backend =
            if cfg!(target_arch = "aarch64") || cpu_name.to_lowercase().contains("apple") {
                GpuBackend::CpuArm
            } else {
                GpuBackend::CpuX86
            };
        let backend = primary.map(|g| g.backend).unwrap_or(cpu_backend);

        // Only Apple Silicon reports unified memory *and* runs Metal, so the
        // GPU-available query is meaningful only there. Other unified-memory
        // paths (AMD APUs, NVIDIA Grace) fall through to `None` because the
        // query is macOS-only.
        let gpu_available_gb = if unified_memory {
            detect_gpu_available_gb()
        } else {
            None
        };

        SystemSpecs {
            total_ram_gb,
            available_ram_gb,
            total_cpu_cores,
            cpu_name,
            has_gpu,
            gpu_vram_gb,
            total_gpu_vram_gb,
            gpu_available_gb,
            gpu_name,
            gpu_count,
            unified_memory,
            backend,
            gpus,
            cluster_mode: false,
            cluster_node_count: 0,
        }
    }

    /// Detect all GPUs across all vendors. Returns a Vec sorted by VRAM descending
    /// (best GPU first). Unlike the old cascade, this does NOT short-circuit:
    /// a system with both NVIDIA and AMD GPUs will report both.
    fn detect_all_gpus(total_ram_gb: f64, cpu_name: &str) -> Vec<GpuInfo> {
        let mut gpus = Vec::new();

        // NVIDIA GPUs via nvidia-smi, with sysfs fallback for Linux/toolbox setups
        let nvidia = Self::detect_nvidia_gpus();
        if nvidia.is_empty() {
            if let Some(nvidia_sysfs) = Self::detect_nvidia_gpu_sysfs_info() {
                gpus.push(nvidia_sysfs);
            }
        } else {
            gpus.extend(nvidia);
        }

        // AMD GPUs via rocm-smi or sysfs
        let amd_rocm = Self::detect_amd_gpu_rocm_info();
        if amd_rocm.is_empty() {
            gpus.extend(Self::detect_amd_gpu_sysfs_info());
        } else {
            gpus.extend(amd_rocm);
        }

        // Windows WMI (catches GPUs not found by vendor-specific tools)
        for wmi_gpu in Self::detect_gpu_windows_info() {
            // Skip if we already found a GPU with the same name from a vendor tool
            let dominated = gpus.iter().any(|existing| {
                let existing_lower = existing.name.to_lowercase();
                let wmi_lower = wmi_gpu.name.to_lowercase();
                existing_lower.contains(&wmi_lower) || wmi_lower.contains(&existing_lower)
            });
            if !dominated {
                gpus.push(wmi_gpu);
            }
        }

        // AMD unified memory APUs (e.g. Ryzen AI MAX series).
        // These share the full system RAM between CPU and GPU, like Apple Silicon.
        // WMI AdapterRAM is a 32-bit field capped at ~4 GB, so we override with
        // total system RAM for these APUs.
        //
        // On Windows, BIOS GPU UMA carveouts cause sysinfo to report only the
        // CPU-accessible portion (e.g. 32 GB on a 128 GB system where 96 GB is
        // allocated to the GPU). Query total physical DIMM capacity via
        // Win32_PhysicalMemory, which reads SMBIOS and is unaffected by the
        // carveout, so model fit estimates reflect the full memory pool.
        if is_amd_unified_memory_apu(cpu_name) {
            let apu_pool_gb = detect_windows_physical_total_ram_gb().unwrap_or(total_ram_gb);
            let amd_idx = gpus.iter().position(|g| {
                let lower = g.name.to_lowercase();
                lower.contains("amd") || lower.contains("radeon")
            });
            if let Some(idx) = amd_idx {
                gpus[idx].unified_memory = true;
                gpus[idx].vram_gb = Some(apu_pool_gb);
                // When detection could only produce a generic name (e.g. rocm-smi
                // reported "N/A"), use the APU model instead — it names the iGPU
                // (e.g. "AMD Ryzen AI MAX+ 395 w/ Radeon 8060S"), giving a stable
                // hardware identity for the leaderboard.
                if is_generic_amd_gpu_name(&gpus[idx].name) {
                    gpus[idx].name = format!("{cpu_name} (integrated)");
                }
            } else {
                // No AMD GPU found via other methods; create one.
                gpus.push(GpuInfo {
                    name: format!("{} (integrated)", cpu_name),
                    vram_gb: Some(apu_pool_gb),
                    backend: GpuBackend::Vulkan,
                    count: 1,
                    unified_memory: true,
                });
            }
        }

        // NVIDIA Grace / DGX Spark unified memory SoCs (e.g. GB10, GB20).
        // These share the full system RAM between CPU and GPU, like Apple Silicon.
        // nvidia-smi may report 0 VRAM or a small dedicated portion, so we
        // override with total system RAM and flag as unified memory.
        // Inside Docker the friendly name may be missing; we also match by PCI
        // device ID (e.g. "Device [10de:2e12]").
        let is_nvidia_unified = gpus.iter().any(|g| is_nvidia_unified_memory_gpu(&g.name));
        if is_nvidia_unified {
            for gpu in &mut gpus {
                if is_nvidia_unified_memory_gpu(&gpu.name) {
                    gpu.unified_memory = true;
                    gpu.vram_gb = Some(total_ram_gb);
                }
            }
        }

        // Intel GPUs (integrated or discrete Arc) via lspci/sysfs
        let intel_gpus = Self::detect_intel_gpus(total_ram_gb);
        if !intel_gpus.is_empty() {
            let already_found = gpus.iter().any(|g| g.name.to_lowercase().contains("intel"));
            if !already_found {
                gpus.extend(intel_gpus);
            }
        }

        // Intel macOS machines expose Intel and AMD GPUs through Metal, but
        // not through Linux ROCm/sysfs or NVIDIA-specific tools. Read
        // system_profiler so older MacBook Pros report their discrete Radeon.
        for mac_gpu in Self::detect_macos_metal_gpus() {
            let dominated = gpus
                .iter()
                .any(|existing| Self::is_same_gpu_name(&existing.name, &mac_gpu.name));
            if !dominated {
                gpus.push(mac_gpu);
            }
        }

        // Apple Silicon (unified memory)
        if let Some(vram) = Self::detect_apple_gpu(total_ram_gb) {
            let name = if cpu_name.to_lowercase().contains("apple") {
                cpu_name.to_string()
            } else {
                "Apple Silicon".to_string()
            };
            gpus.push(GpuInfo {
                name,
                vram_gb: Some(vram),
                backend: GpuBackend::Metal,
                count: 1,
                unified_memory: true,
            });
        }

        // Ascend NPUs via npu-smi
        let ascend = Self::detect_ascend_npus();
        if !ascend.is_empty() {
            gpus.extend(ascend);
        }

        // Vulkan fallback (e.g. Android/Termux with Turnip)
        let has_rocm_gpu = gpus.iter().any(|g| g.backend == GpuBackend::Rocm);
        for vulkan_gpu in Self::detect_vulkan_gpu_info() {
            // When a ROCm AMD GPU is already detected, skip any Vulkan AMD/RADV
            // devices — they represent the same physical GPU and ROCm is the
            // higher-quality detection path (provides real VRAM and product name).
            if has_rocm_gpu {
                let vk_lower = vulkan_gpu.name.to_lowercase();
                if vk_lower.contains("amd")
                    || vk_lower.contains("radeon")
                    || vk_lower.contains("radv")
                {
                    continue;
                }
            }
            let dominated = gpus
                .iter_mut()
                .find(|existing| Self::is_same_gpu_name(&existing.name, &vulkan_gpu.name));
            match dominated {
                Some(existing) => {
                    // The earlier detection path may know the device but not
                    // its VRAM (e.g. discrete Intel Arc when the sysfs VRAM
                    // files are absent) — if the Vulkan path ever supplies a
                    // real value, adopt it rather than dropping it (#609).
                    if !existing.unified_memory
                        && existing.vram_gb.unwrap_or(0.0) == 0.0
                        && vulkan_gpu.vram_gb.unwrap_or(0.0) > 0.0
                    {
                        existing.vram_gb = vulkan_gpu.vram_gb;
                    }
                }
                None => gpus.push(vulkan_gpu),
            }
        }

        // When both discrete and integrated GPUs are present, drop the
        // integrated GPUs so the discrete GPU becomes primary. This applies
        // globally, not just to the Windows WMI path, to handle cases where
        // an iGPU is detected via Vulkan or APU detection alongside a dGPU.
        // Keep macOS Metal iGPUs visible because Activity Monitor and
        // llama.cpp's Metal device list can expose both built-in GPUs.
        if !cfg!(target_os = "macos") {
            gpus = Self::prefer_discrete_gpus(gpus);
        }

        // Sort by VRAM descending so the best GPU is primary
        gpus.sort_by(|a, b| {
            let va = a.vram_gb.unwrap_or(0.0);
            let vb = b.vram_gb.unwrap_or(0.0);
            vb.partial_cmp(&va).unwrap_or(std::cmp::Ordering::Equal)
        });

        gpus
    }

    /// Detect NVIDIA GPUs via nvidia-smi. Returns one GpuInfo per unique model,
    /// with count and per-card VRAM for same-model multi-GPU setups.
    ///
    /// First tries querying `addressing_mode` to detect unified memory (Tegra/Grace
    /// Blackwell platforms). Falls back to the standard 2-column query if the field
    /// is unavailable on older nvidia-smi versions.
    fn detect_nvidia_gpus() -> Vec<GpuInfo> {
        // Try the extended query first (addressing_mode,memory.total,name).
        // On NVIDIA Tegra / Grace Blackwell, addressing_mode returns "ATS"
        // (Address Translation Services) which signals unified CPU+GPU memory.
        if let Some(gpus) = Self::try_nvidia_smi_with_addressing_mode() {
            return gpus;
        }

        // Fallback: standard 2-column query for older nvidia-smi versions
        let output = match std::process::Command::new("nvidia-smi")
            .arg("--query-gpu=memory.total,name")
            .arg("--format=csv,noheader,nounits")
            .output()
        {
            Ok(o) if o.status.success() => o,
            _ => return Vec::new(),
        };

        let text = match String::from_utf8(output.stdout) {
            Ok(t) => t,
            Err(_) => return Vec::new(),
        };

        Self::parse_nvidia_smi_list(&text)
    }

    /// Try nvidia-smi with `addressing_mode` column. Returns `None` if the
    /// query fails (e.g. older driver that doesn't support the field), so the
    /// caller can fall back to the standard query.
    fn try_nvidia_smi_with_addressing_mode() -> Option<Vec<GpuInfo>> {
        let output = std::process::Command::new("nvidia-smi")
            .arg("--query-gpu=addressing_mode,memory.total,name")
            .arg("--format=csv,noheader,nounits")
            .output()
            .ok()?;

        if !output.status.success() {
            return None;
        }

        let text = String::from_utf8(output.stdout).ok()?;
        Some(Self::parse_nvidia_smi_extended(&text))
    }

    /// Parse `nvidia-smi --query-gpu=addressing_mode,memory.total,name`.
    /// Detects unified memory when addressing_mode is "ATS" and VRAM is
    /// unavailable — common on NVIDIA Tegra / Grace Blackwell (DGX Spark).
    /// Falls back to system RAM via /proc/meminfo as the unified memory pool.
    fn parse_nvidia_smi_extended(text: &str) -> Vec<GpuInfo> {
        // Track per-model: (count, per_card_vram_mb, is_unified)
        let mut grouped: BTreeMap<String, (u32, f64, bool)> = BTreeMap::new();
        let total_ram_gb = read_proc_meminfo_total_gb();

        for line in text.lines() {
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            let parts: Vec<&str> = line.splitn(3, ',').collect();
            if parts.len() < 3 {
                continue;
            }

            let addr_mode = parts[0].trim();
            let is_unified = addr_mode.eq_ignore_ascii_case("ATS");

            let name = parts[2].trim().to_string();
            let name = if name.is_empty() {
                "NVIDIA GPU".to_string()
            } else {
                name
            };

            let parsed_vram_mb = parts[1].trim().parse::<f64>().unwrap_or(0.0);

            let vram_mb = if parsed_vram_mb > 0.0 {
                parsed_vram_mb
            } else if is_unified {
                // Unified memory: use total system RAM as the shared pool
                total_ram_gb.unwrap_or(0.0) * 1024.0
            } else {
                estimate_vram_from_name(&name) * 1024.0
            };

            let entry = grouped.entry(name).or_insert((0, 0.0, false));
            entry.0 += 1;
            if vram_mb > entry.1 {
                entry.1 = vram_mb;
            }
            if is_unified {
                entry.2 = true;
            }
        }

        if grouped.is_empty() {
            return Vec::new();
        }

        grouped
            .into_iter()
            .map(|(name, (count, per_card_vram_mb, is_unified))| GpuInfo {
                name,
                vram_gb: if per_card_vram_mb > 0.0 {
                    Some(per_card_vram_mb / 1024.0)
                } else {
                    None
                },
                backend: GpuBackend::Cuda,
                count,
                unified_memory: is_unified,
            })
            .collect()
    }

    /// Parse `nvidia-smi --query-gpu=memory.total,name --format=csv,noheader,nounits`.
    /// Groups same-model cards and keeps per-card VRAM (never sums across cards).
    fn parse_nvidia_smi_list(text: &str) -> Vec<GpuInfo> {
        let mut grouped: BTreeMap<String, (u32, f64)> = BTreeMap::new();

        for line in text.lines() {
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            let parts: Vec<&str> = line.splitn(2, ',').collect();

            let name = parts
                .get(1)
                .map(|s| s.trim())
                .filter(|s| !s.is_empty())
                .unwrap_or("NVIDIA GPU")
                .to_string();

            let parsed_vram_mb = parts
                .first()
                .and_then(|s| s.trim().parse::<f64>().ok())
                .unwrap_or(0.0);
            let vram_mb = if parsed_vram_mb > 0.0 {
                parsed_vram_mb
            } else {
                estimate_vram_from_name(&name) * 1024.0
            };

            let entry = grouped.entry(name).or_insert((0, 0.0));
            entry.0 += 1;
            if vram_mb > entry.1 {
                entry.1 = vram_mb;
            }
        }

        if grouped.is_empty() {
            return Vec::new();
        }

        grouped
            .into_iter()
            .map(|(name, (count, per_card_vram_mb))| GpuInfo {
                name,
                vram_gb: if per_card_vram_mb > 0.0 {
                    Some(per_card_vram_mb / 1024.0)
                } else {
                    None
                },
                backend: GpuBackend::Cuda,
                count,
                unified_memory: false,
            })
            .collect()
    }

    /// Detect NVIDIA GPUs via Linux sysfs when nvidia-smi is unavailable.
    /// This is common in containerized environments (e.g. Toolbx) and
    /// Nouveau-based systems.
    fn detect_nvidia_gpu_sysfs_info() -> Option<GpuInfo> {
        if !cfg!(target_os = "linux") {
            return None;
        }

        let entries = std::fs::read_dir("/sys/class/drm").ok()?;
        let mut gpu_count: u32 = 0;
        let mut total_vram_bytes: u64 = 0;
        let mut slot_hints: Vec<String> = Vec::new();
        let mut backend = GpuBackend::Vulkan;

        for entry in entries.flatten() {
            let card_path = entry.path();
            let fname = card_path.file_name()?.to_str()?.to_string();
            // Only look at cardN entries, not connectors (cardN-DP-1, etc.)
            if !fname.starts_with("card") || fname.contains('-') {
                continue;
            }

            let device_path = card_path.join("device");
            let vendor_path = device_path.join("vendor");
            let Ok(vendor) = std::fs::read_to_string(&vendor_path) else {
                continue;
            };
            if vendor.trim() != "0x10de" {
                continue;
            }

            gpu_count += 1;

            if let Ok(vram_str) = std::fs::read_to_string(device_path.join("mem_info_vram_total"))
                && let Ok(vram_bytes) = vram_str.trim().parse::<u64>()
                && vram_bytes > 0
            {
                // Track the maximum per-card VRAM instead of summing across all cards.
                total_vram_bytes = total_vram_bytes.max(vram_bytes);
            }

            if let Ok(uevent) = std::fs::read_to_string(device_path.join("uevent")) {
                for line in uevent.lines() {
                    if let Some(slot) = line.strip_prefix("PCI_SLOT_NAME=") {
                        slot_hints.push(slot.to_string());
                    } else if let Some(driver) = line.strip_prefix("DRIVER=")
                        && driver.eq_ignore_ascii_case("nvidia")
                    {
                        backend = GpuBackend::Cuda;
                    }
                }
            }
        }

        if gpu_count == 0 {
            return None;
        }

        let name = Self::get_nvidia_gpu_name_lspci(&slot_hints)
            .unwrap_or_else(|| "NVIDIA GPU".to_string());

        let mut vram_gb = if total_vram_bytes > 0 {
            Some(total_vram_bytes as f64 / (1024.0 * 1024.0 * 1024.0))
        } else {
            None
        };

        if vram_gb.is_none() {
            let est = estimate_vram_from_name(&name);
            if est > 0.0 {
                vram_gb = Some(est);
            }
        }

        let unified_memory = is_nvidia_unified_memory_gpu(&name);

        Some(GpuInfo {
            name,
            vram_gb,
            backend,
            count: gpu_count,
            unified_memory,
        })
    }

    /// Detect AMD GPUs via rocm-smi (available on Linux with ROCm installed).
    /// Parses per-card VRAM and GPU name from rocm-smi output, returning one
    /// `GpuInfo` per distinct GPU model (like `detect_nvidia_gpus`).
    fn detect_amd_gpu_rocm_info() -> Vec<GpuInfo> {
        let vram_output = match std::process::Command::new("rocm-smi")
            .arg("--showmeminfo")
            .arg("vram")
            .output()
        {
            Ok(o) if o.status.success() => o,
            _ => return Vec::new(),
        };
        let vram_text = match String::from_utf8(vram_output.stdout) {
            Ok(t) => t,
            Err(_) => return Vec::new(),
        };

        let product_text = std::process::Command::new("rocm-smi")
            .arg("--showproductname")
            .output()
            .ok()
            .filter(|o| o.status.success())
            .and_then(|o| String::from_utf8(o.stdout).ok());

        Self::parse_rocm_smi_output(&vram_text, product_text.as_deref())
    }

    /// Parse per-GPU VRAM totals (bytes) from `rocm-smi --showmeminfo vram`.
    ///
    /// Handles both output formats:
    /// * Block (ROCm 5.x / 6.x default): one line per field, e.g.
    ///   `GPU[0] : VRAM Total Memory (B): 8589934592`. The total line is
    ///   matched by containing "total" and not "used".
    /// * Tabular (newer rocm-smi): a header row followed by one row per
    ///   device, e.g.
    ///   `Device  Node  VRAM Total Memory (B)   VRAM Total Used Memory (B)`
    ///   `0       2     34342961152             16893`.
    ///   Device rows begin with the integer device index; the Total column
    ///   precedes the Used column, so the first VRAM-sized number on the row
    ///   is the total.
    fn parse_rocm_vram_bytes(vram_text: &str) -> Vec<u64> {
        let mut out: Vec<u64> = Vec::new();

        // Block format.
        for line in vram_text.lines() {
            let lower = line.to_lowercase();
            if lower.contains("total")
                && !lower.contains("used")
                && let Some(val) = line
                    .split_whitespace()
                    .filter_map(|w| w.parse::<u64>().ok())
                    .next_back()
                && val > 0
            {
                out.push(val);
            }
        }
        if !out.is_empty() {
            return out;
        }

        // Tabular format fallback. A device row starts with the integer device
        // index; pick the first number large enough to be a VRAM total (>= 64
        // MB), which skips the device/node index columns and lands on the
        // Total column before the Used column.
        const MIN_VRAM_BYTES: u64 = 64 * 1024 * 1024; // 64 MB
        for line in vram_text.lines() {
            let mut tokens = line.split_whitespace();
            match tokens.next() {
                Some(first) if first.parse::<u32>().is_ok() => {}
                _ => continue, // not a device data row
            }
            if let Some(total) = line
                .split_whitespace()
                .filter_map(|w| w.parse::<u64>().ok())
                .find(|&v| v >= MIN_VRAM_BYTES)
            {
                out.push(total);
            }
        }
        out
    }

    /// Parse per-GPU product names from `rocm-smi --showproductname`.
    ///
    /// Handles both the block format (`GPU[0] : Card Series: AMD Radeon RX
    /// 7600`) and the tabular format where `Card Series` is a column header
    /// and each device row carries its model in that column. Returns one
    /// name per device, in device order.
    fn parse_rocm_product_names(text: &str) -> Vec<String> {
        // Block format: name is after the last colon on a "Card Series" line,
        // e.g. "GPU[0] : Card Series: AMD Radeon RX 7600". The colon guard
        // avoids matching a tabular "Card Series" column header (no colon).
        let mut block: Vec<String> = Vec::new();
        let mut gfx_versions: Vec<String> = Vec::new();
        for line in text.lines() {
            let lower = line.to_lowercase();
            if lower.contains("card series")
                && line.contains(':')
                && let Some(raw) = line.rsplit(':').next().map(|n| n.trim().to_string())
                && !raw.is_empty()
            {
                // rocm-smi reports "N/A" when it cannot read the marketing name
                // (e.g. libdrm_amdgpu.so missing on Strix Halo APUs). Keep the
                // slot aligned but record it as a generic AMD GPU so callers can
                // fall back to a real name (lspci / the APU model) downstream.
                block.push(if is_placeholder_gpu_name(&raw) {
                    "AMD GPU".to_string()
                } else {
                    raw
                });
            } else if lower.contains("gfx version")
                && line.contains(':')
                && let Some(gfx) = line.rsplit(':').next().map(|g| g.trim().to_string())
                && !gfx.is_empty()
            {
                gfx_versions.push(gfx);
            }
        }
        if !block.is_empty() {
            // Disambiguate generic series names with the GFX version when
            // available: some accelerators (e.g. Instinct MI50/MI60) report
            // `Card Series: AMD Radeon Graphics`, which would otherwise be
            // indistinguishable from — and grouped with — an APU iGPU that
            // reports the same generic name (issue #638).
            if gfx_versions.len() == block.len() {
                for (name, gfx) in block.iter_mut().zip(&gfx_versions) {
                    if Self::is_integrated_gpu_name(name) {
                        *name = format!("{name} ({gfx})");
                    }
                }
            }
            return block;
        }

        // Tabular format: slice the "Card Series" column out of each device
        // row using the header column offsets. The column runs from the start
        // of "Card Series" to the start of the next known column header.
        let mut out: Vec<String> = Vec::new();
        let Some(header) = text
            .lines()
            .find(|l| l.to_lowercase().contains("card series"))
        else {
            return out;
        };
        let header_lower = header.to_lowercase();
        let Some(start) = header_lower.find("card series") else {
            return out;
        };
        let end = ["card model", "card vendor", "card sku", "card partition"]
            .iter()
            .filter_map(|h| header_lower.find(h))
            .filter(|&i| i > start)
            .min()
            .unwrap_or(header.len());

        for line in text.lines() {
            // Device rows start with the integer device index.
            match line.split_whitespace().next() {
                Some(first) if first.parse::<u32>().is_ok() => {}
                _ => continue,
            }
            if line.len() <= start {
                out.push("AMD GPU".to_string());
                continue;
            }
            let slice_end = end.min(line.len());
            let name = line[start..slice_end].trim().to_string();
            out.push(if name.is_empty() || is_placeholder_gpu_name(&name) {
                "AMD GPU".to_string()
            } else {
                name
            });
        }
        out
    }

    /// Parse rocm-smi `--showmeminfo vram` and `--showproductname` output
    /// into one `GpuInfo` per distinct GPU model. Identical models are
    /// grouped with a `count` field, like `parse_nvidia_smi_list`.
    fn parse_rocm_smi_output(vram_text: &str, product_text: Option<&str>) -> Vec<GpuInfo> {
        let per_gpu_vram_bytes = Self::parse_rocm_vram_bytes(vram_text);
        let per_gpu_names = product_text
            .map(Self::parse_rocm_product_names)
            .unwrap_or_default();

        // Filter out integrated GPUs (iGPUs) that have very little VRAM.
        // rocm-smi reports all GPU agents including iGPUs on APUs like
        // Ryzen 9800X3D, which would otherwise inflate the GPU count.
        // Discrete GPUs have > 2 GB VRAM; iGPUs typically show <= 2 GB.
        const IGPU_VRAM_THRESHOLD: u64 = 2 * 1024 * 1024 * 1024; // 2 GB
        let has_discrete = per_gpu_vram_bytes.iter().any(|&v| v > IGPU_VRAM_THRESHOLD);

        // Pair each GPU index with its name and VRAM, filtering iGPUs when
        // discrete GPUs are present.
        let gpu_count = per_gpu_vram_bytes.len().max(per_gpu_names.len());
        let mut grouped: std::collections::BTreeMap<String, (u32, u64)> =
            std::collections::BTreeMap::new();

        for i in 0..gpu_count {
            let vram = per_gpu_vram_bytes.get(i).copied().unwrap_or(0);
            if has_discrete && vram <= IGPU_VRAM_THRESHOLD {
                continue; // skip iGPU
            }
            let name = per_gpu_names
                .get(i)
                .cloned()
                .unwrap_or_else(|| "AMD GPU".to_string());
            let entry = grouped.entry(name).or_insert((0, 0));
            entry.0 += 1;
            if vram > entry.1 {
                entry.1 = vram;
            }
        }

        if grouped.is_empty() {
            return Vec::new();
        }

        grouped
            .into_iter()
            .map(|(name, (count, vram_bytes))| {
                let vram_gb = if vram_bytes > 0 {
                    Some(vram_bytes as f64 / (1024.0 * 1024.0 * 1024.0))
                } else {
                    let est = estimate_vram_from_name(&name);
                    if est > 0.0 { Some(est) } else { None }
                };
                GpuInfo {
                    name,
                    vram_gb,
                    backend: GpuBackend::Rocm,
                    count,
                    unified_memory: false,
                }
            })
            .collect()
    }

    /// Detect AMD GPUs via sysfs on Linux (works without ROCm installed).
    /// AMD vendor ID is 0x1002. Enumerates every `cardN` entry in
    /// `/sys/class/drm`, groups identical models with a `count` (like the
    /// ROCm and NVIDIA paths), and returns one `GpuInfo` per distinct model
    /// so multi-GPU setups are reported in full.
    fn detect_amd_gpu_sysfs_info() -> Vec<GpuInfo> {
        if !cfg!(target_os = "linux") {
            return Vec::new();
        }

        let entries = match std::fs::read_dir("/sys/class/drm") {
            Ok(e) => e,
            Err(_) => return Vec::new(),
        };

        // Collect per-card (name, vram) pairs.
        let mut cards: Vec<(String, Option<f64>)> = Vec::new();

        for entry in entries.flatten() {
            let card_path = entry.path();
            let fname = match card_path.file_name().and_then(|f| f.to_str()) {
                Some(f) => f.to_string(),
                None => continue,
            };
            // Only look at cardN entries, not cardN-DP-1 etc.
            if !fname.starts_with("card") || fname.contains('-') {
                continue;
            }

            let device_path = card_path.join("device");
            let vendor_path = device_path.join("vendor");
            match std::fs::read_to_string(&vendor_path) {
                Ok(vendor) if vendor.trim() == "0x1002" => {}
                _ => continue,
            }

            // Found an AMD GPU. Try to read VRAM.
            let mut vram_gb: Option<f64> = None;
            let vram_path = device_path.join("mem_info_vram_total");
            if let Ok(vram_str) = std::fs::read_to_string(&vram_path)
                && let Ok(vram_bytes) = vram_str.trim().parse::<u64>()
                && vram_bytes > 0
            {
                vram_gb = Some(vram_bytes as f64 / (1024.0 * 1024.0 * 1024.0));
            }

            // Resolve this card's PCI slot so lspci yields a per-card name.
            let mut slot_hints: Vec<String> = Vec::new();
            if let Ok(uevent) = std::fs::read_to_string(device_path.join("uevent")) {
                for line in uevent.lines() {
                    if let Some(slot) = line.strip_prefix("PCI_SLOT_NAME=") {
                        slot_hints.push(slot.to_string());
                    }
                }
            }

            // Try to get GPU name from lspci
            let gpu_name = Self::get_amd_gpu_name_lspci(&slot_hints);
            let name = gpu_name.unwrap_or_else(|| "AMD GPU".to_string());

            // If we still don't have VRAM, try to estimate from name
            if vram_gb.is_none() {
                let estimated = estimate_vram_from_name(&name);
                if estimated > 0.0 {
                    vram_gb = Some(estimated);
                }
            }

            cards.push((name, vram_gb));
        }

        Self::group_and_filter_amd_sysfs_cards(cards)
    }

    /// Group sysfs AMD cards by model name and drop integrated GPUs when a
    /// discrete card is present. Pure so the #303/#638 multi-GPU
    /// configurations can be regression-tested without a live sysfs.
    fn group_and_filter_amd_sysfs_cards(cards: Vec<(String, Option<f64>)>) -> Vec<GpuInfo> {
        // Group identical models, tracking count and max per-card VRAM.
        let mut grouped: BTreeMap<String, (u32, Option<f64>)> = BTreeMap::new();
        for (name, vram_gb) in cards {
            let entry = grouped.entry(name).or_insert((0, None));
            entry.0 += 1;
            match (entry.1, vram_gb) {
                (Some(existing), Some(new)) if new > existing => entry.1 = Some(new),
                (None, Some(_)) => entry.1 = vram_gb,
                _ => {}
            }
        }

        // Filter out integrated GPUs when discrete GPUs are present. A card
        // whose VRAM could not be read (None) gets the benefit of the doubt:
        // only a *known* small VRAM (<= 2 GB) marks a card as
        // integrated-class. Requiring Some(vram) here silently dropped
        // discrete cards with an unreadable mem_info_vram_total and a name
        // missing from the VRAM estimate table.
        let has_discrete = grouped.iter().any(|(name, (_, vram))| {
            !Self::is_integrated_gpu_name(name) && vram.unwrap_or(0.0) > 2.0
        });
        if has_discrete {
            grouped.retain(|name, (_, vram)| {
                !Self::is_integrated_gpu_name(name) && vram.is_none_or(|v| v > 2.0)
            });
        }

        grouped
            .into_iter()
            .map(|(name, (count, vram_gb))| GpuInfo {
                name,
                // AMD GPU without ROCm — Vulkan is the most likely backend
                vram_gb,
                backend: GpuBackend::Vulkan,
                count,
                unified_memory: false,
            })
            .collect()
    }

    /// Extract AMD GPU name from lspci output.
    fn get_amd_gpu_name_lspci(slot_hints: &[String]) -> Option<String> {
        let text = Self::lspci_output()?;

        // First pass: match exact slot (e.g. "0000:01:00.0"), if available.
        for slot in slot_hints {
            for line in text.lines() {
                let lower = line.to_lowercase();
                if line.starts_with(slot)
                    && (lower.contains("vga") || lower.contains("3d") || lower.contains("display"))
                    && (lower.contains("amd") || lower.contains("ati"))
                    && let Some(model) = Self::extract_model_from_lspci_line(line)
                {
                    return Some(model);
                }
            }
        }

        // Fallback: any AMD/ATI display controller line. Headless/secondary
        // cards (e.g. Instinct MI50s, #638) enumerate as "Display controller",
        // not "VGA compatible controller", so match all three classes just
        // like the slot-hint pass above.
        for line in text.lines() {
            let lower = line.to_lowercase();
            if (lower.contains("vga") || lower.contains("3d") || lower.contains("display"))
                && (lower.contains("amd") || lower.contains("ati"))
                && let Some(model) = Self::extract_model_from_lspci_line(line)
            {
                return Some(model);
            }
        }
        None
    }

    /// Resolve NVIDIA GPU name from lspci, optionally prioritizing specific
    /// PCI slots discovered from sysfs.
    fn get_nvidia_gpu_name_lspci(slot_hints: &[String]) -> Option<String> {
        let text = Self::lspci_output()?;

        // First pass: match exact slot (e.g. "0000:01:00.0"), if available.
        for slot in slot_hints {
            for line in text.lines() {
                let lower = line.to_lowercase();
                if line.starts_with(slot)
                    && (lower.contains("vga") || lower.contains("3d") || lower.contains("display"))
                    && lower.contains("nvidia")
                    && let Some(model) = Self::extract_model_from_lspci_line(line)
                {
                    return Some(model);
                }
            }
        }

        // Fallback: any NVIDIA display controller line.
        for line in text.lines() {
            let lower = line.to_lowercase();
            if (lower.contains("vga") || lower.contains("3d") || lower.contains("display"))
                && lower.contains("nvidia")
                && let Some(model) = Self::extract_model_from_lspci_line(line)
            {
                return Some(model);
            }
        }

        None
    }

    /// Read lspci output, with host fallback for containerized environments.
    fn lspci_output() -> Option<String> {
        let local = std::process::Command::new("lspci")
            .arg("-nnD")
            .output()
            .ok()
            .filter(|o| o.status.success())
            .and_then(|o| String::from_utf8(o.stdout).ok());

        if local.is_some() {
            return local;
        }

        std::process::Command::new("flatpak-spawn")
            .args(["--host", "lspci", "-nnD"])
            .output()
            .ok()
            .filter(|o| o.status.success())
            .and_then(|o| String::from_utf8(o.stdout).ok())
    }

    /// Extract a likely model name from an lspci line.
    /// Prefers human-readable bracketed tokens (e.g. "[GeForce RTX 2060]").
    fn extract_model_from_lspci_line(line: &str) -> Option<String> {
        let mut best: Option<String> = None;
        let mut rest = line;

        while let Some(start) = rest.find('[') {
            let after = &rest[start + 1..];
            let Some(end) = after.find(']') else { break };
            let token = after[..end].trim();
            let usable = !token.is_empty()
                && !token.contains(':')
                && !token.chars().all(|c| c.is_ascii_digit());

            if usable
                && best
                    .as_ref()
                    .map(|current| token.len() > current.len())
                    .unwrap_or(true)
            {
                best = Some(token.to_string());
            }

            rest = &after[end + 1..];
        }

        if best.is_some() {
            return best;
        }

        // Fallback: text after the first ": " separator.
        line.split_once(": ")
            .map(|(_, right)| right.trim().to_string())
            .filter(|s| !s.is_empty())
    }

    /// Detect GPUs on Windows via WMI (Win32_VideoController).
    /// Returns all discrete GPUs found (AMD, NVIDIA, Intel, etc.).
    /// When both discrete and integrated GPUs are present, the integrated
    /// GPUs are filtered out so the discrete GPU is selected as primary.
    fn detect_gpu_windows_info() -> Vec<GpuInfo> {
        if !cfg!(target_os = "windows") {
            return Vec::new();
        }

        // Use PowerShell to query WMI — more reliable than wmic (deprecated)
        if let Ok(output) = std::process::Command::new("powershell")
            .arg("-NoProfile")
            .arg("-Command")
            .arg("Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM | ForEach-Object { $_.Name + '|' + $_.AdapterRAM }")
            .output()
            && output.status.success()
        {
            // Lossy decode: PowerShell 5.1 emits the console OEM codepage, and
            // a strict decode would throw away every adapter over one ® in a
            // single name.
            let text = String::from_utf8_lossy(&output.stdout);
            let gpus = Self::parse_windows_gpu_list(&text);
            if !gpus.is_empty() {
                let gpus = Self::apply_registry_vram(gpus, &detect_windows_registry_vram());
                return Self::prefer_discrete_gpus(gpus);
            }
        }

        // Fallback to wmic for older Windows
        let gpus = Self::detect_gpu_windows_wmic_list();
        let gpus = Self::apply_registry_vram(gpus, &detect_windows_registry_vram());
        Self::prefer_discrete_gpus(gpus)
    }

    /// Override WMI-derived VRAM with the 64-bit value the display driver
    /// publishes in the registry (issue #830).
    ///
    /// `Win32_VideoController.AdapterRAM` is a `uint32`, so any card with 4 GB
    /// or more reports ~4293918720 bytes. `resolve_wmi_vram` then falls back to
    /// guessing from the product name, and cards missing from that table land
    /// on a generic vendor default — an AMD Radeon AI PRO R9700 (32 GB) was
    /// reported as 8 GB. `HardwareInformation.qwMemorySize` is a REG_QWORD
    /// written by the driver itself, so it is exact for any adapter size.
    ///
    /// Entries are matched to adapters by driver description, which mirrors
    /// `Win32_VideoController.Name` on every mainstream WDDM driver. Adapters
    /// with no registry match keep whatever VRAM detection already produced.
    fn apply_registry_vram(gpus: Vec<GpuInfo>, registry: &[(String, u64)]) -> Vec<GpuInfo> {
        if registry.is_empty() {
            return gpus;
        }
        gpus.into_iter()
            .map(|mut gpu| {
                if let Some(bytes) = Self::match_registry_vram(&gpu.name, registry) {
                    gpu.vram_gb = Some(bytes as f64 / (1024.0 * 1024.0 * 1024.0));
                }
                gpu
            })
            .collect()
    }

    /// Find the registry VRAM entry belonging to `name`.
    ///
    /// An exact driver-description match is preferred; `DriverDesc` is what
    /// populates `Win32_VideoController.Name`, so it normally hits. Failing
    /// that, a containment match is accepted only when exactly one entry
    /// matches — an ambiguous partial (a "RTX 4090" adapter against both
    /// "RTX 4090" and "RTX 4090 Ti" entries) must not silently bind to the
    /// wrong card's VRAM.
    fn match_registry_vram(name: &str, registry: &[(String, u64)]) -> Option<u64> {
        let target = normalize_gpu_name_for_match(name);
        if target.is_empty() {
            return None;
        }
        if let Some((_, bytes)) = registry
            .iter()
            .find(|(desc, _)| normalize_gpu_name_for_match(desc) == target)
        {
            return Some(*bytes);
        }
        let mut partial = registry.iter().filter(|(desc, _)| {
            let desc = normalize_gpu_name_for_match(desc);
            !desc.is_empty() && (desc.contains(&target) || target.contains(&desc))
        });
        match (partial.next(), partial.next()) {
            (Some((_, bytes)), None) => Some(*bytes),
            _ => None,
        }
    }

    /// Parse `DriverDesc|bytes` lines from the display-class registry dump.
    ///
    /// Implausibly small values are dropped rather than trusted. The WDK
    /// documents `HardwareInformation.MemorySize` as megabytes while WMI maps
    /// the same value to a bytes-typed field, and real drivers write bytes; a
    /// driver that followed the WDK to the letter would report "8192" for an
    /// 8 GB card. Anything under 64 MiB is therefore treated as unusable, so a
    /// mis-typed value falls back to the existing estimate instead of
    /// overwriting a good one with a few kilobytes.
    fn parse_windows_registry_vram(text: &str) -> Vec<(String, u64)> {
        // Smallest value that can plausibly be a byte count for a real
        // adapter. Below this the units are wrong, not the card.
        const MIN_PLAUSIBLE_VRAM_BYTES: u64 = 64 * 1024 * 1024;

        let mut entries = Vec::new();
        for line in text.lines() {
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            let Some((desc, bytes)) = line.split_once('|') else {
                continue;
            };
            let desc = desc.trim();
            let Ok(bytes) = bytes.trim().parse::<u64>() else {
                continue;
            };
            if desc.is_empty() || bytes < MIN_PLAUSIBLE_VRAM_BYTES {
                continue;
            }
            entries.push((desc.to_string(), bytes));
        }
        entries
    }

    /// Windows RAM figure for AMD APUs with a BIOS UMA carveout (issue #810).
    ///
    /// The OS view of RAM excludes the carveout, so a 32 GB machine with an
    /// 8 GB frame buffer reports ~24 GB. For AMD APUs, prefer installed DIMM
    /// capacity (`Win32_PhysicalMemory`, unaffected by the carveout) when the
    /// gap is carveout-sized. Everything else keeps the sysinfo figure, as
    /// does any failure of the WMI query. `available_ram_gb` is untouched
    /// either way, so fit grading against currently-free RAM is unaffected.
    fn windows_apu_total_ram_gb(cpu_name: &str, sysinfo_total_gb: f64) -> f64 {
        if !is_amd_apu(cpu_name) {
            return sysinfo_total_gb;
        }
        match detect_windows_physical_total_ram_gb() {
            Some(physical) => apply_ram_carveout_override(sysinfo_total_gb, physical),
            None => sysinfo_total_gb,
        }
    }

    /// Fallback Windows GPU detection via wmic (works on older systems).
    fn detect_gpu_windows_wmic_list() -> Vec<GpuInfo> {
        let output = match std::process::Command::new("wmic")
            .arg("path")
            .arg("win32_VideoController")
            .arg("get")
            .arg("Name,AdapterRAM")
            .arg("/format:csv")
            .output()
        {
            Ok(o) if o.status.success() => o,
            _ => return Vec::new(),
        };

        let text = match String::from_utf8(output.stdout) {
            Ok(t) => t,
            Err(_) => return Vec::new(),
        };

        let mut gpus = Vec::new();
        // CSV format: Node,AdapterRAM,Name
        for line in text.lines().skip(1) {
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            let parts: Vec<&str> = line.split(',').collect();
            if parts.len() >= 3 {
                let raw_vram: u64 = parts[1].trim().parse().unwrap_or(0);
                let name = parts[2..].join(",").trim().to_string();
                if let Some(gpu) = Self::windows_gpu_from_wmi(name, raw_vram) {
                    gpus.push(gpu);
                }
            }
        }
        gpus
    }

    /// Parse all GPU entries from PowerShell output (Name|AdapterRAM per line).
    fn parse_windows_gpu_list(text: &str) -> Vec<GpuInfo> {
        let mut gpus = Vec::new();
        for line in text.lines() {
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            let parts: Vec<&str> = line.splitn(2, '|').collect();
            let name = parts[0].trim().to_string();
            let raw_vram: u64 = parts
                .get(1)
                .and_then(|v| v.trim().parse().ok())
                .unwrap_or(0);

            if let Some(gpu) = Self::windows_gpu_from_wmi(name, raw_vram) {
                gpus.push(gpu);
            }
        }
        gpus
    }

    /// Convert one Win32_VideoController row into an inference-capable GPU.
    fn windows_gpu_from_wmi(name: String, raw_vram: u64) -> Option<GpuInfo> {
        let lower = name.to_lowercase();
        if lower.is_empty()
            || lower.contains("microsoft")
            || lower.contains("basic")
            || lower.contains("virtual")
            || Self::is_unsupported_windows_intel_gpu(&lower)
        {
            return None;
        }

        Some(GpuInfo {
            backend: Self::infer_gpu_backend(&name),
            vram_gb: Self::resolve_wmi_vram(raw_vram, &name),
            name,
            count: 1,
            unified_memory: false,
        })
    }

    /// Intel HD Graphics predates the integrated GPU generations supported by
    /// current oneAPI runtimes. WMI's AdapterRAM is a shared-memory aperture on
    /// these devices, not a dedicated SYCL pool, so exposing it as a GPU makes
    /// fit and throughput estimates fundamentally misleading (issue #840).
    fn is_unsupported_windows_intel_gpu(lower_name: &str) -> bool {
        lower_name.contains("intel") && lower_name.contains("hd graphics")
    }

    /// When both discrete and integrated GPUs are detected on Windows,
    /// drop the integrated GPUs so the discrete GPU becomes primary.
    /// If only integrated GPUs are present, keep them all (iGPU-only systems).
    fn prefer_discrete_gpus(gpus: Vec<GpuInfo>) -> Vec<GpuInfo> {
        let discrete: Vec<GpuInfo> = gpus
            .iter()
            .filter(|g| !Self::is_integrated_gpu(&g.name, g.vram_gb))
            .cloned()
            .collect();

        if discrete.is_empty() {
            // No discrete GPUs found; keep everything (iGPU-only system).
            gpus
        } else {
            discrete
        }
    }

    /// VRAM-aware integrated-GPU check.
    ///
    /// Intel iGPU product names (UHD/HD/Iris) are conclusive, but the AMD
    /// "Radeon Graphics" pattern is ambiguous: datacenter accelerators like
    /// the Instinct MI50/MI60 report the generic `Card Series: AMD Radeon
    /// Graphics` through rocm-smi on some firmware. No true iGPU has this
    /// much *dedicated* VRAM, so a large-VRAM AMD-generic device is treated
    /// as discrete rather than dropped (issue #638).
    fn is_integrated_gpu(name: &str, vram_gb: Option<f64>) -> bool {
        const AMD_GENERIC_DISCRETE_VRAM_GB: f64 = 8.0;
        if !Self::is_integrated_gpu_name(name) {
            return false;
        }
        // A named AMD mobile iGPU (Radeon 610M…890M) is conclusively
        // integrated. With BIOS UMA carveouts of 8 GB+ now reported correctly
        // (issue #810), these would otherwise trip the large-VRAM "generic
        // name is really an Instinct" escape hatch below (issue #638) and be
        // promoted to discrete.
        if Self::is_amd_mobile_igpu_name(name) {
            return true;
        }
        let lower = name.to_lowercase();
        let amd_generic = lower.contains("radeon") && !lower.contains("(integrated)");
        !(amd_generic && vram_gb.unwrap_or(0.0) >= AMD_GENERIC_DISCRETE_VRAM_GB)
    }

    /// Heuristic: returns true when the GPU name matches known integrated GPU
    /// patterns on Windows (Intel UHD/HD/Iris, AMD Radeon Graphics without a
    /// discrete model number like RX).
    fn is_integrated_gpu_name(name: &str) -> bool {
        let lower = name.to_lowercase();

        // Explicitly tagged as integrated (e.g. from APU detection path)
        if lower.contains("(integrated)") {
            return true;
        }

        // Named AMD mobile iGPUs (Radeon 610M…890M) are conclusive even
        // without the "Graphics" suffix some drivers omit.
        if Self::is_amd_mobile_igpu_name(name) {
            return true;
        }

        // Intel integrated: UHD, HD Graphics, Iris (but NOT Intel Arc discrete)
        if lower.contains("intel") {
            return lower.contains("uhd")
                || lower.contains("hd graphics")
                || (lower.contains("iris") && !lower.contains("arc"));
        }

        // AMD integrated: "Radeon Graphics" or "Radeon(TM) Graphics" without
        // a discrete series identifier (RX, PRO, Vega 56/64, VII, W-series).
        if lower.contains("radeon") && lower.contains("graphics") {
            let has_discrete_tag = lower.contains("rx ")
                || lower.contains("pro ")
                || lower.contains("vega")
                || lower.contains(" vii")
                || lower.contains(" w");
            return !has_discrete_tag;
        }

        false
    }

    /// True for AMD's mobile-iGPU naming: "Radeon" plus a bare three-digit
    /// model number suffixed with M ("Radeon 780M Graphics", "Radeon 890M").
    /// No discrete card uses that shape — discrete mobile parts carry an RX
    /// prefix ("Radeon RX 7900M") — so the pattern is conclusive regardless
    /// of reported VRAM.
    fn is_amd_mobile_igpu_name(name: &str) -> bool {
        let lower = name.to_lowercase();
        if !lower.contains("radeon") || lower.contains("rx") {
            return false;
        }
        lower
            .split(|c: char| !c.is_ascii_alphanumeric())
            .any(|tok| {
                tok.len() == 4 && tok.ends_with('m') && tok[..3].chars().all(|c| c.is_ascii_digit())
            })
    }

    /// WMI AdapterRAM is a 32-bit field, capped at ~4 GB.
    /// If reported value is suspiciously low, estimate from GPU name.
    fn resolve_wmi_vram(raw_bytes: u64, name: &str) -> Option<f64> {
        let mut vram_gb = raw_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
        if vram_gb < 0.1 || (vram_gb <= 4.1 && estimate_vram_from_name(name) > 4.1) {
            let estimated = estimate_vram_from_name(name);
            if estimated > 0.0 {
                vram_gb = estimated;
            }
        }
        if vram_gb > 0.0 { Some(vram_gb) } else { None }
    }

    /// Infer the most likely inference backend from a GPU name string.
    fn infer_gpu_backend(name: &str) -> GpuBackend {
        let lower = name.to_lowercase();
        if lower.contains("nvidia")
            || lower.contains("geforce")
            || lower.contains("quadro")
            || lower.contains("tesla")
            || lower.contains("rtx")
        {
            GpuBackend::Cuda
        } else if lower.contains("amd") || lower.contains("radeon") || lower.contains("ati") {
            // On Windows, Vulkan is the primary inference path for AMD GPUs
            // (ROCm support on Windows is limited)
            GpuBackend::Vulkan
        } else if lower.contains("intel") || lower.contains("arc") {
            GpuBackend::Sycl
        } else {
            GpuBackend::Vulkan
        }
    }

    /// Detect Intel GPUs (integrated or discrete Arc) via lspci, with a sysfs
    /// vendor-ID fallback when lspci is unavailable.
    ///
    /// Dedicated VRAM for discrete Arc cards is read from sysfs per PCI
    /// address (issue #609): the `xe` driver exposes per-tile
    /// `tileN/physical_vram_size_bytes` and i915 exposes
    /// `drm/cardN/lmem_total_bytes` (`mem_info_vram_total` is amdgpu-only).
    /// Integrated GPUs (always at PCI address 00:02.0 on Intel platforms)
    /// share system RAM and are reported as unified-memory devices with the
    /// full RAM pool, matching the AMD APU and Apple Silicon conventions.
    fn detect_intel_gpus(total_ram_gb: f64) -> Vec<GpuInfo> {
        if let Some(text) = Self::lspci_output() {
            let gpus = Self::parse_intel_gpus_from_lspci(
                &text,
                total_ram_gb,
                Self::intel_dgpu_vram_gb_from_sysfs,
            );
            if !gpus.is_empty() {
                return gpus;
            }
        }

        // Fallback: lspci unavailable — sysfs vendor ID at least tells us an
        // Intel GPU exists, but not whether it's integrated or discrete.
        if let Ok(entries) = std::fs::read_dir("/sys/class/drm") {
            for entry in entries.flatten() {
                let card_path = entry.path();
                let fname = match card_path.file_name().and_then(|f| f.to_str()) {
                    Some(f) => f,
                    None => continue,
                };
                if !fname.starts_with("card") || fname.contains('-') {
                    continue;
                }
                if let Ok(vendor) = std::fs::read_to_string(card_path.join("device/vendor"))
                    && vendor.trim() == "0x8086"
                {
                    // Dedicated VRAM (if any) identifies the card as discrete.
                    let vram_gb = Self::intel_dgpu_vram_gb_from_pci_dir(&card_path.join("device"));
                    return vec![GpuInfo {
                        name: "Intel Graphics".to_string(),
                        vram_gb,
                        backend: GpuBackend::Sycl,
                        count: 1,
                        unified_memory: false,
                    }];
                }
            }
        }

        Vec::new()
    }

    /// Classify Intel display controllers from `lspci -nnD` output.
    /// Separated from [`Self::detect_intel_gpus`] so real lspci captures can
    /// be used as regression fixtures; `dgpu_vram_gb` maps a discrete card's
    /// PCI address to its dedicated VRAM (sysfs in production, a fixture in
    /// tests).
    fn parse_intel_gpus_from_lspci(
        text: &str,
        total_ram_gb: f64,
        dgpu_vram_gb: impl Fn(&str) -> Option<f64>,
    ) -> Vec<GpuInfo> {
        let mut gpus = Vec::new();
        for line in text.lines() {
            let lower = line.to_lowercase();
            let is_display = lower.contains("vga compatible")
                || lower.contains("3d controller")
                || lower.contains("display controller");
            if !is_display || !line.contains("[8086:") {
                continue;
            }
            let name = Self::intel_name_from_lspci_line(line);
            // Intel iGPUs live at PCI 00:02.0 on the root complex; discrete
            // cards enumerate behind a bridge on a nonzero bus.
            let addr = line.split_whitespace().next().unwrap_or("");
            let integrated = addr.ends_with(":00:02.0") || addr == "00:02.0";
            if integrated {
                gpus.push(GpuInfo {
                    name: format!("{name} (integrated)"),
                    vram_gb: Some(total_ram_gb),
                    backend: GpuBackend::Sycl,
                    count: 1,
                    unified_memory: true,
                });
            } else {
                gpus.push(GpuInfo {
                    name,
                    vram_gb: dgpu_vram_gb(addr),
                    backend: GpuBackend::Sycl,
                    count: 1,
                    unified_memory: false,
                });
            }
        }
        gpus
    }

    /// Dedicated VRAM of a discrete Intel GPU, from sysfs by PCI address
    /// (domain-qualified, as printed by `lspci -nnD`, e.g. "0000:03:00.0").
    fn intel_dgpu_vram_gb_from_sysfs(pci_addr: &str) -> Option<f64> {
        Self::intel_dgpu_vram_gb_from_pci_dir(
            &std::path::Path::new("/sys/bus/pci/devices").join(pci_addr),
        )
    }

    /// Read a discrete Intel GPU's dedicated VRAM from its sysfs PCI device
    /// directory. The `xe` driver exposes one `tileN/physical_vram_size_bytes`
    /// per tile (summed here for multi-tile cards); i915 exposes a single
    /// `drm/cardN/lmem_total_bytes`. Returns `None` for iGPUs (neither file
    /// exists) or when the values are unreadable.
    fn intel_dgpu_vram_gb_from_pci_dir(dev_dir: &std::path::Path) -> Option<f64> {
        let mut total_bytes: u64 = 0;

        if let Ok(entries) = std::fs::read_dir(dev_dir) {
            for entry in entries.flatten() {
                let fname = entry.file_name();
                let fname = fname.to_string_lossy();
                if fname.starts_with("tile")
                    && let Ok(text) =
                        std::fs::read_to_string(entry.path().join("physical_vram_size_bytes"))
                    && let Ok(bytes) = text.trim().parse::<u64>()
                {
                    total_bytes += bytes;
                }
            }
        }

        if total_bytes == 0
            && let Ok(entries) = std::fs::read_dir(dev_dir.join("drm"))
        {
            for entry in entries.flatten() {
                let fname = entry.file_name();
                let fname = fname.to_string_lossy();
                if fname.starts_with("card")
                    && !fname.contains('-')
                    && let Ok(text) = std::fs::read_to_string(entry.path().join("lmem_total_bytes"))
                    && let Ok(bytes) = text.trim().parse::<u64>()
                {
                    total_bytes = bytes;
                    break;
                }
            }
        }

        (total_bytes > 0).then(|| total_bytes as f64 / 1_073_741_824.0)
    }

    /// Extract a readable GPU name from an Intel lspci line, e.g.
    /// `"... Intel Corporation Core Ultra 200V Series Processors Arc Graphics
    /// 130V/140V GPU [8086:64a0] (rev 04)"` → `"Intel Arc Graphics 130V/140V"`.
    fn intel_name_from_lspci_line(line: &str) -> String {
        let after = line
            .split_once("Intel Corporation")
            .map(|(_, r)| r)
            .unwrap_or(line);
        let cleaned = after.split(" [8086:").next().unwrap_or(after).trim();
        let mut name = if let Some(idx) = cleaned.find("Arc") {
            // Codename lines bracket the marketing name: "DG2 [Arc A770]".
            format!("Intel {}", cleaned[idx..].trim_end_matches(']'))
        } else if cleaned.is_empty() {
            "Intel Graphics".to_string()
        } else {
            format!("Intel {cleaned}")
        };
        if let Some(stripped) = name.strip_suffix(" GPU") {
            name = stripped.to_string();
        }
        name
    }

    /// Detect Apple Silicon GPU via system_profiler.
    /// Returns total system RAM as VRAM since memory is unified.
    /// The unified memory pool capacity is the total RAM -- it doesn't
    /// fluctuate with current usage the way available RAM does.
    fn detect_apple_gpu(total_ram_gb: f64) -> Option<f64> {
        // system_profiler only exists on macOS
        let output = std::process::Command::new("system_profiler")
            .arg("SPDisplaysDataType")
            .output()
            .ok()?;

        if !output.status.success() {
            return None;
        }

        let text = String::from_utf8(output.stdout).ok()?;

        // Apple Silicon GPUs show "Apple M1/M2/M3/M4" in the chipset line.
        // Discrete AMD/Intel GPUs on older Macs won't match.
        let is_apple_gpu = text.lines().any(|line| {
            let lower = line.to_lowercase();
            lower.contains("apple m") || lower.contains("apple gpu")
        });

        if is_apple_gpu {
            // Unified memory: GPU and CPU share the same RAM pool.
            // Report total RAM as the VRAM capacity.
            Some(total_ram_gb)
        } else {
            None
        }
    }

    /// Detect macOS Metal GPUs from system_profiler.
    ///
    /// This covers Intel Macs with built-in Intel graphics and discrete AMD
    /// Radeon GPUs. Apple Silicon is intentionally skipped because it is
    /// handled by `detect_apple_gpu` as unified memory.
    fn detect_macos_metal_gpus() -> Vec<GpuInfo> {
        if !cfg!(target_os = "macos") {
            return Vec::new();
        }

        let output = std::process::Command::new("system_profiler")
            .args(["SPDisplaysDataType", "-json"])
            .output();
        let Ok(output) = output else {
            return Vec::new();
        };
        if !output.status.success() {
            return Vec::new();
        }

        Self::parse_macos_metal_gpus_from_system_profiler_json(&output.stdout)
    }

    fn parse_macos_metal_gpus_from_system_profiler_json(data: &[u8]) -> Vec<GpuInfo> {
        let Ok(json) = serde_json::from_slice::<serde_json::Value>(data) else {
            return Vec::new();
        };
        let Some(displays) = json.get("SPDisplaysDataType").and_then(|v| v.as_array()) else {
            return Vec::new();
        };
        displays
            .iter()
            .filter_map(|entry| {
                let name = entry
                    .get("sppci_model")
                    .or_else(|| entry.get("_name"))
                    .and_then(|v| v.as_str())?
                    .trim()
                    .to_string();
                let lower = name.to_lowercase();
                if lower.contains("apple m") || lower.contains("apple gpu") {
                    return None;
                }

                let metal = entry
                    .get("spdisplays_mtlgpufamilysupport")
                    .and_then(|v| v.as_str())
                    .map(|s| !s.trim().is_empty())
                    .unwrap_or(false);
                if !metal {
                    return None;
                }

                let vram_gb = entry
                    .get("spdisplays_vram")
                    .or_else(|| entry.get("_spdisplays_vram"))
                    .or_else(|| entry.get("spdisplays_vram_shared"))
                    .and_then(|v| v.as_str())
                    .and_then(parse_memory_size);

                Some(GpuInfo {
                    name,
                    vram_gb,
                    backend: GpuBackend::Metal,
                    count: 1,
                    unified_memory: false,
                })
            })
            .collect()
    }

    fn has_command(command: &str) -> bool {
        let Some(path_var) = std::env::var_os("PATH") else {
            return false;
        };

        for path in std::env::split_paths(&path_var) {
            let candidate = path.join(command);
            if candidate.is_file() {
                return true;
            }

            #[cfg(target_os = "windows")]
            for ext in [".exe", ".cmd", ".bat", ".com"] {
                let candidate = path.join(format!("{command}{ext}"));
                if candidate.is_file() {
                    return true;
                }
            }
        }

        false
    }

    /// Detect GPUs via Vulkan. This is especially useful on Android/Termux,
    /// where vendor-specific Linux utilities may be unavailable.
    fn detect_vulkan_gpu_info() -> Vec<GpuInfo> {
        if !Self::has_command("vulkaninfo") {
            return Vec::new();
        }

        let output = match std::process::Command::new("vulkaninfo")
            .arg("--summary")
            .output()
        {
            Ok(o) if o.status.success() => o,
            _ => match std::process::Command::new("vulkaninfo").output() {
                Ok(o) if o.status.success() => o,
                _ => return Vec::new(),
            },
        };

        let text = String::from_utf8_lossy(&output.stdout);
        let mut grouped: BTreeMap<String, u32> = BTreeMap::new();

        for name in Self::parse_vulkan_device_names(&text) {
            if Self::is_software_vulkan_device(&name) {
                continue;
            }
            *grouped.entry(name).or_insert(0) += 1;
        }

        grouped
            .into_iter()
            .map(|(name, count)| GpuInfo {
                backend: GpuBackend::Vulkan,
                count,
                name,
                unified_memory: false,
                vram_gb: None,
            })
            .collect()
    }

    fn is_same_gpu_name(existing_name: &str, candidate_name: &str) -> bool {
        if Self::normalize_gpu_name_for_dedupe(existing_name)
            == Self::normalize_gpu_name_for_dedupe(candidate_name)
        {
            return true;
        }

        // ROCm reports AMD GPUs using a generic family name that lists multiple
        // model variants separated by "/" (e.g. "Radeon RX 7700S/7600/7600S/7600M
        // XT/PRO W7600"), while Vulkan/RADV reports the specific model with a
        // driver codename suffix (e.g. "AMD Radeon RX 7600 XT (RADV NAVI33)").
        // These refer to the same physical GPU but never match via exact
        // normalization, so we do a secondary check: if both names contain "amd"
        // or "radeon" and share at least one 3-5 digit model number, treat them
        // as the same device.
        let e_lower = existing_name.to_lowercase();
        let c_lower = candidate_name.to_lowercase();
        let is_amd = |s: &str| s.contains("radeon") || s.starts_with("amd ") || s.contains(" amd ");
        if is_amd(&e_lower) && is_amd(&c_lower) {
            let e_nums = Self::extract_gpu_model_numbers(&e_lower);
            let c_nums = Self::extract_gpu_model_numbers(&c_lower);
            if !e_nums.is_empty() && e_nums.iter().any(|n| c_nums.contains(n)) {
                return true;
            }
        }

        // Intel: lspci reports platform names ("Intel Arc Graphics 130V/140V
        // (integrated)") while Mesa/Vulkan reports codenames ("Intel(R)
        // Arc(tm) Graphics (LNL)"). Same-model-number matches (A770 vs
        // "Arc A770") are the same device; an integrated entry also matches
        // a Vulkan Intel device with no model number of its own, since a
        // platform has at most one Intel iGPU.
        let is_intel = |s: &str| s.contains("intel");
        if is_intel(&e_lower) && is_intel(&c_lower) {
            let e_nums = Self::extract_gpu_model_numbers(&e_lower);
            let c_nums = Self::extract_gpu_model_numbers(&c_lower);
            if !e_nums.is_empty() && e_nums.iter().any(|n| c_nums.contains(n)) {
                return true;
            }
            // Arc Pro cards use 2-digit model numbers ("Pro B70") that the
            // 3-5 digit extractor drops; compare full letter-prefixed model
            // tokens (a770, b580, b70) as well.
            let e_toks = Self::extract_arc_model_tokens(&e_lower);
            let c_toks = Self::extract_arc_model_tokens(&c_lower);
            if !e_toks.is_empty() && e_toks.iter().any(|t| c_toks.contains(t)) {
                return true;
            }
            // An integrated entry matches a Vulkan Intel device only when the
            // latter has no model identifier at all — a two-digit Arc Pro
            // model ("B70") is a dGPU, not the platform iGPU.
            if (e_lower.contains("(integrated)") && c_nums.is_empty() && c_toks.is_empty())
                || (c_lower.contains("(integrated)") && e_nums.is_empty() && e_toks.is_empty())
            {
                return true;
            }
        }

        false
    }

    /// Extract Intel Arc model tokens — a series letter (A/B/C/D) followed by
    /// 2-4 digits, e.g. "a770", "b580", "b70" — from a lowercased GPU name.
    fn extract_arc_model_tokens(name: &str) -> Vec<String> {
        name.split(|c: char| !c.is_ascii_alphanumeric())
            .filter(|tok| {
                (3..=5).contains(&tok.len())
                    && matches!(tok.as_bytes()[0], b'a' | b'b' | b'c' | b'd')
                    && tok.as_bytes()[1..].iter().all(u8::is_ascii_digit)
            })
            .map(str::to_string)
            .collect()
    }

    /// Extract 3-5 digit numeric tokens from a GPU name (e.g. "7600", "6800").
    /// Used to compare AMD family names from ROCm against specific model names
    /// from Vulkan/RADV for deduplication.
    fn extract_gpu_model_numbers(name: &str) -> Vec<String> {
        let mut numbers = Vec::new();
        let mut current = String::new();
        for c in name.chars() {
            if c.is_ascii_digit() {
                current.push(c);
            } else {
                if current.len() >= 3 && current.len() <= 5 {
                    numbers.push(current.clone());
                }
                current.clear();
            }
        }
        if current.len() >= 3 && current.len() <= 5 {
            numbers.push(current);
        }
        numbers
    }

    fn normalize_gpu_name_for_dedupe(name: &str) -> String {
        let mut normalized = String::with_capacity(name.len());
        let mut last_was_separator = true;

        for ch in name.chars().flat_map(char::to_lowercase) {
            if ch.is_alphanumeric() {
                normalized.push(ch);
                last_was_separator = false;
            } else if !last_was_separator {
                normalized.push(' ');
                last_was_separator = true;
            }
        }

        normalized.trim().to_string()
    }

    fn parse_vulkan_device_names(text: &str) -> Vec<String> {
        let mut names = Vec::new();

        for line in text.lines() {
            let trimmed = line.trim();
            if trimmed.is_empty() {
                continue;
            }

            if let Some((key, value)) = trimmed.split_once('=')
                && key.trim().eq_ignore_ascii_case("deviceName")
            {
                let name = value.trim();
                if !name.is_empty() {
                    names.push(name.to_string());
                }
                continue;
            }

            if let Some(rest) = trimmed.strip_prefix("GPU id")
                && let Some(start) = rest.find('(')
                && let Some(end) = rest.rfind(')')
                && end > start + 1
            {
                let name = rest[start + 1..end].trim();
                if !name.is_empty() {
                    names.push(name.to_string());
                }
            }
        }

        names
    }

    fn is_software_vulkan_device(name: &str) -> bool {
        let lower = name.to_lowercase();
        // Software rasterizers / CPU emulation
        if lower.contains("llvmpipe")
            || lower.contains("lavapipe")
            || lower.contains("swiftshader")
            || lower.contains("software rasterizer")
        {
            return true;
        }
        // CPU compute devices exposed as Vulkan by Mesa/RADV.
        // These appear when ROCm or Mesa exposes the CPU's compute
        // engine as a Vulkan device (e.g. "AMD Ryzen 7 9800X3D
        // 8-Core Processor (RADV RAPHAEL_MENDOCINO)").  CPUs are
        // not inference GPUs and should never be scored as one.
        if lower.contains("core processor") {
            return true;
        }
        false
    }

    /// Detect Ascend NPUs via npu-smi. Returns a vector of NPU info.
    fn detect_ascend_npus() -> Vec<GpuInfo> {
        // 1. Get the list of IDs
        let list_output = match std::process::Command::new("npu-smi")
            .args(["info", "-l"])
            .output()
        {
            Ok(o) if o.status.success() => o,
            _ => return Vec::new(),
        };

        let list_stdout = String::from_utf8_lossy(&list_output.stdout);

        // Extracting IDs: ["0", "1", "2"...]
        let ids: Vec<String> = list_stdout
            .lines()
            .filter(|line| line.contains("NPU ID"))
            .filter_map(|line| line.split(':').next_back())
            .map(|s| s.trim().to_string())
            .collect();

        if ids.is_empty() {
            return Vec::new();
        }

        let mut npu_infos: Vec<GpuInfo> = Vec::new();
        let npu_name = "Ascend NPU";

        // 2. Loop through NPUs
        for id in &ids {
            let mem_output = std::process::Command::new("npu-smi")
                .args(["info", "-t", "memory", "-i", id])
                .output();

            if let Ok(o) = mem_output {
                let s = String::from_utf8_lossy(&o.stdout);

                // Parse HBM Capacity (e.g., from "HBM Capacity(MB) : 65536")
                let mem = s
                    .lines()
                    .find(|l| l.contains("HBM Capacity"))
                    .and_then(|l| l.split(':').next_back())
                    .and_then(|v| v.split_whitespace().next())
                    .and_then(|num| num.parse::<u64>().ok())
                    .unwrap_or(0);

                let npu_info = GpuInfo {
                    name: npu_name.to_string(),
                    vram_gb: Some((mem as f64) / 1024.0),
                    backend: GpuBackend::Ascend,
                    count: 1,
                    unified_memory: false,
                };
                npu_infos.push(npu_info);
            }
        }

        npu_infos
    }

    /// Fallback for available RAM when sysinfo returns 0.
    /// Tries total - used first, then macOS vm_stat parsing.
    fn available_ram_fallback(sys: &System, total_bytes: u64, total_gb: f64) -> f64 {
        // Try total - used from sysinfo (may also use vm_statistics64 internally)
        let used = sys.used_memory();
        if used > 0 && used < total_bytes {
            return (total_bytes - used) as f64 / (1024.0 * 1024.0 * 1024.0);
        }

        // macOS fallback: parse vm_stat output
        if let Some(avail) = Self::available_ram_from_vm_stat() {
            return avail;
        }

        // Last resort: assume 80% of total is available (conservative)
        total_gb * 0.8
    }

    /// Parse macOS `vm_stat` to compute available memory.
    /// Available ≈ (free + inactive + purgeable) * page_size
    fn available_ram_from_vm_stat() -> Option<f64> {
        let output = std::process::Command::new("vm_stat").output().ok()?;
        if !output.status.success() {
            return None;
        }
        let text = String::from_utf8(output.stdout).ok()?;

        // First line: "Mach Virtual Memory Statistics: (page size of NNNNN bytes)"
        let page_size: u64 = text
            .lines()
            .next()
            .and_then(|line| {
                line.split("page size of ")
                    .nth(1)?
                    .split(' ')
                    .next()?
                    .parse()
                    .ok()
            })
            .unwrap_or(16384); // Apple Silicon default is 16 KB pages

        let mut free: u64 = 0;
        let mut inactive: u64 = 0;
        let mut purgeable: u64 = 0;

        for line in text.lines() {
            if let Some(val) = Self::parse_vm_stat_line(line, "Pages free") {
                free = val;
            } else if let Some(val) = Self::parse_vm_stat_line(line, "Pages inactive") {
                inactive = val;
            } else if let Some(val) = Self::parse_vm_stat_line(line, "Pages purgeable") {
                purgeable = val;
            }
        }

        let available_bytes = (free + inactive + purgeable) * page_size;
        if available_bytes > 0 {
            Some(available_bytes as f64 / (1024.0 * 1024.0 * 1024.0))
        } else {
            None
        }
    }

    /// Parse a single vm_stat line like "Pages free:    123456."
    fn parse_vm_stat_line(line: &str, key: &str) -> Option<u64> {
        if !line.starts_with(key) {
            return None;
        }
        line.split(':')
            .nth(1)?
            .trim()
            .trim_end_matches('.')
            .parse()
            .ok()
    }

    fn detect_cpu_name(sys: &System) -> String {
        if let Some(cpu_name) = sys
            .cpus()
            .iter()
            .map(|cpu| cpu.brand().trim())
            .find(|brand| !brand.is_empty() && !brand.eq_ignore_ascii_case("unknown"))
        {
            return cpu_name.to_string();
        }

        if let Some(cpu_name) = Self::read_cpu_name_from_proc_cpuinfo() {
            return cpu_name;
        }

        if let Some(cpu_name) = Self::read_android_soc_name() {
            return cpu_name;
        }

        "Unknown CPU".to_string()
    }

    fn read_cpu_name_from_proc_cpuinfo() -> Option<String> {
        #[cfg(target_os = "linux")]
        {
            let text = std::fs::read_to_string("/proc/cpuinfo").ok()?;
            Self::parse_cpu_name_from_cpuinfo(&text)
        }

        #[cfg(not(target_os = "linux"))]
        {
            None
        }
    }

    fn parse_cpu_name_from_cpuinfo(text: &str) -> Option<String> {
        for key in ["model name", "hardware", "processor", "cpu model", "model"] {
            for line in text.lines() {
                let Some((lhs, rhs)) = line.split_once(':') else {
                    continue;
                };
                if lhs.trim().eq_ignore_ascii_case(key) {
                    let candidate = rhs.trim();
                    if !candidate.is_empty() && !candidate.eq_ignore_ascii_case("unknown") {
                        return Some(candidate.to_string());
                    }
                }
            }
        }

        None
    }

    fn read_android_soc_name() -> Option<String> {
        #[cfg(target_os = "linux")]
        {
            let output = std::process::Command::new("getprop")
                .arg("ro.soc.model")
                .output()
                .ok()?;
            if !output.status.success() {
                return None;
            }

            let model = String::from_utf8(output.stdout).ok()?;
            let model = model.trim();
            if model.is_empty() {
                return None;
            }

            Some(model.to_string())
        }

        #[cfg(not(target_os = "linux"))]
        {
            None
        }
    }

    /// Override the primary GPU's VRAM with a user-specified value (in GB).
    /// This is used by the `--memory` CLI flag when GPU autodetection fails.
    /// If no GPU was detected, this creates a synthetic GPU entry.
    pub fn with_gpu_memory_override(mut self, vram_gb: f64) -> Self {
        if self.gpus.is_empty() {
            // No GPU was detected; create a synthetic one.
            let backend = if cfg!(target_arch = "aarch64")
                || self.cpu_name.to_lowercase().contains("apple")
            {
                GpuBackend::Metal
            } else {
                GpuBackend::Cuda
            };
            self.gpus.push(GpuInfo {
                name: "User-specified GPU".to_string(),
                vram_gb: Some(vram_gb),
                backend,
                count: 1,
                unified_memory: false,
            });
            self.has_gpu = true;
            self.gpu_vram_gb = Some(vram_gb);
            self.total_gpu_vram_gb = Some(vram_gb);
            self.gpu_name = Some("User-specified GPU".to_string());
            self.gpu_count = 1;
            self.backend = backend;
        } else {
            // Override the primary (first) GPU's VRAM.
            self.gpus[0].vram_gb = Some(vram_gb);
            self.gpu_vram_gb = Some(vram_gb);
            // Update total VRAM: per-card VRAM * count.
            let count = self.gpus[0].count;
            self.total_gpu_vram_gb = Some(vram_gb * count as f64);
            self.has_gpu = true;
        }
        // The detected GPU-available cap describes the real host, not the
        // simulated one; clear it rather than report a stale figure.
        self.gpu_available_gb = None;
        self
    }

    /// Override total and available system RAM with a user-specified value (in GB).
    /// Sets available RAM to 90% of the override to model typical system usage.
    /// On unified-memory systems (Apple Silicon), this also updates GPU VRAM
    /// to stay consistent — use `--memory` after `--ram` to override VRAM separately.
    pub fn with_ram_override(mut self, ram_gb: f64) -> Self {
        self.total_ram_gb = ram_gb;
        self.available_ram_gb = ram_gb * 0.9;
        // The detected GPU-available cap describes the real host, not the
        // simulated one; clear it rather than report a stale figure.
        self.gpu_available_gb = None;
        if self.unified_memory {
            self.gpu_vram_gb = Some(ram_gb);
            self.total_gpu_vram_gb = Some(ram_gb);
            for gpu in &mut self.gpus {
                if gpu.unified_memory {
                    gpu.vram_gb = Some(ram_gb);
                }
            }
        }
        self
    }

    /// Override the detected CPU core count with a user-specified value.
    pub fn with_cpu_core_override(mut self, cores: usize) -> Self {
        self.total_cpu_cores = cores;
        self
    }

    pub fn display(&self) {
        println!("\n=== System Specifications ===");
        println!("CPU: {} ({} cores)", self.cpu_name, self.total_cpu_cores);
        println!("Total RAM: {:.2} GB", self.total_ram_gb);
        println!("Available RAM: {:.2} GB", self.available_ram_gb);
        if let Some(bw) = measured_ram_bandwidth_gbps() {
            println!("RAM Bandwidth: ~{bw:.0} GB/s (measured)");
        }
        println!("Backend: {}", self.backend.label());

        if self.gpus.is_empty() {
            println!("GPU: Not detected");
        } else {
            for (i, gpu) in self.gpus.iter().enumerate() {
                let prefix = if self.gpus.len() > 1 {
                    format!("GPU {}: ", i + 1)
                } else {
                    "GPU: ".to_string()
                };
                if gpu.unified_memory {
                    println!(
                        "{}{}",
                        prefix,
                        format_unified_memory_line(
                            &gpu.name,
                            gpu.vram_gb.unwrap_or(0.0),
                            self.gpu_available_gb,
                            gpu.backend.label(),
                        )
                    );
                } else {
                    match gpu.vram_gb {
                        Some(vram) if vram > 0.0 => {
                            if gpu.count > 1 {
                                let total_vram = vram * gpu.count as f64;
                                println!(
                                    "{}{} x{} ({:.2} GB VRAM each = {:.0} GB total, {})",
                                    prefix,
                                    gpu.name,
                                    gpu.count,
                                    vram,
                                    total_vram,
                                    gpu.backend.label()
                                );
                            } else {
                                println!(
                                    "{}{} ({:.2} GB VRAM, {})",
                                    prefix,
                                    gpu.name,
                                    vram,
                                    gpu.backend.label()
                                );
                            }
                        }
                        Some(_) => println!(
                            "{}{} (shared system memory, {})",
                            prefix,
                            gpu.name,
                            gpu.backend.label()
                        ),
                        None => println!(
                            "{}{} (VRAM unknown, {})",
                            prefix,
                            gpu.name,
                            gpu.backend.label()
                        ),
                    }
                }
            }
        }
        println!();
    }
}

/// Format the unified-memory GPU line. When the GPU-available figure is known
/// (Apple Silicon), it is shown alongside the total shared pool; otherwise the
/// line falls back to reporting the shared pool alone. The GPU-available
/// figure is clamped to the shared pool: Metal passes `iogpu.wired_limit_mb`
/// through verbatim, so a sysctl set above physical RAM would otherwise be
/// reported as-is.
pub(crate) fn format_unified_memory_line(
    name: &str,
    shared_gb: f64,
    gpu_available_gb: Option<f64>,
    backend_label: &str,
) -> String {
    match gpu_available_gb {
        Some(available) => {
            let available = available.min(shared_gb);
            format!(
                "{name} (unified memory, {available:.2} GB GPU-available of {shared_gb:.2} GB shared, {backend_label})"
            )
        }
        None => format!("{name} (unified memory, {shared_gb:.2} GB shared, {backend_label})"),
    }
}

/// Query how much unified memory the GPU may wire on Apple Silicon.
///
/// Metal's `recommendedMaxWorkingSetSize` reflects the kernel's wired limit,
/// including a manual `sysctl iogpu.wired_limit_mb` override — verified on an
/// M3 (16 GB): raising the sysctl to 14000 MiB made Metal report exactly
/// 14000 MiB — so no separate sysctl read is needed. Returns `None` when no
/// default Metal device is available. Reported in base-2 GB to match the rest
/// of the display.
#[cfg(target_os = "macos")]
pub(crate) fn detect_gpu_available_gb() -> Option<f64> {
    use objc2_metal::{MTLCreateSystemDefaultDevice, MTLDevice};
    let device = MTLCreateSystemDefaultDevice()?;
    Some(device.recommendedMaxWorkingSetSize() as f64 / (1024.0 * 1024.0 * 1024.0))
}

/// Non-macOS platforms have no Metal working-set concept.
#[cfg(not(target_os = "macos"))]
pub(crate) fn detect_gpu_available_gb() -> Option<f64> {
    None
}

/// Parse a human-readable memory size string into gigabytes.
/// Accepts formats: "32G", "32g", "32GB", "32gb", "32000M", "32000m", "32000MB", etc.
/// Returns `None` if the input is malformed.
pub fn parse_memory_size(s: &str) -> Option<f64> {
    let s = s.trim();
    if s.is_empty() {
        return None;
    }

    // Split into numeric part and suffix
    let num_end = s
        .find(|c: char| !c.is_ascii_digit() && c != '.')
        .unwrap_or(s.len());
    let (num_str, suffix) = s.split_at(num_end);
    let value: f64 = num_str.parse().ok()?;
    if value < 0.0 {
        return None;
    }

    let suffix = suffix.trim().to_lowercase();
    match suffix.as_str() {
        "g" | "gb" | "gib" | "" => Some(value),     // already in GB
        "m" | "mb" | "mib" => Some(value / 1024.0), // MB → GB
        "t" | "tb" | "tib" => Some(value * 1024.0), // TB → GB
        _ => None,
    }
}

pub fn is_running_in_wsl() -> bool {
    static IS_WSL: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
    *IS_WSL.get_or_init(detect_running_in_wsl)
}

fn detect_running_in_wsl() -> bool {
    if !cfg!(target_os = "linux") {
        return false;
    }

    if std::env::var_os("WSL_INTEROP").is_some() || std::env::var_os("WSL_DISTRO_NAME").is_some() {
        return true;
    }

    ["/proc/sys/kernel/osrelease", "/proc/version"]
        .iter()
        .any(|path| {
            std::fs::read_to_string(path)
                .map(|text| text.to_ascii_lowercase().contains("microsoft"))
                .unwrap_or(false)
        })
}

/// Check if the CPU name indicates an AMD APU with unified memory architecture.
/// These APUs share the full system RAM between CPU and GPU (like Apple Silicon).
/// Currently covers:
///  - Ryzen AI MAX / MAX+ (Strix Halo): up to 128 GB unified.
///  - Ryzen AI 9 / 7 / 5 (Strix Point, Krackan Point): configurable shared
///    memory, users can allocate most of system RAM to GPU via BIOS.
/// All Ryzen AI APUs have integrated Radeon GPUs that share system memory.
/// Placeholder GPU-name tokens that mean detection failed to read a real
/// marketing name (rocm-smi prints "N/A" when it can't load libdrm). These
/// must never be used as an actual GPU identity.
fn is_placeholder_gpu_name(name: &str) -> bool {
    let lower = name.trim().to_lowercase();
    matches!(
        lower.as_str(),
        "" | "n/a" | "na" | "n-a" | "n\\a" | "unknown" | "none" | "null" | "-"
    )
}

/// Whether a GPU name is too generic to identify the specific model, so a more
/// descriptive fallback (e.g. the APU model string) should be preferred.
fn is_generic_amd_gpu_name(name: &str) -> bool {
    let lower = name.trim().to_lowercase();
    matches!(
        lower.as_str(),
        "amd gpu" | "amd/ati" | "radeon graphics" | "amd radeon graphics"
    )
}

fn is_amd_unified_memory_apu(cpu_name: &str) -> bool {
    let lower = cpu_name.to_lowercase();
    // Only "Ryzen AI MAX" / "Ryzen AI MAX+" APUs have a large unified memory
    // pool shared between CPU and GPU (similar to Apple Silicon).
    // Regular Ryzen AI chips (e.g. HX 370, HX 365) have a standard small iGPU
    // and should NOT be treated as unified-memory systems.
    // Examples that match:
    //   "AMD Ryzen AI MAX+ 395 w/ Radeon 8060S"
    //   "AMD Ryzen AI MAX 390"
    if lower.contains("ryzen ai max") {
        return true;
    }
    false
}

/// True for any AMD APU whose iGPU shares system RAM — the population that can
/// carry a BIOS UMA carveout. Windows bakes the iGPU into the CPU brand string
/// ("AMD Ryzen 7 8845HS w/ Radeon 780M Graphics"), so Ryzen + Radeon in the
/// CPU name is the reliable signal; desktop CPUs without an iGPU carveout
/// ("AMD Ryzen 9 7950X") don't carry it.
fn is_amd_apu(cpu_name: &str) -> bool {
    let lower = cpu_name.to_lowercase();
    lower.contains("ryzen") && lower.contains("radeon")
}

/// Minimum gap between installed DIMM capacity and the OS view of RAM before
/// the physical figure is preferred. Ordinary firmware/reserved overhead is
/// well under 1 GB; BIOS UMA carveout options start at 1 GB.
const UMA_CARVEOUT_MIN_GAP_GB: f64 = 1.0;

/// Pure decision half of the issue-#810 RAM fix: prefer the physical DIMM
/// total only when it exceeds the sysinfo figure by a clear carveout-sized
/// margin, so machines without a carveout keep the OS view (which already
/// nets out normal reserved memory).
fn apply_ram_carveout_override(sysinfo_total_gb: f64, physical_total_gb: f64) -> f64 {
    if physical_total_gb - sysinfo_total_gb >= UMA_CARVEOUT_MIN_GAP_GB {
        physical_total_gb
    } else {
        sysinfo_total_gb
    }
}

/// Query total installed physical RAM on Windows by summing DIMM capacities
/// from WMI `Win32_PhysicalMemory`. Unlike `sysinfo::System::total_memory()`
/// or `Win32_ComputerSystem.TotalPhysicalMemory`, this reads directly from
/// SMBIOS and is unaffected by BIOS-level GPU UMA carveouts.
///
/// On AMD Ryzen AI MAX / MAX+ systems where users configure e.g. 96 GB as GPU
/// UMA in BIOS, the OS only sees the remaining ~32 GB as system RAM, causing
/// `sysinfo` to report 32 GB. `Win32_PhysicalMemory.Capacity` correctly sums
/// all installed DIMMs (e.g. 128 GB) regardless of that carveout.
///
/// Returns `None` when not on Windows, PowerShell is unavailable, or the
/// query fails; callers fall back to the sysinfo value.
fn detect_windows_physical_total_ram_gb() -> Option<f64> {
    if !cfg!(target_os = "windows") {
        return None;
    }
    let output = std::process::Command::new("powershell")
        .args([
            "-NoProfile",
            "-Command",
            "(Get-CimInstance Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum",
        ])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let text = String::from_utf8(output.stdout).ok()?;
    let bytes: u64 = text.trim().parse().ok()?;
    if bytes == 0 {
        return None;
    }
    Some(bytes as f64 / (1024.0 * 1024.0 * 1024.0))
}

/// PowerShell one-liner that dumps `DriverDesc|<bytes>` for every display
/// adapter, reading the driver-published 64-bit VRAM size from the registry.
///
/// Modern WDDM drivers (Windows 10 and later, all three vendors) write
/// `HardwareInformation.qwMemorySize` — a REG_QWORD in bytes. Microsoft never
/// documented it, so its absence on older Windows or an exotic driver is
/// expected rather than a fault; the query then falls back to the documented
/// `HardwareInformation.MemorySize`, which may be a REG_BINARY blob or a
/// REG_DWORD. Both widths are decoded here because PowerShell is the only
/// layer that still knows the registry type. REG_DWORD values above 2 GiB
/// arrive as a negative `Int32` and are folded back into range — without that,
/// the fallback would be dead over exactly the 2-4 GB span it exists to cover.
///
/// Shared with `llmfit doctor` so diagnostic reports carry the same evidence
/// detection uses.
pub const WINDOWS_REGISTRY_VRAM_PS_COMMAND: &str = r"Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\*' -ErrorAction SilentlyContinue | ForEach-Object { $d = $_.DriverDesc; $m = $_.'HardwareInformation.qwMemorySize'; if ($null -eq $m) { $m = $_.'HardwareInformation.MemorySize' }; if ($m -is [byte[]]) { if ($m.Length -ge 8) { $m = [System.BitConverter]::ToUInt64($m, 0) } elseif ($m.Length -ge 4) { $m = [System.BitConverter]::ToUInt32($m, 0) } else { $m = $null } } elseif ($m -is [int] -and $m -lt 0) { $m = [long]$m + 4294967296 }; if ($d -and $m) { $d + '|' + $m } }";

/// Normalize a GPU product name for cross-source comparison: lowercase, with
/// trademark decoration and punctuation reduced to whitespace, then whitespace
/// collapsed. WMI and the registry disagree on these decorations for the same
/// adapter ("AMD Radeon(TM) Graphics" vs "AMD Radeon Graphics"), and neither
/// string contains the other, so matching has to see through them.
///
/// The textual markers are expanded first, because stripping punctuation alone
/// would turn "(TM)" into a literal "tm" glued to the preceding word. Every
/// remaining non-alphanumeric character is then dropped, which also covers the
/// symbol forms: a ™ or ® arrives from PowerShell 5.1 in the console OEM
/// codepage, so a lossy decode leaves U+FFFD rather than the character itself,
/// and no amount of marker matching would remove it.
fn normalize_gpu_name_for_match(name: &str) -> String {
    let expanded = name
        .to_lowercase()
        .replace("(tm)", " ")
        .replace("(r)", " ")
        .replace("(c)", " ");
    expanded
        .chars()
        .map(|c| if c.is_alphanumeric() { c } else { ' ' })
        .collect::<String>()
        .split_whitespace()
        .collect::<Vec<_>>()
        .join(" ")
}

/// Read each display adapter's true VRAM size from the Windows registry.
///
/// Returns `(DriverDesc, bytes)` pairs, or an empty vector when not on
/// Windows, when PowerShell is unavailable, or when no driver publishes a
/// memory size — callers then keep their existing estimate.
fn detect_windows_registry_vram() -> Vec<(String, u64)> {
    if !cfg!(target_os = "windows") {
        return Vec::new();
    }
    let Ok(output) = std::process::Command::new("powershell")
        .args(["-NoProfile", "-Command", WINDOWS_REGISTRY_VRAM_PS_COMMAND])
        .output()
    else {
        return Vec::new();
    };
    if !output.status.success() {
        return Vec::new();
    }
    // Windows PowerShell 5.1 writes stdout in the console OEM codepage, so a
    // single non-ASCII byte in any adapter name (® and ™ do appear in INFs)
    // would make a strict decode discard every entry, not just that line.
    let text = String::from_utf8_lossy(&output.stdout);
    SystemSpecs::parse_windows_registry_vram(&text)
}

/// Read total system RAM from /proc/meminfo (Linux only).
/// Used as the unified memory pool on NVIDIA Tegra / Grace Blackwell platforms
/// where nvidia-smi cannot report dedicated VRAM.
fn read_proc_meminfo_total_gb() -> Option<f64> {
    let text = std::fs::read_to_string("/proc/meminfo").ok()?;
    for line in text.lines() {
        if let Some(rest) = line.strip_prefix("MemTotal:") {
            let kb: u64 = rest.split_whitespace().next()?.parse().ok()?;
            return Some(kb as f64 / (1024.0 * 1024.0));
        }
    }
    None
}

/// Effective system RAM bandwidth in GB/s, measured once per process with a
/// short multithreaded memcpy sweep (~100 ms total) and cached.
///
/// This is *achievable* streaming bandwidth (STREAM-copy convention: bytes
/// read + bytes written per pass), which is what MoE-offload expert streaming
/// actually sees — typically 60–80% of the spec-sheet peak. Returns `None`
/// if the measurement fails or produces an implausible value; callers should
/// fall back to a conservative constant.
pub fn measured_ram_bandwidth_gbps() -> Option<f64> {
    static MEASURED: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
    *MEASURED.get_or_init(measure_ram_bandwidth_gbps)
}

fn measure_ram_bandwidth_gbps() -> Option<f64> {
    use std::time::{Duration, Instant};

    // Per-thread working set (2 × 32 MiB) must comfortably exceed L3 so we
    // measure DRAM, not cache. A single thread rarely saturates multi-channel
    // memory controllers, so spread the sweep across up to 8 cores.
    const BUF_BYTES: usize = 32 * 1024 * 1024;
    const MEASURE_WINDOW: Duration = Duration::from_millis(80);

    let threads = std::thread::available_parallelism()
        .map(|n| n.get().min(8))
        .unwrap_or(4);

    let barrier = std::sync::Barrier::new(threads);
    let per_thread_gbps: Vec<f64> = std::thread::scope(|scope| {
        let barrier = &barrier;
        let handles: Vec<_> = (0..threads)
            .map(|_| {
                scope.spawn(move || {
                    let src = vec![1u8; BUF_BYTES];
                    let mut dst = vec![0u8; BUF_BYTES];
                    // Warmup pass faults pages in before the timed window.
                    dst.copy_from_slice(&src);
                    std::hint::black_box(&mut dst);
                    barrier.wait();
                    let start = Instant::now();
                    let mut passes = 0u64;
                    while start.elapsed() < MEASURE_WINDOW {
                        dst.copy_from_slice(&src);
                        std::hint::black_box(&mut dst);
                        passes += 1;
                    }
                    let secs = start.elapsed().as_secs_f64();
                    (passes as f64) * (2 * BUF_BYTES) as f64 / secs / 1e9
                })
            })
            .collect();
        handles.into_iter().filter_map(|h| h.join().ok()).collect()
    });

    if per_thread_gbps.len() != threads {
        return None;
    }
    let total: f64 = per_thread_gbps.iter().sum();
    // Sanity band: below 2 GB/s means the measurement was starved (heavy
    // contention, throttled VM); above 4000 GB/s means we measured cache.
    (2.0..=4000.0).contains(&total).then_some(total)
}

/// Estimate GPU memory bandwidth in GB/s from the GPU model name.
///
/// Token generation in LLM inference is memory-bandwidth-bound (each token
/// requires reading the full model weights once). Using per-GPU bandwidth
/// produces significantly more accurate tok/s estimates than a single
/// constant for all CUDA/ROCm/Metal devices.
///
/// References:
///  - kipply, "Transformer Inference Arithmetic" (2022)
///  - ggerganov, llama.cpp Apple Silicon benchmarks (Discussion #4167)
///  - Google, "Efficiently Scaling Transformer Inference" (arXiv:2211.05102)
///  - ggerganov, llama.cpp NVIDIA T4 benchmarks (Discussion #4225)
///
/// Returns `None` when the GPU is not recognized; callers should fall back
/// to the existing fixed-constant approach.
pub fn gpu_memory_bandwidth_gbps(name: &str) -> Option<f64> {
    let lower = name.to_lowercase();

    // ── NVIDIA Consumer (GeForce) ──────────────────────────────────
    // RTX 50 series (Blackwell)
    if lower.contains("5090") {
        return Some(1792.0);
    }
    if lower.contains("5080") {
        return Some(960.0);
    }
    if lower.contains("5070 ti") {
        return Some(896.0);
    }
    if lower.contains("5070") {
        return Some(672.0);
    }
    if lower.contains("5060 ti") {
        return Some(448.0);
    }
    if lower.contains("5060") {
        return Some(256.0);
    }

    // RTX 40 series (Ada Lovelace)
    if lower.contains("4090") {
        return Some(1008.0);
    }
    if lower.contains("4080 super") {
        return Some(736.0);
    }
    if lower.contains("4080") {
        return Some(717.0);
    }
    if lower.contains("4070 ti super") {
        return Some(672.0);
    }
    if lower.contains("4070 ti") {
        return Some(504.0);
    }
    if lower.contains("4070 super") {
        return Some(504.0);
    }
    if lower.contains("4070") {
        return Some(504.0);
    }
    if lower.contains("4060 ti") {
        return Some(288.0);
    }
    if lower.contains("4060") {
        return Some(272.0);
    }

    // RTX 30 series (Ampere)
    if lower.contains("3090 ti") {
        return Some(1008.0);
    }
    if lower.contains("3090") {
        return Some(936.0);
    }
    if lower.contains("3080 ti") {
        return Some(912.0);
    }
    if lower.contains("3080") {
        return Some(760.0);
    }
    if lower.contains("3070 ti") {
        return Some(608.0);
    }
    if lower.contains("3070") {
        return Some(448.0);
    }
    if lower.contains("3060 ti") {
        return Some(448.0);
    }
    if lower.contains("3060") {
        return Some(360.0);
    }

    // RTX 20 series (Turing)
    if lower.contains("2080 ti") {
        return Some(616.0);
    }
    if lower.contains("2080 super") {
        return Some(496.0);
    }
    if lower.contains("2080") {
        return Some(448.0);
    }
    if lower.contains("2070 super") {
        return Some(448.0);
    }
    if lower.contains("2070") {
        return Some(448.0);
    }
    if lower.contains("2060 super") {
        return Some(448.0);
    }
    if lower.contains("2060") {
        return Some(336.0);
    }

    // GTX 16 series (Turing, no RT cores)
    if lower.contains("1660 ti") {
        return Some(288.0);
    }
    if lower.contains("1660 super") {
        return Some(336.0);
    }
    if lower.contains("1660") {
        return Some(192.0);
    }
    if lower.contains("1650 super") {
        return Some(192.0);
    }
    if lower.contains("1650") {
        return Some(128.0);
    }

    // ── NVIDIA Data Center / Professional ──────────────────────────
    if lower.contains("h100 sxm") {
        return Some(3350.0);
    }
    if lower.contains("h100") {
        return Some(2039.0);
    } // PCIe
    if lower.contains("h200") {
        return Some(4800.0);
    }
    if lower.contains("a100 sxm") {
        return Some(2039.0);
    }
    if lower.contains("a100") {
        return Some(1555.0);
    } // PCIe 40GB
    if lower.contains("l40s") {
        return Some(864.0);
    }
    if lower.contains("l40") {
        return Some(864.0);
    }
    if lower.contains("l4") {
        return Some(300.0);
    }
    if lower.contains("a10g") {
        return Some(600.0);
    }
    if lower.contains("a10") {
        return Some(600.0);
    }
    if lower.contains("t4") {
        return Some(320.0);
    }
    if lower.contains("v100 sxm") {
        return Some(900.0);
    }
    if lower.contains("v100") {
        return Some(897.0);
    }
    if lower.contains("a6000") {
        return Some(768.0);
    }
    if lower.contains("a5000") {
        return Some(768.0);
    }
    if lower.contains("a4000") {
        return Some(448.0);
    }

    // ── AMD unified-memory APUs (Strix Halo) ───────────────────────
    // Ryzen AI MAX / MAX+ (Radeon 8050S/8060S): 256-bit LPDDR5X-8000.
    // Names vary by detection path: lspci ("Strix Halo [Radeon ...]"),
    // marketing ("Radeon 8060S"), or the cpu-derived fallback
    // ("AMD Ryzen AI MAX+ 395 w/ Radeon 8060S (integrated)").
    if lower.contains("8060s")
        || lower.contains("8050s")
        || lower.contains("strix halo")
        || lower.contains("ryzen ai max")
    {
        return Some(256.0);
    }

    // ── AMD Discrete (RDNA) ────────────────────────────────────────
    // RX 9000 series (RDNA 4)
    if lower.contains("9070 xt") {
        return Some(624.0);
    }
    if lower.contains("9070") {
        return Some(488.0);
    }

    // RX 7000 series (RDNA 3)
    if lower.contains("7900 xtx") {
        return Some(960.0);
    }
    if lower.contains("7900 xt") {
        return Some(800.0);
    }
    if lower.contains("7900 gre") {
        return Some(576.0);
    }
    if lower.contains("7800 xt") {
        return Some(624.0);
    }
    if lower.contains("7700 xt") {
        return Some(432.0);
    }
    if lower.contains("7600") {
        return Some(288.0);
    }

    // RX 6000 series (RDNA 2)
    if lower.contains("6950 xt") {
        return Some(576.0);
    }
    if lower.contains("6900 xt") {
        return Some(512.0);
    }
    if lower.contains("6800 xt") {
        return Some(512.0);
    }
    if lower.contains("6800") {
        return Some(512.0);
    }
    if lower.contains("6700 xt") {
        return Some(384.0);
    }
    if lower.contains("6600 xt") {
        return Some(256.0);
    }
    if lower.contains("6600") {
        return Some(224.0);
    }

    // AMD data center (CDNA)
    if lower.contains("mi300x") {
        return Some(5300.0);
    }
    if lower.contains("mi300") {
        return Some(5300.0);
    }
    if lower.contains("mi250x") {
        return Some(3277.0);
    }
    if lower.contains("mi250") {
        return Some(3277.0);
    }
    if lower.contains("mi210") {
        return Some(1638.0);
    }
    if lower.contains("mi100") {
        return Some(1229.0);
    }

    // ── Apple Silicon (unified memory bandwidth) ───────────────────
    if lower.contains("m5 max") {
        return Some(614.0);
    }
    if lower.contains("m5 pro") {
        return Some(307.0);
    }
    if lower.contains("m5") {
        return Some(153.6);
    }
    if lower.contains("m4 ultra") {
        return Some(819.0);
    }
    if lower.contains("m4 max") {
        return Some(546.0);
    }
    if lower.contains("m4 pro") {
        return Some(273.0);
    }
    if lower.contains("m4") {
        return Some(120.0);
    }
    if lower.contains("m3 ultra") {
        return Some(800.0);
    }
    if lower.contains("m3 max") {
        return Some(400.0);
    }
    if lower.contains("m3 pro") {
        return Some(150.0);
    }
    if lower.contains("m3") {
        return Some(100.0);
    }
    if lower.contains("m2 ultra") {
        return Some(800.0);
    }
    if lower.contains("m2 max") {
        return Some(400.0);
    }
    if lower.contains("m2 pro") {
        return Some(200.0);
    }
    if lower.contains("m2") {
        return Some(100.0);
    }
    if lower.contains("m1 ultra") {
        return Some(800.0);
    }
    if lower.contains("m1 max") {
        return Some(400.0);
    }
    if lower.contains("m1 pro") {
        return Some(200.0);
    }
    if lower.contains("m1") {
        return Some(68.0);
    }

    None
}

/// Returns the NVIDIA compute capability (major, minor) for a known GPU name.
/// Used to determine compatibility with quantization formats that require
/// specific hardware features (e.g. AWQ requires Turing+ / cc >= 7.5).
///
/// Returns `None` for non-NVIDIA GPUs or unrecognized models.
pub fn gpu_compute_capability(name: &str) -> Option<(u8, u8)> {
    let lower = name.to_lowercase();

    // ── Blackwell (RTX 50xx, B100/B200) ──────────────────────────
    if lower.contains("5090")
        || lower.contains("5080")
        || lower.contains("5070")
        || lower.contains("5060")
        || lower.contains("b200")
        || lower.contains("b100")
        || lower.contains("gb200")
        || lower.contains("gb100")
    {
        return Some((10, 0));
    }

    // ── Hopper (H100, H200) ─────────────────────────────────────
    if lower.contains("h100") || lower.contains("h200") {
        return Some((9, 0));
    }

    // ── Ada Lovelace (RTX 40xx, L4, L40/L40S) ──────────────────
    if lower.contains("4090")
        || lower.contains("4080")
        || lower.contains("4070")
        || lower.contains("4060")
        || lower.contains("l40")
        || lower.contains("l4")
    {
        return Some((8, 9));
    }

    // ── Ampere (RTX 30xx consumer = 8.6, A100/A10/A6000 = 8.0) ─
    if lower.contains("a100") {
        return Some((8, 0));
    }
    if lower.contains("3090")
        || lower.contains("3080")
        || lower.contains("3070")
        || lower.contains("3060")
        || lower.contains("a10")
        || lower.contains("a6000")
        || lower.contains("a5000")
        || lower.contains("a4000")
        || lower.contains("a2000")
        || lower.contains("a16")
    {
        return Some((8, 6));
    }

    // ── Turing (RTX 20xx, GTX 16xx, T4) ─────────────────────────
    if lower.contains("2080")
        || lower.contains("2070")
        || lower.contains("2060")
        || lower.contains("1660")
        || lower.contains("1650")
        || lower.contains("t4")
    {
        return Some((7, 5));
    }

    // ── Volta (V100, Titan V) ───────────────────────────────────
    if lower.contains("v100") || lower.contains("titan v") {
        return Some((7, 0));
    }

    // ── Pascal (P100, GTX 10xx, Titan X Pascal) ─────────────────
    if lower.contains("p100")
        || lower.contains("1080")
        || lower.contains("1070")
        || lower.contains("1060")
        || lower.contains("1050")
        || lower.contains("p40")
        || lower.contains("p4")
    {
        return Some((6, 1));
    }

    None
}

/// Minimum NVIDIA compute capability required by a quantization format
/// when running under vLLM. Based on vLLM's documented hardware support:
/// <https://docs.vllm.ai/en/latest/features/quantization/#supported-hardware>
///
/// Returns `None` for quantization formats that have no known CC restriction
/// (e.g. GGUF quants which run through llama.cpp, not vLLM).
pub fn quant_min_compute_capability(quantization: &str) -> Option<(u8, u8)> {
    match quantization {
        // AWQ requires Turing+ (int4 tensor-core kernels)
        "AWQ-4bit" | "AWQ-8bit" => Some((7, 5)),
        // GPTQ Marlin kernels require Turing+
        "GPTQ-Int4" | "GPTQ-Int8" => Some((7, 5)),
        _ => None,
    }
}

/// Check if a GPU name (including PCI device IDs from lspci) indicates an
/// NVIDIA unified memory SoC (Grace Blackwell / DGX Spark / GB-series).
/// Inside Docker, nvidia-smi may report the raw PCI device ID instead of the
/// friendly model name, e.g. "NVIDIA Corporation Device [10de:2e12] (rev a1)"
/// instead of "NVIDIA GB10".
fn is_nvidia_unified_memory_gpu(name: &str) -> bool {
    let lower = name.to_lowercase();
    // Friendly model names
    if lower.contains("gb10") || lower.contains("gb20") {
        return true;
    }
    // PCI device IDs (hex) — these are the known GB-series SoCs.
    // 10de:2e12 = GB10 (DGX Spark / Project DIGITS)
    if lower.contains("2e12") {
        return true;
    }
    // Jetson / older Tegra SoCs (Orin, Xavier, Nano, ...) run the legacy
    // nvgpu/gk20a driver stack, which reports `addressing_mode` as "N/A"
    // rather than "ATS" (that field is only populated on newer Grace/Thor
    // chips). nvidia-smi always suffixes these iGPUs' name with "(nvgpu)",
    // so use that as a reliable unified-memory signal independent of
    // addressing_mode.
    if lower.contains("nvgpu") {
        return true;
    }
    false
}

/// Fallback VRAM estimation from GPU model name.
/// Used when nvidia-smi or other tools report 0 VRAM.
fn estimate_vram_from_name(name: &str) -> f64 {
    let lower = name.to_lowercase();
    // NVIDIA RTX 50 series
    if lower.contains("5090") {
        return 32.0;
    }
    if lower.contains("5080") {
        return 16.0;
    }
    if lower.contains("5070 ti") {
        return 16.0;
    }
    if lower.contains("5070") {
        return 12.0;
    }
    if lower.contains("5060 ti") {
        return 16.0;
    }
    if lower.contains("5060") {
        return 8.0;
    }
    // NVIDIA RTX 40 series
    if lower.contains("4090") {
        return 24.0;
    }
    if lower.contains("4080") {
        return 16.0;
    }
    if lower.contains("4070 ti") {
        return 12.0;
    }
    if lower.contains("4070") {
        return 12.0;
    }
    if lower.contains("4060 ti") {
        return 16.0;
    }
    if lower.contains("4060") {
        return 8.0;
    }
    // NVIDIA RTX 30 series
    if lower.contains("3090") {
        return 24.0;
    }
    if lower.contains("3080 ti") {
        return 12.0;
    }
    if lower.contains("3080") {
        return 10.0;
    }
    if lower.contains("3070") {
        return 8.0;
    }
    if lower.contains("3060 ti") {
        return 8.0;
    }
    if lower.contains("3060") {
        return 12.0;
    }
    // Data center / professional
    if lower.contains("h100") {
        return 80.0;
    }
    if lower.contains("a100") {
        return 80.0;
    }
    if lower.contains("l40") {
        return 48.0;
    }
    // NVIDIA RTX professional (Ampere) — must be checked before the broad "a10" match
    if lower.contains("a6000") {
        return 48.0;
    }
    if lower.contains("a5500") {
        return 24.0;
    }
    if lower.contains("a5000") {
        return 24.0;
    }
    if lower.contains("a4500") {
        return 20.0;
    }
    if lower.contains("a4000") {
        return 16.0;
    }
    if lower.contains("a2000") {
        return 12.0;
    }
    if lower.contains("a10") {
        return 24.0;
    }
    if lower.contains("t4") {
        return 16.0;
    }
    // NVIDIA Grace / DGX Spark unified memory SoCs.
    // Also match PCI device ID 2e12 (GB10) for Docker/container environments
    // where lspci shows "Device [10de:2e12]" instead of the friendly name.
    if lower.contains("gb10") || lower.contains("2e12") {
        return 128.0;
    }
    if lower.contains("gb20") {
        return 128.0;
    }
    // AMD Radeon AI PRO (RDNA 4 workstation). Checked before the RX 9000
    // series: without an entry here "R9700" misses every model check and
    // lands on the generic 8 GB Radeon fallback (issue #830).
    if lower.contains("r9700") {
        return 32.0;
    }
    // AMD RX 9000 series (RDNA 4)
    if lower.contains("9070 xt") {
        return 16.0;
    }
    if lower.contains("9070") {
        return 12.0;
    }
    if lower.contains("9060 xt") {
        return 16.0;
    }
    if lower.contains("9060") {
        return 8.0;
    }
    // AMD RX 7000 series
    if lower.contains("7900 xtx") {
        return 24.0;
    }
    if lower.contains("7900") {
        return 20.0;
    }
    if lower.contains("7800") {
        return 16.0;
    }
    if lower.contains("7700") {
        return 12.0;
    }
    if lower.contains("7600") {
        return 8.0;
    }
    // AMD RX 6000 series
    if lower.contains("6950") {
        return 16.0;
    }
    if lower.contains("6900") {
        return 16.0;
    }
    if lower.contains("6800") {
        return 16.0;
    }
    if lower.contains("6750") {
        return 12.0;
    }
    if lower.contains("6700") {
        return 12.0;
    }
    if lower.contains("6650") {
        return 8.0;
    }
    if lower.contains("6600") {
        return 8.0;
    }
    if lower.contains("6500") {
        return 4.0;
    }
    // AMD RX 5000 series
    if lower.contains("5700 xt") {
        return 8.0;
    }
    if lower.contains("5700") {
        return 8.0;
    }
    if lower.contains("5600") {
        return 6.0;
    }
    if lower.contains("5500") {
        return 4.0;
    }
    // AMD Radeon 8000 series (Ryzen AI MAX / Strix Halo integrated)
    // These are unified memory APUs; VRAM = system RAM in practice,
    // but this fallback gives a reasonable discrete estimate for name-only detection.
    if lower.contains("8060s") {
        return 32.0;
    }
    if lower.contains("8050s") {
        return 24.0;
    }
    if lower.contains("8060") && !lower.contains("8060s") {
        return 16.0;
    }
    if lower.contains("8050") && !lower.contains("8050s") {
        return 12.0;
    }
    // AMD Radeon 800M series (Ryzen AI 9 / Strix Point integrated)
    if lower.contains("890m") {
        return 16.0;
    }
    if lower.contains("880m") {
        return 12.0;
    }
    if lower.contains("870m") {
        return 8.0;
    }
    if lower.contains("860m") {
        return 8.0;
    }

    // Integrated GPUs (APU iGPUs) — must check before generic fallbacks
    // APU names like "AMD Radeon(TM) Graphics" or "Radeon Graphics" without
    // a discrete model number (RX/HD/R5/R7/R9) have very limited dedicated VRAM.
    if (lower.contains("radeon") || lower.contains("amd"))
        && !lower.contains("rx ")
        && !lower.contains("hd ")
        && !lower.contains(" r5 ")
        && !lower.contains(" r7 ")
        && !lower.contains(" r9 ")
        && !lower.contains("8060")
        && !lower.contains("8050")
        && (lower.contains("graphics") || lower.contains("igpu"))
    {
        return 0.5;
    }

    // Generic fallbacks
    if lower.contains("rtx") {
        return 8.0;
    }
    if lower.contains("gtx") {
        return 4.0;
    }
    if lower.contains("rx ") || lower.contains("radeon") {
        return 8.0;
    }
    0.0
}

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

    // Regression for #303 (wezm): Granite Ridge iGPU ("Radeon Graphics",
    // 2 GB UMA carve-out) enumerated alongside an RX 9060 XT. The iGPU must
    // be filtered out and the discrete card kept.
    #[test]
    fn test_amd_sysfs_igpu_filtered_when_discrete_present() {
        let gpus = SystemSpecs::group_and_filter_amd_sysfs_cards(vec![
            ("Radeon Graphics".to_string(), Some(2.0)),
            ("Radeon RX 9060 XT".to_string(), Some(16.0)),
        ]);
        assert_eq!(gpus.len(), 1);
        assert_eq!(gpus[0].name, "Radeon RX 9060 XT");
        assert_eq!(gpus[0].vram_gb, Some(16.0));
        assert!(!SystemSpecs::is_integrated_gpu_name("Radeon RX 9060 XT"));
    }

    // A discrete card whose mem_info_vram_total is unreadable (None) and
    // whose name isn't in the VRAM estimate table must not be silently
    // dropped when another discrete card is present.
    #[test]
    fn test_amd_sysfs_vramless_discrete_card_kept() {
        let gpus = SystemSpecs::group_and_filter_amd_sysfs_cards(vec![
            ("Radeon RX 7900 XTX".to_string(), Some(24.0)),
            ("Radeon Pro W7800X Duo".to_string(), None),
        ]);
        assert_eq!(gpus.len(), 2, "VRAM-less discrete card was dropped");
    }

    // Without any discrete card, the iGPU must survive the filter.
    #[test]
    fn test_amd_sysfs_igpu_kept_when_alone() {
        let gpus = SystemSpecs::group_and_filter_amd_sysfs_cards(vec![(
            "Radeon Graphics".to_string(),
            Some(2.0),
        )]);
        assert_eq!(gpus.len(), 1);
        assert_eq!(gpus[0].name, "Radeon Graphics");
    }

    // lspci line for the RX 9060 XT (Navi 44) as seen in #303.
    #[test]
    fn test_extract_model_navi44_lspci_line() {
        let line = "0000:03:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 44 [Radeon RX 9060 XT] [1002:7590]";
        assert_eq!(
            SystemSpecs::extract_model_from_lspci_line(line).as_deref(),
            Some("Radeon RX 9060 XT")
        );
    }

    #[test]
    fn test_measured_ram_bandwidth_plausible_and_cached() {
        // May legitimately be None on a starved CI runner; when it measures,
        // the value must sit in the sanity band and be stable across calls.
        if let Some(bw) = super::measured_ram_bandwidth_gbps() {
            assert!((2.0..=4000.0).contains(&bw), "implausible bandwidth: {bw}");
            assert_eq!(super::measured_ram_bandwidth_gbps(), Some(bw));
        }
    }

    #[test]
    fn test_parse_nvidia_smi_does_not_sum_multi_gpu_vram() {
        let text = "24564, NVIDIA GeForce RTX 4090\n24564, NVIDIA GeForce RTX 4090\n";
        let gpus = SystemSpecs::parse_nvidia_smi_list(text);

        assert_eq!(gpus.len(), 1);
        assert_eq!(gpus[0].count, 2);
        let vram = gpus[0]
            .vram_gb
            .expect("VRAM should be parsed for RTX 4090 entries");
        // 24564 MiB ~= 23.99 GiB; must stay single-card VRAM, not 2x summed.
        assert!(vram > 23.0 && vram < 25.0, "unexpected VRAM value: {vram}");
    }

    #[test]
    fn test_parse_nvidia_smi_keeps_distinct_models() {
        let text = "24564, NVIDIA GeForce RTX 4090\n16376, NVIDIA GeForce RTX 4080\n";
        let gpus = SystemSpecs::parse_nvidia_smi_list(text);

        assert_eq!(gpus.len(), 2);
        assert!(gpus.iter().any(|g| g.name.contains("4090") && g.count == 1));
        assert!(gpus.iter().any(|g| g.name.contains("4080") && g.count == 1));
    }

    #[test]
    fn test_parse_nvidia_smi_gb10_gets_vram_estimate() {
        // DGX Spark reports GB10 with 0 VRAM from nvidia-smi
        let text = "0, NVIDIA GB10\n";
        let gpus = SystemSpecs::parse_nvidia_smi_list(text);

        assert_eq!(gpus.len(), 1);
        assert!(gpus[0].name.contains("GB10"));
        // estimate_vram_from_name should kick in and return 128GB
        let vram = gpus[0].vram_gb.expect("GB10 should have estimated VRAM");
        assert!(vram > 100.0, "GB10 VRAM should be ~128GB, got {vram}");
    }

    #[test]
    fn test_estimate_vram_gb10() {
        assert_eq!(super::estimate_vram_from_name("NVIDIA GB10"), 128.0);
        assert_eq!(super::estimate_vram_from_name("NVIDIA GB20"), 128.0);
    }

    #[test]
    fn test_estimate_vram_rtx_professional() {
        assert_eq!(super::estimate_vram_from_name("NVIDIA RTX A6000"), 48.0);
        assert_eq!(super::estimate_vram_from_name("NVIDIA RTX A5500"), 24.0);
        assert_eq!(super::estimate_vram_from_name("NVIDIA RTX A5000"), 24.0);
        assert_eq!(super::estimate_vram_from_name("NVIDIA RTX A4500"), 20.0);
        assert_eq!(super::estimate_vram_from_name("NVIDIA RTX A4000"), 16.0);
        assert_eq!(super::estimate_vram_from_name("NVIDIA RTX A2000"), 12.0);
    }

    #[test]
    fn test_parse_extended_discrete_gpu_not_unified() {
        // Discrete GPU: addressing_mode is "None", VRAM is reported normally
        let text = "None, 24564, NVIDIA GeForce RTX 4090\n";
        let gpus = SystemSpecs::parse_nvidia_smi_extended(text);

        assert_eq!(gpus.len(), 1);
        assert_eq!(gpus[0].name, "NVIDIA GeForce RTX 4090");
        assert!(
            !gpus[0].unified_memory,
            "discrete GPU should not be unified"
        );
        let vram = gpus[0].vram_gb.expect("VRAM should be present");
        assert!(vram > 23.0 && vram < 25.0, "unexpected VRAM: {vram}");
    }

    #[test]
    fn test_parse_extended_tegra_unified_memory() {
        // NVIDIA Tegra / Grace Blackwell: ATS addressing, VRAM is [N/A]
        // On a real system, /proc/meminfo would provide the fallback.
        // In tests, /proc/meminfo may or may not exist.
        let text = "ATS, [N/A], NVIDIA Thor\n";
        let gpus = SystemSpecs::parse_nvidia_smi_extended(text);

        assert_eq!(gpus.len(), 1);
        assert_eq!(gpus[0].name, "NVIDIA Thor");
        assert!(gpus[0].unified_memory, "ATS should set unified_memory=true");
        // VRAM comes from /proc/meminfo; if unavailable, it's None
        // (on Linux test machines it will be Some, on macOS CI it will be None)
    }

    #[test]
    fn test_parse_extended_jetson_orin_unified_memory() {
        // Jetson Orin: addressing_mode is "N/A" (legacy nvgpu/gk20a stack, not
        // ATS), and memory.total is also "N/A" since nvidia-smi can't query
        // dedicated VRAM for the on-chip GPU.
        let text = "N/A, [N/A], Orin (nvgpu)\n";
        let gpus = SystemSpecs::parse_nvidia_smi_extended(text);

        assert_eq!(gpus.len(), 1);
        assert_eq!(gpus[0].name, "Orin (nvgpu)");
        assert!(
            super::is_nvidia_unified_memory_gpu(&gpus[0].name),
            "\"(nvgpu)\" name should be recognized as unified memory"
        );
    }

    #[test]
    fn test_is_nvidia_unified_memory_gpu_jetson_names() {
        assert!(super::is_nvidia_unified_memory_gpu("Orin (nvgpu)"));
        assert!(super::is_nvidia_unified_memory_gpu("Xavier (nvgpu)"));
        assert!(!super::is_nvidia_unified_memory_gpu(
            "NVIDIA GeForce RTX 4090"
        ));
    }

    #[test]
    fn test_parse_extended_multi_gpu_discrete() {
        // Two discrete GPUs, no unified memory
        let text = "None, 24564, NVIDIA GeForce RTX 4090\nNone, 24564, NVIDIA GeForce RTX 4090\n";
        let gpus = SystemSpecs::parse_nvidia_smi_extended(text);

        assert_eq!(gpus.len(), 1);
        assert_eq!(gpus[0].count, 2);
        assert!(!gpus[0].unified_memory);
    }

    #[test]
    fn test_gpu_bandwidth_known_gpus() {
        // Spot-check a few well-known GPUs
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 4090"),
            Some(1008.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 3060"),
            Some(360.0)
        );
        assert_eq!(super::gpu_memory_bandwidth_gbps("Tesla T4"), Some(320.0));
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA H100 SXM"),
            Some(3350.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA A100"),
            Some(1555.0)
        );
    }

    #[test]
    fn test_gpu_bandwidth_apple_silicon() {
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("Apple M1 Max"),
            Some(400.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("Apple M4 Pro"),
            Some(273.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("Apple M5 Max"),
            Some(614.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("Apple M5 Pro"),
            Some(307.0)
        );
        assert_eq!(super::gpu_memory_bandwidth_gbps("Apple M5"), Some(153.6));
    }

    #[test]
    fn test_gpu_bandwidth_unknown_returns_none() {
        assert_eq!(super::gpu_memory_bandwidth_gbps("Some Random GPU"), None);
        assert_eq!(super::gpu_memory_bandwidth_gbps(""), None);
    }

    #[test]
    fn test_gpu_bandwidth_amd() {
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("AMD Radeon RX 7900 XTX"),
            Some(960.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("AMD Instinct MI300X"),
            Some(5300.0)
        );
    }

    #[test]
    fn test_parse_cpu_name_from_cpuinfo_prefers_model_name() {
        let cpuinfo = "\
processor   : 0
model name  : Qualcomm Kryo 680
Hardware    : Qualcomm Technologies, Inc SM8350
";
        assert_eq!(
            SystemSpecs::parse_cpu_name_from_cpuinfo(cpuinfo),
            Some("Qualcomm Kryo 680".to_string())
        );
    }

    #[test]
    fn test_parse_cpu_name_from_cpuinfo_uses_hardware_fallback() {
        let cpuinfo = "\
processor   : 0
Hardware    : Qualcomm Technologies, Inc SM8650
";
        assert_eq!(
            SystemSpecs::parse_cpu_name_from_cpuinfo(cpuinfo),
            Some("Qualcomm Technologies, Inc SM8650".to_string())
        );
    }

    #[test]
    fn test_parse_vulkan_device_names_from_summary_output() {
        let text = "\
GPU0:
deviceName         = Adreno (TM) 740
GPU1:
deviceName         = llvmpipe (LLVM 17.0.0, 256 bits)
";
        let names = SystemSpecs::parse_vulkan_device_names(text);
        assert_eq!(
            names,
            vec![
                "Adreno (TM) 740".to_string(),
                "llvmpipe (LLVM 17.0.0, 256 bits)".to_string()
            ]
        );
    }

    #[test]
    fn test_parse_vulkan_device_names_from_gpu_id_lines() {
        let text = "\
GPU id = 0 (Adreno (TM) 740)
GPU id = 1 (NVIDIA GeForce RTX 4090)
";
        let names = SystemSpecs::parse_vulkan_device_names(text);
        assert_eq!(
            names,
            vec![
                "Adreno (TM) 740".to_string(),
                "NVIDIA GeForce RTX 4090".to_string()
            ]
        );
    }

    #[test]
    fn test_is_software_vulkan_device() {
        assert!(SystemSpecs::is_software_vulkan_device(
            "llvmpipe (LLVM 17.0.0, 256 bits)"
        ));
        assert!(SystemSpecs::is_software_vulkan_device("SwiftShader Device"));
        assert!(!SystemSpecs::is_software_vulkan_device("Adreno (TM) 740"));
        // CPU compute devices exposed by Mesa/RADV must be filtered out
        assert!(SystemSpecs::is_software_vulkan_device(
            "AMD Ryzen 7 9800X3D 8-Core Processor (RADV RAPHAEL_MENDOCINO)"
        ));
        assert!(SystemSpecs::is_software_vulkan_device(
            "AMD Ryzen 5 7600X 6-Core Processor (RADV RAPHAEL)"
        ));
        // Real discrete GPUs must still pass through
        assert!(!SystemSpecs::is_software_vulkan_device(
            "AMD Radeon RX 7900 XTX (RADV NAVI31)"
        ));
    }

    #[test]
    fn test_is_same_gpu_name_uses_normalized_exact_match() {
        assert!(SystemSpecs::is_same_gpu_name(
            "NVIDIA-GeForce RTX 4090",
            "nvidia geforce rtx 4090"
        ));
        assert!(!SystemSpecs::is_same_gpu_name("RTX", "RTX 4090"));
    }

    #[test]
    fn test_is_same_gpu_name_amd_rocm_vs_vulkan_radv() {
        // ROCm reports a family name listing multiple variants; RADV reports the
        // specific model with a driver codename.  They should be treated as the
        // same physical GPU.
        assert!(SystemSpecs::is_same_gpu_name(
            "Radeon RX 7700S/7600/7600S/7600M XT/PRO W7600",
            "AMD Radeon RX 7600 XT (RADV NAVI33)"
        ));
        // A 7700 XT via RADV should also match the same ROCm family name.
        assert!(SystemSpecs::is_same_gpu_name(
            "Radeon RX 7700S/7600/7600S/7600M XT/PRO W7600",
            "AMD Radeon RX 7700 XT (RADV NAVI33)"
        ));
        // Non-AMD GPUs must not be affected.
        assert!(!SystemSpecs::is_same_gpu_name(
            "NVIDIA GeForce RTX 3060",
            "AMD Radeon RX 6600"
        ));
        // Different AMD model numbers must not match.
        assert!(!SystemSpecs::is_same_gpu_name(
            "AMD Radeon RX 6600",
            "AMD Radeon RX 7900 XTX (RADV NAVI31)"
        ));
    }

    #[test]
    fn test_extract_gpu_model_numbers() {
        assert_eq!(
            SystemSpecs::extract_gpu_model_numbers("radeon rx 7700s 7600 7600s 7600m xt pro w7600"),
            vec!["7700", "7600", "7600", "7600", "7600"]
        );
        assert_eq!(
            SystemSpecs::extract_gpu_model_numbers("amd radeon rx 7600 xt radv navi33"),
            vec!["7600"]
        );
        // Numbers shorter than 3 or longer than 5 digits are ignored.
        assert!(SystemSpecs::extract_gpu_model_numbers("rx 42 xt").is_empty());
    }

    #[test]
    fn test_normalize_gpu_name_for_dedupe() {
        assert_eq!(
            SystemSpecs::normalize_gpu_name_for_dedupe(" Adreno (TM) 740 "),
            "adreno tm 740"
        );
    }

    // ── GpuBackend::label ────────────────────────────────────────────

    #[test]
    fn test_gpu_backend_labels() {
        assert_eq!(super::GpuBackend::Cuda.label(), "CUDA");
        assert_eq!(super::GpuBackend::Metal.label(), "Metal");
        assert_eq!(super::GpuBackend::Rocm.label(), "ROCm");
        assert_eq!(super::GpuBackend::Vulkan.label(), "Vulkan");
        assert_eq!(super::GpuBackend::Sycl.label(), "SYCL");
        assert_eq!(super::GpuBackend::CpuArm.label(), "CPU (ARM)");
        assert_eq!(super::GpuBackend::CpuX86.label(), "CPU (x86)");
        assert_eq!(super::GpuBackend::Ascend.label(), "NPU (Ascend)");
    }

    // ── parse_memory_size ────────────────────────────────────────────

    #[test]
    fn test_parse_memory_size_gb() {
        assert_eq!(super::parse_memory_size("32G"), Some(32.0));
        assert_eq!(super::parse_memory_size("32GB"), Some(32.0));
        assert_eq!(super::parse_memory_size("32GiB"), Some(32.0));
        assert_eq!(super::parse_memory_size("24g"), Some(24.0));
        assert_eq!(super::parse_memory_size("24gb"), Some(24.0));
    }

    #[test]
    fn test_parse_memory_size_mb() {
        let result = super::parse_memory_size("16384M").unwrap();
        assert!((result - 16.0).abs() < 0.01);
        let result = super::parse_memory_size("8192MB").unwrap();
        assert!((result - 8.0).abs() < 0.01);
    }

    #[test]
    fn test_parse_memory_size_tb() {
        let result = super::parse_memory_size("1T").unwrap();
        assert!((result - 1024.0).abs() < 0.01);
        let result = super::parse_memory_size("2TB").unwrap();
        assert!((result - 2048.0).abs() < 0.01);
    }

    #[test]
    fn test_parse_memory_size_bare_number() {
        assert_eq!(super::parse_memory_size("16"), Some(16.0));
    }

    #[test]
    fn test_parse_memory_size_whitespace() {
        assert_eq!(super::parse_memory_size("  32G  "), Some(32.0));
    }

    #[test]
    fn test_parse_memory_size_empty() {
        assert_eq!(super::parse_memory_size(""), None);
        assert_eq!(super::parse_memory_size("  "), None);
    }

    #[test]
    fn test_parse_memory_size_invalid_suffix() {
        assert_eq!(super::parse_memory_size("32X"), None);
        assert_eq!(super::parse_memory_size("32KB"), None);
    }

    #[test]
    fn test_parse_memory_size_fractional() {
        assert_eq!(super::parse_memory_size("16.5G"), Some(16.5));
    }

    // ── with_gpu_memory_override ─────────────────────────────────────

    fn make_specs_no_gpu() -> SystemSpecs {
        SystemSpecs {
            total_ram_gb: 32.0,
            available_ram_gb: 24.0,
            total_cpu_cores: 8,
            cpu_name: "Test CPU".to_string(),
            has_gpu: false,
            gpu_vram_gb: None,
            total_gpu_vram_gb: None,
            gpu_available_gb: None,
            gpu_name: None,
            gpu_count: 0,
            unified_memory: false,
            backend: super::GpuBackend::CpuX86,
            gpus: vec![],
            cluster_mode: false,
            cluster_node_count: 0,
        }
    }

    fn make_specs_with_gpu() -> SystemSpecs {
        SystemSpecs {
            total_ram_gb: 32.0,
            available_ram_gb: 24.0,
            total_cpu_cores: 8,
            cpu_name: "Test CPU".to_string(),
            has_gpu: true,
            gpu_vram_gb: Some(8.0),
            total_gpu_vram_gb: Some(8.0),
            gpu_available_gb: None,
            gpu_name: Some("NVIDIA RTX 3070".to_string()),
            gpu_count: 1,
            unified_memory: false,
            backend: super::GpuBackend::Cuda,
            gpus: vec![super::GpuInfo {
                name: "NVIDIA RTX 3070".to_string(),
                vram_gb: Some(8.0),
                backend: super::GpuBackend::Cuda,
                count: 1,
                unified_memory: false,
            }],
            cluster_mode: false,
            cluster_node_count: 0,
        }
    }

    #[test]
    fn test_gpu_override_creates_synthetic_gpu_when_none() {
        let specs = make_specs_no_gpu().with_gpu_memory_override(24.0);
        assert!(specs.has_gpu);
        assert_eq!(specs.gpu_vram_gb, Some(24.0));
        assert_eq!(specs.total_gpu_vram_gb, Some(24.0));
        assert_eq!(specs.gpu_count, 1);
        assert_eq!(specs.gpus.len(), 1);
        assert_eq!(specs.gpus[0].name, "User-specified GPU");
    }

    #[test]
    fn test_gpu_override_updates_existing_gpu() {
        let specs = make_specs_with_gpu().with_gpu_memory_override(24.0);
        assert_eq!(specs.gpu_vram_gb, Some(24.0));
        assert_eq!(specs.total_gpu_vram_gb, Some(24.0));
        assert_eq!(specs.gpus[0].vram_gb, Some(24.0));
        assert_eq!(specs.gpus[0].name, "NVIDIA RTX 3070");
    }

    #[test]
    fn test_gpu_override_multi_gpu_scales_total() {
        let mut specs = make_specs_with_gpu();
        specs.gpus[0].count = 2;
        let specs = specs.with_gpu_memory_override(24.0);
        assert_eq!(specs.gpu_vram_gb, Some(24.0));
        assert_eq!(specs.total_gpu_vram_gb, Some(48.0));
    }

    #[test]
    fn test_gpu_override_clears_gpu_available() {
        let mut specs = make_specs_with_gpu();
        specs.gpu_available_gb = Some(11.84);
        let specs = specs.with_gpu_memory_override(24.0);
        assert_eq!(specs.gpu_available_gb, None);
    }

    #[test]
    fn test_gpu_override_synthetic_gpu_clears_gpu_available() {
        let mut specs = make_specs_no_gpu();
        specs.gpu_available_gb = Some(11.84);
        let specs = specs.with_gpu_memory_override(24.0);
        assert_eq!(specs.gpu_available_gb, None);
    }

    // ── format_unified_memory_line ───────────────────────────────────

    #[test]
    fn test_unified_line_shows_gpu_available_when_known() {
        let line = super::format_unified_memory_line("Apple M3", 16.0, Some(11.84), "Metal");
        assert_eq!(
            line,
            "Apple M3 (unified memory, 11.84 GB GPU-available of 16.00 GB shared, Metal)"
        );
    }

    #[test]
    fn test_unified_line_clamps_gpu_available_to_shared_pool() {
        // An iogpu.wired_limit_mb set above physical RAM passes through Metal
        // verbatim; the display must bound it by the shared pool.
        let line = super::format_unified_memory_line("Apple M3", 16.0, Some(976.56), "Metal");
        assert_eq!(
            line,
            "Apple M3 (unified memory, 16.00 GB GPU-available of 16.00 GB shared, Metal)"
        );
    }

    #[test]
    fn test_unified_line_falls_back_when_unknown() {
        let line = super::format_unified_memory_line("Apple M3", 16.0, None, "Metal");
        assert_eq!(line, "Apple M3 (unified memory, 16.00 GB shared, Metal)");
    }

    // ── is_amd_unified_memory_apu ────────────────────────────────────

    #[test]
    fn test_amd_unified_memory_apu_detection() {
        // Only Ryzen AI MAX / MAX+ have true unified memory
        assert!(super::is_amd_unified_memory_apu(
            "AMD Ryzen AI MAX+ 395 w/ Radeon 8060S"
        ));
        assert!(super::is_amd_unified_memory_apu("AMD Ryzen AI MAX 390"));
        // Regular Ryzen AI chips are NOT unified memory APUs
        assert!(!super::is_amd_unified_memory_apu(
            "AMD Ryzen AI 9 HX 370 w/ Radeon 890M"
        ));
        assert!(!super::is_amd_unified_memory_apu("AMD Ryzen AI 7 350"));
        assert!(!super::is_amd_unified_memory_apu("AMD Ryzen 9 7950X"));
        assert!(!super::is_amd_unified_memory_apu("Intel Core i9-14900K"));
    }

    // ── detect_windows_physical_total_ram_gb ─────────────────────────

    #[test]
    #[cfg(not(target_os = "windows"))]
    fn test_windows_physical_total_ram_returns_none_on_non_windows() {
        // On Linux/macOS the function must return None (it is Windows-only).
        assert!(super::detect_windows_physical_total_ram_gb().is_none());
    }

    // ── issue #810: BIOS UMA carveouts on non-MAX AMD APUs ───────────

    #[test]
    fn test_is_amd_apu_matches_igpu_brand_strings() {
        // Any Ryzen with an iGPU baked into the brand string qualifies.
        assert!(super::is_amd_apu(
            "AMD Ryzen 7 8845HS w/ Radeon 780M Graphics"
        ));
        assert!(super::is_amd_apu("AMD Ryzen AI 9 HX 370 w/ Radeon 890M"));
        assert!(super::is_amd_apu("AMD Ryzen AI MAX+ 395 w/ Radeon 8060S"));
        // Desktop parts without an iGPU carveout, and non-AMD, do not.
        assert!(!super::is_amd_apu("AMD Ryzen 9 7950X"));
        assert!(!super::is_amd_apu("Intel Core i9-14900K"));
        assert!(!super::is_amd_apu("Apple M3 Pro"));
    }

    #[test]
    fn test_ram_carveout_override_prefers_physical_on_real_gap() {
        // Fixture from issue #810: 32 GB installed, 8 GB UMA carveout,
        // sysinfo sees 23.82 GB. The physical figure must win.
        let got = super::apply_ram_carveout_override(23.824_016_571_044_922, 32.0);
        assert!((got - 32.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_ram_carveout_override_keeps_sysinfo_on_normal_overhead() {
        // ~0.3 GB firmware reservation is not a carveout; keep the OS view.
        let got = super::apply_ram_carveout_override(31.7, 32.0);
        assert!((got - 31.7).abs() < f64::EPSILON);
        // A physical read *below* the OS view (bad WMI data) must never win.
        let got = super::apply_ram_carveout_override(32.0, 16.0);
        assert!((got - 32.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_amd_mobile_igpu_name() {
        assert!(super::SystemSpecs::is_amd_mobile_igpu_name(
            "AMD Radeon 780M Graphics"
        ));
        assert!(super::SystemSpecs::is_amd_mobile_igpu_name(
            "AMD Radeon(TM) 890M"
        ));
        // Discrete mobile parts carry the RX prefix.
        assert!(!super::SystemSpecs::is_amd_mobile_igpu_name(
            "AMD Radeon RX 7900M"
        ));
        // Generic and datacenter names are not the mobile-iGPU shape.
        assert!(!super::SystemSpecs::is_amd_mobile_igpu_name(
            "AMD Radeon Graphics"
        ));
        assert!(!super::SystemSpecs::is_amd_mobile_igpu_name(
            "AMD Instinct MI50"
        ));
    }

    #[test]
    fn test_mobile_igpu_with_large_carveout_stays_integrated() {
        // With the registry fix, a 780M can legitimately report 8 GB+, which
        // used to trip the issue-#638 "generic AMD name with big VRAM is an
        // Instinct" escape hatch and promote it to discrete.
        assert!(super::SystemSpecs::is_integrated_gpu(
            "AMD Radeon 780M Graphics",
            Some(8.25)
        ));
        // The #638 behaviour itself must survive: a generic-named card with
        // datacenter-sized VRAM is still treated as discrete.
        assert!(!super::SystemSpecs::is_integrated_gpu(
            "AMD Radeon Graphics",
            Some(32.0)
        ));
    }

    // ── bandwidth: RTX 20 series ─────────────────────────────────────

    #[test]
    fn test_bandwidth_rtx_20_series() {
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 2080 Ti"),
            Some(616.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 2060"),
            Some(336.0)
        );
    }

    // ── bandwidth: GTX 16 series ─────────────────────────────────────

    #[test]
    fn test_bandwidth_gtx_16_series() {
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA GeForce GTX 1660 Ti"),
            Some(288.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA GeForce GTX 1650"),
            Some(128.0)
        );
    }

    // ── bandwidth: RTX 50 series ─────────────────────────────────────

    #[test]
    fn test_bandwidth_rtx_50_series() {
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 5090"),
            Some(1792.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 5080"),
            Some(960.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 5070 Ti"),
            Some(896.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 5070"),
            Some(672.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 5060 Ti"),
            Some(448.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA GeForce RTX 5060"),
            Some(256.0)
        );
    }

    // ── bandwidth: AMD RX 6000 series ────────────────────────────────

    #[test]
    fn test_bandwidth_amd_rx_6000() {
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("AMD Radeon RX 6950 XT"),
            Some(576.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("AMD Radeon RX 6700 XT"),
            Some(384.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("AMD Radeon RX 6600"),
            Some(224.0)
        );
    }

    // ── bandwidth: NVIDIA professional ───────────────────────────────

    #[test]
    fn test_bandwidth_nvidia_professional() {
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA RTX A6000"),
            Some(768.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("NVIDIA RTX A4000"),
            Some(448.0)
        );
        assert_eq!(super::gpu_memory_bandwidth_gbps("NVIDIA L40S"), Some(864.0));
        assert_eq!(super::gpu_memory_bandwidth_gbps("NVIDIA L4"), Some(300.0));
    }

    // ── bandwidth: Apple Silicon all variants ────────────────────────

    #[test]
    fn test_bandwidth_apple_silicon_all() {
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("Apple M4 Ultra"),
            Some(819.0)
        );
        assert_eq!(super::gpu_memory_bandwidth_gbps("Apple M4"), Some(120.0));
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("Apple M3 Ultra"),
            Some(800.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("Apple M3 Max"),
            Some(400.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("Apple M3 Pro"),
            Some(150.0)
        );
        assert_eq!(super::gpu_memory_bandwidth_gbps("Apple M3"), Some(100.0));
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("Apple M1 Pro"),
            Some(200.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("Apple M1 Ultra"),
            Some(800.0)
        );
    }

    // ── bandwidth: AMD CDNA ──────────────────────────────────────────

    #[test]
    fn test_bandwidth_amd_cdna() {
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("AMD Instinct MI250X"),
            Some(3277.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("AMD Instinct MI210"),
            Some(1638.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("AMD Instinct MI100"),
            Some(1229.0)
        );
    }

    // ── bandwidth: AMD RDNA 4 ────────────────────────────────────────

    #[test]
    fn test_bandwidth_amd_rdna4() {
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("AMD Radeon RX 9070 XT"),
            Some(624.0)
        );
        assert_eq!(
            super::gpu_memory_bandwidth_gbps("AMD Radeon RX 9070"),
            Some(488.0)
        );
    }

    // ── compute capability tests ──────────────────────────────────────

    #[test]
    fn test_compute_capability_nvidia_generations() {
        // Pascal
        assert_eq!(super::gpu_compute_capability("Tesla P100"), Some((6, 1)));
        // Volta
        assert_eq!(
            super::gpu_compute_capability("Tesla V100-PCIE-16GB"),
            Some((7, 0))
        );
        // Turing
        assert_eq!(super::gpu_compute_capability("Tesla T4"), Some((7, 5)));
        assert_eq!(
            super::gpu_compute_capability("NVIDIA GeForce RTX 2080 Ti"),
            Some((7, 5))
        );
        assert_eq!(
            super::gpu_compute_capability("NVIDIA GeForce GTX 1660 Ti"),
            Some((7, 5))
        );
        // Ampere
        assert_eq!(super::gpu_compute_capability("NVIDIA A100"), Some((8, 0)));
        assert_eq!(
            super::gpu_compute_capability("NVIDIA GeForce RTX 3090"),
            Some((8, 6))
        );
        // Ada Lovelace
        assert_eq!(
            super::gpu_compute_capability("NVIDIA GeForce RTX 4090"),
            Some((8, 9))
        );
        assert_eq!(super::gpu_compute_capability("NVIDIA L40S"), Some((8, 9)));
        // Hopper
        assert_eq!(
            super::gpu_compute_capability("NVIDIA H100 SXM"),
            Some((9, 0))
        );
        // Blackwell
        assert_eq!(
            super::gpu_compute_capability("NVIDIA GeForce RTX 5090"),
            Some((10, 0))
        );
    }

    #[test]
    fn test_compute_capability_unknown_returns_none() {
        assert_eq!(super::gpu_compute_capability("Some Random GPU"), None);
        assert_eq!(super::gpu_compute_capability("Apple M4 Max"), None);
        assert_eq!(
            super::gpu_compute_capability("AMD Radeon RX 7900 XTX"),
            None
        );
    }

    #[test]
    fn test_is_integrated_gpu_name() {
        // Intel integrated
        assert!(SystemSpecs::is_integrated_gpu_name(
            "Intel(R) UHD Graphics 770"
        ));
        assert!(SystemSpecs::is_integrated_gpu_name(
            "Intel(R) HD Graphics 630"
        ));
        assert!(SystemSpecs::is_integrated_gpu_name(
            "Intel(R) Iris(R) Xe Graphics"
        ));
        assert!(SystemSpecs::is_integrated_gpu_name(
            "Intel(R) Iris(R) Plus Graphics"
        ));
        // Intel discrete should NOT match
        assert!(!SystemSpecs::is_integrated_gpu_name(
            "Intel(R) Arc(TM) A770"
        ));
        assert!(!SystemSpecs::is_integrated_gpu_name(
            "Intel(R) Arc(TM) B580"
        ));
        // Explicit "(integrated)" tag from APU detection
        assert!(SystemSpecs::is_integrated_gpu_name(
            "AMD Ryzen AI 9 HX 370 w/ Radeon 890M (integrated)"
        ));
    }

    #[test]
    fn test_is_integrated_gpu_name_amd() {
        // AMD integrated (generic "Radeon Graphics" with no RX/PRO)
        assert!(SystemSpecs::is_integrated_gpu_name(
            "AMD Radeon(TM) Graphics"
        ));
        assert!(SystemSpecs::is_integrated_gpu_name("AMD Radeon Graphics"));
        // AMD discrete should NOT match
        assert!(!SystemSpecs::is_integrated_gpu_name(
            "AMD Radeon RX 7900 XTX"
        ));
        assert!(!SystemSpecs::is_integrated_gpu_name("AMD Radeon Pro W7900"));
    }

    #[test]
    fn test_is_integrated_gpu_name_nvidia() {
        // NVIDIA GPUs are never integrated in the traditional sense
        assert!(!SystemSpecs::is_integrated_gpu_name(
            "NVIDIA GeForce RTX 4090"
        ));
        assert!(!SystemSpecs::is_integrated_gpu_name(
            "NVIDIA GeForce GTX 1650"
        ));
    }

    #[test]
    fn test_prefer_discrete_gpus_filters_integrated() {
        use super::GpuBackend;
        let gpus = vec![
            super::GpuInfo {
                name: "Intel(R) UHD Graphics 770".to_string(),
                vram_gb: Some(8.0),
                backend: GpuBackend::Vulkan,
                count: 1,
                unified_memory: false,
            },
            super::GpuInfo {
                name: "NVIDIA GeForce RTX 4090".to_string(),
                vram_gb: Some(4.0), // WMI 32-bit cap may report low value
                backend: GpuBackend::Cuda,
                count: 1,
                unified_memory: false,
            },
        ];
        let result = SystemSpecs::prefer_discrete_gpus(gpus);
        assert_eq!(result.len(), 1);
        assert!(result[0].name.contains("RTX 4090"));
    }

    #[test]
    fn test_prefer_discrete_gpus_keeps_igpu_only() {
        use super::GpuBackend;
        let gpus = vec![super::GpuInfo {
            name: "Intel(R) UHD Graphics 770".to_string(),
            vram_gb: Some(2.0),
            backend: GpuBackend::Vulkan,
            count: 1,
            unified_memory: false,
        }];
        let result = SystemSpecs::prefer_discrete_gpus(gpus);
        assert_eq!(result.len(), 1);
        assert!(result[0].name.contains("UHD"));
    }

    #[test]
    fn test_parse_macos_metal_gpus_from_system_profiler_json() {
        let json = br#"
        {
          "SPDisplaysDataType": [
            {
              "_name": "Intel HD Graphics 630",
              "sppci_model": "Intel HD Graphics 630",
              "spdisplays_mtlgpufamilysupport": "Metal 3",
              "spdisplays_vram_shared": "1536 MB"
            },
            {
              "_name": "AMD Radeon RX Baffin Prototype",
              "sppci_model": "Radeon Pro 560",
              "spdisplays_mtlgpufamilysupport": "Metal 3",
              "spdisplays_vram": "4 GB"
            },
            {
              "_name": "Display",
              "sppci_model": "Display",
              "spdisplays_vram": "0 MB"
            },
            {
              "_name": "Apple M2",
              "sppci_model": "Apple M2",
              "spdisplays_mtlgpufamilysupport": "Metal 3",
              "spdisplays_vram_shared": "16 GB"
            }
          ]
        }
        "#;

        let gpus = SystemSpecs::parse_macos_metal_gpus_from_system_profiler_json(json);

        assert_eq!(gpus.len(), 2);
        assert_eq!(gpus[0].name, "Intel HD Graphics 630");
        assert_eq!(gpus[0].backend, super::GpuBackend::Metal);
        assert_eq!(gpus[0].vram_gb, Some(1.5));
        assert!(!gpus[0].unified_memory);
        assert_eq!(gpus[1].name, "Radeon Pro 560");
        assert_eq!(gpus[1].backend, super::GpuBackend::Metal);
        assert_eq!(gpus[1].vram_gb, Some(4.0));
        assert!(!gpus[1].unified_memory);
    }

    #[test]
    fn test_quant_min_compute_capability() {
        assert_eq!(
            super::quant_min_compute_capability("AWQ-4bit"),
            Some((7, 5))
        );
        assert_eq!(
            super::quant_min_compute_capability("AWQ-8bit"),
            Some((7, 5))
        );
        assert_eq!(
            super::quant_min_compute_capability("GPTQ-Int4"),
            Some((7, 5))
        );
        assert_eq!(
            super::quant_min_compute_capability("GPTQ-Int8"),
            Some((7, 5))
        );
        // GGUF quants have no CC restriction
        assert_eq!(super::quant_min_compute_capability("Q4_K_M"), None);
        assert_eq!(super::quant_min_compute_capability("Q8_0"), None);
    }

    #[test]
    fn test_ram_override_updates_ram_values() {
        let specs = SystemSpecs {
            total_ram_gb: 32.0,
            available_ram_gb: 24.0,
            total_cpu_cores: 8,
            cpu_name: "Test CPU".to_string(),
            has_gpu: true,
            gpu_vram_gb: Some(16.0),
            total_gpu_vram_gb: Some(16.0),
            gpu_available_gb: None,
            gpu_name: Some("Test GPU".to_string()),
            gpu_count: 1,
            unified_memory: false,
            backend: super::GpuBackend::Cuda,
            gpus: vec![super::GpuInfo {
                name: "Test GPU".to_string(),
                vram_gb: Some(16.0),
                backend: super::GpuBackend::Cuda,
                count: 1,
                unified_memory: false,
            }],
            cluster_mode: false,
            cluster_node_count: 0,
        };

        let overridden = specs.with_ram_override(128.0);
        assert_eq!(overridden.total_ram_gb, 128.0);
        assert!((overridden.available_ram_gb - 115.2).abs() < 0.01);
        // Discrete GPU VRAM unchanged
        assert_eq!(overridden.gpu_vram_gb, Some(16.0));
        assert_eq!(overridden.total_gpu_vram_gb, Some(16.0));
    }

    #[test]
    fn test_ram_override_unified_memory_updates_gpu() {
        let specs = SystemSpecs {
            total_ram_gb: 36.0,
            available_ram_gb: 30.0,
            total_cpu_cores: 10,
            cpu_name: "Apple M2 Max".to_string(),
            has_gpu: true,
            gpu_vram_gb: Some(36.0),
            total_gpu_vram_gb: Some(36.0),
            gpu_available_gb: Some(27.0),
            gpu_name: Some("Apple M2 Max".to_string()),
            gpu_count: 1,
            unified_memory: true,
            backend: super::GpuBackend::Metal,
            gpus: vec![super::GpuInfo {
                name: "Apple M2 Max".to_string(),
                vram_gb: Some(36.0),
                backend: super::GpuBackend::Metal,
                count: 1,
                unified_memory: true,
            }],
            cluster_mode: false,
            cluster_node_count: 0,
        };

        let overridden = specs.with_ram_override(96.0);
        assert_eq!(overridden.total_ram_gb, 96.0);
        assert_eq!(overridden.gpu_vram_gb, Some(96.0));
        assert_eq!(overridden.total_gpu_vram_gb, Some(96.0));
        assert_eq!(overridden.gpus[0].vram_gb, Some(96.0));
        assert_eq!(overridden.gpu_available_gb, None);
    }

    #[test]
    fn test_cpu_core_override() {
        let specs = SystemSpecs {
            total_ram_gb: 32.0,
            available_ram_gb: 24.0,
            total_cpu_cores: 8,
            cpu_name: "Test CPU".to_string(),
            has_gpu: false,
            gpu_vram_gb: None,
            total_gpu_vram_gb: None,
            gpu_available_gb: None,
            gpu_name: None,
            gpu_count: 0,
            unified_memory: false,
            backend: super::GpuBackend::CpuX86,
            gpus: vec![],
            cluster_mode: false,
            cluster_node_count: 0,
        };

        let overridden = specs.with_cpu_core_override(64);
        assert_eq!(overridden.total_cpu_cores, 64);
        // Other fields unchanged
        assert_eq!(overridden.total_ram_gb, 32.0);
        assert_eq!(overridden.available_ram_gb, 24.0);
        assert!(!overridden.has_gpu);
    }

    #[test]
    fn test_parse_rocm_smi_two_different_gpus() {
        // Exact output from the issue reporter's system
        let vram_text = "\
GPU[0]          : VRAM Total Memory (B): 8573157376
GPU[0]          : VRAM Total Used Memory (B): 60448768
GPU[1]          : VRAM Total Memory (B): 34208743424
GPU[1]          : VRAM Total Used Memory (B): 33732509696";

        let product_text = "\
GPU[0]          : Card Series:          AMD Radeon RX 7600
GPU[0]          : Card Model:           0x7480
GPU[0]          : Card Vendor:          Advanced Micro Devices, Inc. [AMD/ATI]
GPU[0]          : Card SKU:             D7451000
GPU[1]          : Card Series:          AMD Radeon AI PRO R9700
GPU[1]          : Card Model:           0x7551
GPU[1]          : Card Vendor:          Advanced Micro Devices, Inc. [AMD/ATI]
GPU[1]          : Card SKU:             1E4990U";

        let gpus = SystemSpecs::parse_rocm_smi_output(vram_text, Some(product_text));

        assert_eq!(gpus.len(), 2, "should detect two distinct GPUs");
        assert!(
            gpus.iter()
                .any(|g| g.name.contains("RX 7600") && g.count == 1),
            "should find RX 7600"
        );
        assert!(
            gpus.iter()
                .any(|g| g.name.contains("R9700") && g.count == 1),
            "should find R9700"
        );

        let rx7600 = gpus.iter().find(|g| g.name.contains("RX 7600")).unwrap();
        let r9700 = gpus.iter().find(|g| g.name.contains("R9700")).unwrap();
        // RX 7600 ~8 GB, R9700 ~32 GB
        assert!(rx7600.vram_gb.unwrap() > 7.0 && rx7600.vram_gb.unwrap() < 9.0);
        assert!(r9700.vram_gb.unwrap() > 31.0 && r9700.vram_gb.unwrap() < 33.0);
    }

    #[test]
    fn test_parse_rocm_smi_identical_gpus_grouped() {
        let vram_text = "\
GPU[0]          : VRAM Total Memory (B): 34208743424
GPU[0]          : VRAM Total Used Memory (B): 100000
GPU[1]          : VRAM Total Memory (B): 34208743424
GPU[1]          : VRAM Total Used Memory (B): 200000";

        let product_text = "\
GPU[0]          : Card Series:          AMD Radeon AI PRO R9700
GPU[1]          : Card Series:          AMD Radeon AI PRO R9700";

        let gpus = SystemSpecs::parse_rocm_smi_output(vram_text, Some(product_text));

        assert_eq!(gpus.len(), 1, "identical GPUs should be grouped");
        assert_eq!(gpus[0].count, 2);
        assert!(gpus[0].name.contains("R9700"));
    }

    #[test]
    fn test_parse_rocm_smi_igpu_filtered() {
        // Simulate an APU iGPU (512 MB) alongside a discrete GPU
        let vram_text = "\
GPU[0]          : VRAM Total Memory (B): 536870912
GPU[0]          : VRAM Total Used Memory (B): 100000
GPU[1]          : VRAM Total Memory (B): 34208743424
GPU[1]          : VRAM Total Used Memory (B): 200000";

        let product_text = "\
GPU[0]          : Card Series:          AMD Radeon Graphics
GPU[1]          : Card Series:          AMD Radeon AI PRO R9700";

        let gpus = SystemSpecs::parse_rocm_smi_output(vram_text, Some(product_text));

        assert_eq!(gpus.len(), 1, "iGPU should be filtered out");
        assert!(gpus[0].name.contains("R9700"));
    }

    #[test]
    fn test_parse_rocm_smi_no_product_text() {
        let vram_text = "\
GPU[0]          : VRAM Total Memory (B): 34208743424
GPU[0]          : VRAM Total Used Memory (B): 200000";

        let gpus = SystemSpecs::parse_rocm_smi_output(vram_text, None);

        assert_eq!(gpus.len(), 1);
        assert_eq!(gpus[0].name, "AMD GPU");
        assert!(gpus[0].vram_gb.unwrap() > 31.0);
    }

    // Strix Halo (AMD Ryzen AI MAX+) with libdrm_amdgpu.so missing: rocm-smi
    // reports `Card Series: N/A` for the marketing name. That "N/A" must not
    // become the GPU identity — it falls back to a generic "AMD GPU" so the
    // APU-unify step (and, ultimately, the leaderboard) can name it properly.
    #[test]
    fn test_parse_rocm_smi_na_product_name_falls_back() {
        let vram_text = "\
GPU[0]          : VRAM Total Memory (B): 68719476736
GPU[0]          : VRAM Total Used Memory (B): 53637746688";
        let product_text = "\
GPU[0]          : Card Series:            N/A
GPU[0]          : Card Model:             0x1586
GPU[0]          : Card SKU:               STRXLGEN
GPU[0]          : GFX Version:            gfx1151";

        let gpus = SystemSpecs::parse_rocm_smi_output(vram_text, Some(product_text));

        assert_eq!(gpus.len(), 1);
        assert_eq!(gpus[0].name, "AMD GPU", "N/A must not be used as a name");
        assert!(gpus[0].vram_gb.unwrap() > 63.0);
    }

    #[test]
    fn test_is_placeholder_and_generic_amd_gpu_name() {
        assert!(super::is_placeholder_gpu_name("N/A"));
        assert!(super::is_placeholder_gpu_name(" n/a "));
        assert!(super::is_placeholder_gpu_name("unknown"));
        assert!(!super::is_placeholder_gpu_name("Radeon 8060S"));
        assert!(super::is_generic_amd_gpu_name("AMD GPU"));
        assert!(super::is_generic_amd_gpu_name("AMD/ATI"));
        assert!(super::is_generic_amd_gpu_name("Radeon Graphics"));
        assert!(!super::is_generic_amd_gpu_name("AMD Radeon RX 7900 XTX"));
    }

    // Newer rocm-smi emits a tabular layout instead of one line per field.
    // Models the dual Instinct MI50 setup from issue #638 (both cards share
    // the same product name and 32 GB VRAM), which the block-only parser
    // collapsed to a single card.
    #[test]
    fn test_parse_rocm_smi_tabular_identical_gpus() {
        let vram_text = "\
====================== ROCm System Management Interface ======================
================================ Memory Usage ================================
Device  Node  VRAM Total Memory (B)   VRAM Total Used Memory (B)
0       2     34342961152             16893952
1       1     34342961152             33678336
=============================================================================";

        let product_text = "\
====================== ROCm System Management Interface ======================
================================ Product Info ================================
Device  Card Series            Card Model  Card Vendor
0       Instinct MI60 / MI50   0x66a1      Advanced Micro Devices, Inc. [AMD/ATI]
1       Instinct MI60 / MI50   0x66a1      Advanced Micro Devices, Inc. [AMD/ATI]
=============================================================================";

        let gpus = SystemSpecs::parse_rocm_smi_output(vram_text, Some(product_text));

        assert_eq!(gpus.len(), 1, "identical tabular GPUs should be grouped");
        assert_eq!(gpus[0].count, 2, "both MI50s should be detected");
        assert!(gpus[0].name.contains("MI50"), "name: {}", gpus[0].name);
        assert!(gpus[0].vram_gb.unwrap() > 31.0 && gpus[0].vram_gb.unwrap() < 33.0);
    }

    #[test]
    fn test_parse_rocm_smi_tabular_two_different_gpus() {
        let vram_text = "\
Device  Node  VRAM Total Memory (B)   VRAM Total Used Memory (B)
0       2     8573157376              60448768
1       1     34208743424             33732509696";

        let product_text = "\
Device  Card Series              Card Model
0       AMD Radeon RX 7600       0x7480
1       AMD Radeon AI PRO R9700  0x7551";

        let gpus = SystemSpecs::parse_rocm_smi_output(vram_text, Some(product_text));

        assert_eq!(gpus.len(), 2, "should detect two distinct tabular GPUs");
        let rx7600 = gpus.iter().find(|g| g.name.contains("RX 7600")).unwrap();
        let r9700 = gpus.iter().find(|g| g.name.contains("R9700")).unwrap();
        assert_eq!(rx7600.count, 1);
        assert_eq!(r9700.count, 1);
        assert!(rx7600.vram_gb.unwrap() > 7.0 && rx7600.vram_gb.unwrap() < 9.0);
        assert!(r9700.vram_gb.unwrap() > 31.0 && r9700.vram_gb.unwrap() < 33.0);
    }

    // Tabular VRAM without parseable product names still yields the right
    // count (names fall back to "AMD GPU" but cards are not lost).
    #[test]
    fn test_parse_rocm_smi_tabular_vram_no_names() {
        let vram_text = "\
Device  Node  VRAM Total Memory (B)   VRAM Total Used Memory (B)
0       2     34342961152             16893952
1       1     34342961152             33678336";

        let gpus = SystemSpecs::parse_rocm_smi_output(vram_text, None);

        assert_eq!(gpus.len(), 1);
        assert_eq!(gpus[0].count, 2);
        assert_eq!(gpus[0].name, "AMD GPU");
    }

    // Regression for issue #638 (keyz182): verbatim rocm-smi block output
    // from a mixed system — a 32 GB MI50 that reports the generic
    // `Card Series: AMD Radeon Graphics`, a 16 GB MI50 with the proper
    // Instinct name, and a 512 MB Cezanne iGPU. The generic-named 32 GB
    // card must survive both the iGPU VRAM filter and prefer_discrete_gpus,
    // and must not be grouped with the iGPU that shares its generic name.
    #[test]
    fn test_parse_rocm_smi_mixed_mi50s_generic_name_and_igpu() {
        let vram_text = "\
============================ ROCm System Management Interface ============================
================================== Memory Usage (Bytes) ==================================
GPU[0]\t\t: VRAM Total Memory (B): 34342961152
GPU[0]\t\t: VRAM Total Used Memory (B): 25227759616
GPU[1]\t\t: VRAM Total Memory (B): 17163091968
GPU[1]\t\t: VRAM Total Used Memory (B): 7695077376
GPU[2]\t\t: VRAM Total Memory (B): 536870912
GPU[2]\t\t: VRAM Total Used Memory (B): 18165760
==========================================================================================
================================== End of ROCm SMI Log ===================================";

        let product_text = "\
============================ ROCm System Management Interface ============================
====================================== Product Info ======================================
GPU[0]\t\t: Card Series: \t\tAMD Radeon Graphics
GPU[0]\t\t: Card Model: \t\t0x66a0
GPU[0]\t\t: Card Vendor: \t\tAdvanced Micro Devices, Inc. [AMD/ATI]
GPU[0]\t\t: Card SKU: \t\tD1640200
GPU[0]\t\t: Subsystem ID: \t0x081e
GPU[0]\t\t: Device Rev: \t\t0x00
GPU[0]\t\t: Node ID: \t\t1
GPU[0]\t\t: GUID: \t\t45854
GPU[0]\t\t: GFX Version: \t\tgfx906
GPU[1]\t\t: Card Series: \t\tAMD Instinct MI60 / MI50
GPU[1]\t\t: Card Model: \t\t0x66a1
GPU[1]\t\t: Card Vendor: \t\tAdvanced Micro Devices, Inc. [AMD/ATI]
GPU[1]\t\t: Card SKU: \t\tD1631400
GPU[1]\t\t: Subsystem ID: \t0x0834
GPU[1]\t\t: Device Rev: \t\t0x02
GPU[1]\t\t: Node ID: \t\t2
GPU[1]\t\t: GUID: \t\t28640
GPU[1]\t\t: GFX Version: \t\tgfx906
GPU[2]\t\t: Card Series: \t\tAMD Radeon Graphics
GPU[2]\t\t: Card Model: \t\t0x1638
GPU[2]\t\t: Card Vendor: \t\tAdvanced Micro Devices, Inc. [AMD/ATI]
GPU[2]\t\t: Card SKU: \t\tCEZANNE
GPU[2]\t\t: Subsystem ID: \t0x1636
GPU[2]\t\t: Device Rev: \t\t0xc8
GPU[2]\t\t: Node ID: \t\t3
GPU[2]\t\t: GUID: \t\t48746
GPU[2]\t\t: GFX Version: \t\tgfx90c
==========================================================================================
================================== End of ROCm SMI Log ===================================";

        let gpus = SystemSpecs::parse_rocm_smi_output(vram_text, Some(product_text));

        assert_eq!(
            gpus.len(),
            2,
            "both MI50s must be detected, iGPU excluded: {gpus:?}"
        );
        let big = gpus
            .iter()
            .find(|g| g.vram_gb.unwrap_or(0.0) > 30.0)
            .expect("32 GB MI50 missing");
        let small = gpus
            .iter()
            .find(|g| {
                let v = g.vram_gb.unwrap_or(0.0);
                v > 15.0 && v < 17.0
            })
            .expect("16 GB MI50 missing");
        // Generic name disambiguated with the GFX version.
        assert_eq!(big.name, "AMD Radeon Graphics (gfx906)");
        assert!(small.name.contains("MI60 / MI50"));

        // The generic-named 32 GB accelerator must survive the global
        // discrete-preference filter alongside the properly named card.
        let filtered = SystemSpecs::prefer_discrete_gpus(gpus);
        assert_eq!(
            filtered.len(),
            2,
            "prefer_discrete_gpus must not drop a 32 GB accelerator: {filtered:?}"
        );
    }

    // Real `lspci -nnD` line from a Lunar Lake laptop (Core Ultra 7 258V,
    // Arc 140V iGPU): must classify as integrated/unified with the RAM pool,
    // not a 0-VRAM discrete device (issue #609 family).
    #[test]
    fn test_parse_intel_igpu_from_lspci_lunar_lake() {
        let text = "0000:00:02.0 VGA compatible controller [0300]: Intel Corporation Core Ultra 200V Series Processors Arc Graphics 130V/140V GPU [8086:64a0] (rev 04)";
        let gpus = SystemSpecs::parse_intel_gpus_from_lspci(text, 32.0, |_| None);
        assert_eq!(gpus.len(), 1, "{gpus:?}");
        assert_eq!(gpus[0].name, "Intel Arc Graphics 130V/140V (integrated)");
        assert!(gpus[0].unified_memory);
        assert_eq!(gpus[0].vram_gb, Some(32.0));
    }

    // Discrete Arc cards enumerate behind a bridge (nonzero bus). Their
    // dedicated VRAM comes from the sysfs lookup keyed by PCI address; when
    // the lookup has nothing (e.g. driver not bound), VRAM stays None but the
    // card must still be detected and named (issue #609).
    #[test]
    fn test_parse_intel_dgpu_from_lspci() {
        let a770 = "0000:03:00.0 VGA compatible controller [0300]: Intel Corporation DG2 [Arc A770] [8086:56a0] (rev 08)";
        let gpus = SystemSpecs::parse_intel_gpus_from_lspci(a770, 32.0, |_| None);
        assert_eq!(gpus.len(), 1, "{gpus:?}");
        assert_eq!(gpus[0].name, "Intel Arc A770");
        assert!(!gpus[0].unified_memory);
        assert_eq!(gpus[0].vram_gb, None);

        let b70 = "0000:03:00.0 VGA compatible controller [0300]: Intel Corporation Battlemage G21 [Arc Pro B70] [8086:e211]";
        let gpus = SystemSpecs::parse_intel_gpus_from_lspci(b70, 32.0, |addr| {
            assert_eq!(addr, "0000:03:00.0");
            Some(24.0)
        });
        assert_eq!(gpus.len(), 1, "{gpus:?}");
        assert_eq!(gpus[0].name, "Intel Arc Pro B70");
        assert!(!gpus[0].unified_memory);
        assert_eq!(gpus[0].vram_gb, Some(24.0));
    }

    // Dual-card setup from issue #609: each card gets its own entry with
    // per-address VRAM, so total_gpu_vram_gb aggregation sees both.
    #[test]
    fn test_parse_intel_dual_dgpu_from_lspci() {
        let text = "\
0000:03:00.0 VGA compatible controller [0300]: Intel Corporation Battlemage G21 [Arc Pro B70] [8086:e211]
0000:04:00.0 VGA compatible controller [0300]: Intel Corporation Battlemage G21 [Arc Pro B70] [8086:e211]";
        let gpus = SystemSpecs::parse_intel_gpus_from_lspci(text, 32.0, |_| Some(24.0));
        assert_eq!(gpus.len(), 2, "{gpus:?}");
        for gpu in &gpus {
            assert_eq!(gpu.name, "Intel Arc Pro B70");
            assert_eq!(gpu.vram_gb, Some(24.0));
            assert!(!gpu.unified_memory);
        }
    }

    #[test]
    fn test_parse_intel_igpu_and_dgpu_together() {
        let text = "\
0000:00:02.0 VGA compatible controller [0300]: Intel Corporation Raptor Lake-S UHD Graphics [8086:a780] (rev 04)
0000:03:00.0 VGA compatible controller [0300]: Intel Corporation DG2 [Arc A770] [8086:56a0] (rev 08)";
        let gpus = SystemSpecs::parse_intel_gpus_from_lspci(text, 64.0, |_| Some(16.0));
        assert_eq!(gpus.len(), 2, "{gpus:?}");
        assert!(gpus[0].unified_memory && gpus[0].name.contains("(integrated)"));
        assert_eq!(gpus[0].vram_gb, Some(64.0), "iGPU shares the RAM pool");
        assert_eq!(gpus[1].name, "Intel Arc A770");
        assert!(!gpus[1].unified_memory);
        assert_eq!(gpus[1].vram_gb, Some(16.0));
    }

    // xe driver sysfs layout: per-tile physical_vram_size_bytes under the PCI
    // device directory. i915 layout: drm/cardN/lmem_total_bytes. Both must
    // yield the card's dedicated VRAM; an iGPU-like tree (neither file) must
    // yield None.
    #[test]
    fn test_intel_dgpu_vram_from_sysfs_layouts() {
        let root = std::env::temp_dir().join(format!(
            "llmfit-test-intel-sysfs-{}-{:?}",
            std::process::id(),
            std::thread::current().id()
        ));

        // Directory names avoid PCI-address colons — invalid in Windows
        // filenames, and the function only cares about the tree layout.
        // xe: two tiles of 12 GiB each → 24 GiB total.
        let xe_dev = root.join("xe-dev");
        std::fs::create_dir_all(xe_dev.join("tile0")).unwrap();
        std::fs::create_dir_all(xe_dev.join("tile1")).unwrap();
        let tile_bytes = (12u64 * 1024 * 1024 * 1024).to_string();
        std::fs::write(xe_dev.join("tile0/physical_vram_size_bytes"), &tile_bytes).unwrap();
        std::fs::write(xe_dev.join("tile1/physical_vram_size_bytes"), &tile_bytes).unwrap();
        assert_eq!(
            SystemSpecs::intel_dgpu_vram_gb_from_pci_dir(&xe_dev),
            Some(24.0)
        );

        // i915 discrete: lmem_total_bytes under the DRM card node.
        let i915_dev = root.join("i915-dev");
        std::fs::create_dir_all(i915_dev.join("drm/card1")).unwrap();
        std::fs::write(
            i915_dev.join("drm/card1/lmem_total_bytes"),
            (16u64 * 1024 * 1024 * 1024).to_string(),
        )
        .unwrap();
        assert_eq!(
            SystemSpecs::intel_dgpu_vram_gb_from_pci_dir(&i915_dev),
            Some(16.0)
        );

        // iGPU: DRM card node exists but no VRAM files anywhere.
        let igpu_dev = root.join("igpu-dev");
        std::fs::create_dir_all(igpu_dev.join("drm/card0")).unwrap();
        assert_eq!(
            SystemSpecs::intel_dgpu_vram_gb_from_pci_dir(&igpu_dev),
            None
        );

        std::fs::remove_dir_all(&root).ok();
    }

    // Mesa/Vulkan reports Intel devices by codename ("(LNL)") — must dedupe
    // against the lspci-derived integrated entry, but a discrete Arc with a
    // model number must NOT be swallowed by the iGPU entry.
    #[test]
    fn test_is_same_gpu_name_intel_igpu_vs_vulkan_codename() {
        assert!(SystemSpecs::is_same_gpu_name(
            "Intel Arc Graphics 130V/140V (integrated)",
            "Intel(R) Arc(tm) Graphics (LNL)"
        ));
        assert!(!SystemSpecs::is_same_gpu_name(
            "Intel Arc Graphics 130V/140V (integrated)",
            "Intel(R) Arc(tm) A770 Graphics"
        ));
        assert!(SystemSpecs::is_same_gpu_name(
            "Intel Arc A770",
            "Intel(R) Arc(tm) A770 Graphics"
        ));
    }

    // Arc Pro cards have 2-digit model numbers ("B70") that the 3-5 digit
    // extractor drops — the lspci name and the Vulkan/Level Zero name must
    // still dedupe via letter-prefixed model tokens (issue #609).
    #[test]
    fn test_is_same_gpu_name_intel_arc_pro_two_digit_model() {
        assert!(SystemSpecs::is_same_gpu_name(
            "Intel Arc Pro B70",
            "Intel(R) Arc(TM) Pro B70 Graphics"
        ));
        assert!(!SystemSpecs::is_same_gpu_name(
            "Intel Arc Pro B70",
            "Intel(R) Arc(TM) Pro B60 Graphics"
        ));
        // A dGPU with a model token must not be swallowed by an iGPU entry.
        assert!(!SystemSpecs::is_same_gpu_name(
            "Intel Arc Graphics 130V/140V (integrated)",
            "Intel(R) Arc(TM) Pro B70 Graphics"
        ));
    }

    #[test]
    fn test_prefer_discrete_gpus_drops_small_generic_radeon_keeps_large() {
        use super::GpuBackend;
        let mk = |name: &str, vram: f64| super::GpuInfo {
            name: name.to_string(),
            vram_gb: Some(vram),
            backend: GpuBackend::Rocm,
            count: 1,
            unified_memory: false,
        };
        let gpus = vec![
            mk("AMD Radeon Graphics", 32.0), // mislabeled MI50-class accelerator
            mk("AMD Radeon(TM) Graphics", 0.5), // true APU iGPU
            mk("AMD Instinct MI60 / MI50", 16.0),
        ];
        let result = SystemSpecs::prefer_discrete_gpus(gpus);
        assert_eq!(result.len(), 2, "{result:?}");
        assert!(result.iter().any(|g| g.vram_gb == Some(32.0)));
        assert!(result.iter().any(|g| g.name.contains("Instinct")));
    }

    // ── Windows VRAM: WMI 32-bit cap and registry override (#830) ─────

    // Verbatim PowerShell output from issue #830 (dro-kid): a 32 GB Radeon
    // AI PRO R9700. AdapterRAM saturates at the uint32 ceiling, so the
    // reported 4293918720 bytes (~4 GB) is meaningless and the name table
    // has to carry the card. It previously fell through to the generic
    // "radeon" fallback and reported 8 GB.
    #[test]
    fn test_parse_windows_gpu_list_r9700_ignores_32bit_cap() {
        let gpus = SystemSpecs::parse_windows_gpu_list("AMD Radeon AI PRO R9700|4293918720\n");
        assert_eq!(gpus.len(), 1, "{gpus:?}");
        assert_eq!(gpus[0].name, "AMD Radeon AI PRO R9700");
        assert_eq!(gpus[0].vram_gb, Some(32.0), "regressed to the 8 GB guess");
        assert!(!SystemSpecs::is_integrated_gpu(
            &gpus[0].name,
            gpus[0].vram_gb
        ));
    }

    // Regression for #840: Win32_VideoController reports its shared-memory
    // aperture as AdapterRAM, but Intel HD Graphics predates the supported
    // oneAPI integrated-GPU generations. It must not create a standalone SYCL
    // memory pool and route model fits through GPU mode.
    #[test]
    fn test_parse_windows_gpu_list_skips_legacy_intel_hd_graphics() {
        let legacy_only =
            SystemSpecs::parse_windows_gpu_list("Intel(R) HD Graphics 4000|2158112768\n");
        assert!(legacy_only.is_empty(), "{legacy_only:?}");

        let gpus = SystemSpecs::parse_windows_gpu_list(
            "Intel(R) HD Graphics 4000|2158112768\n\
             NVIDIA GeForce RTX 4090|4293918720\n",
        );

        assert_eq!(
            gpus.len(),
            1,
            "legacy Intel iGPU was treated as usable: {gpus:?}"
        );
        assert_eq!(gpus[0].name, "NVIDIA GeForce RTX 4090");
        assert_eq!(gpus[0].backend, super::GpuBackend::Cuda);
    }

    #[test]
    fn test_estimate_vram_radeon_ai_pro_r9700() {
        assert_eq!(
            super::estimate_vram_from_name("AMD Radeon AI PRO R9700"),
            32.0
        );
        // The RDNA 4 consumer parts must keep their own sizes.
        assert_eq!(
            super::estimate_vram_from_name("AMD Radeon RX 9070 XT"),
            16.0
        );
        assert_eq!(
            super::estimate_vram_from_name("AMD Radeon RX 9060 XT"),
            16.0
        );
    }

    #[test]
    fn test_parse_windows_registry_vram() {
        let text = "AMD Radeon AI PRO R9700|34359738368\n\
                    NVIDIA GeForce RTX 4090|25769803776\n\
                    \n\
                    Broken Adapter|not-a-number\n\
                    Zero Adapter|0\n\
                    |1073741824\n";
        let entries = SystemSpecs::parse_windows_registry_vram(text);
        assert_eq!(entries.len(), 2, "{entries:?}");
        assert_eq!(
            entries[0],
            ("AMD Radeon AI PRO R9700".to_string(), 34359738368)
        );
        assert_eq!(
            entries[1],
            ("NVIDIA GeForce RTX 4090".to_string(), 25769803776)
        );
    }

    // The WDK documents HardwareInformation.MemorySize in megabytes while WMI
    // treats the same value as bytes. A driver that writes megabytes must not
    // be allowed to overwrite a sane estimate with a few kilobytes, so values
    // below the plausibility floor are dropped and the estimate survives.
    #[test]
    fn test_parse_windows_registry_vram_rejects_megabyte_units() {
        let entries = SystemSpecs::parse_windows_registry_vram("Some Adapter|8192\n");
        assert!(entries.is_empty(), "{entries:?}");

        let gpus = SystemSpecs::parse_windows_gpu_list("AMD Radeon AI PRO R9700|4293918720\n");
        let gpus = SystemSpecs::apply_registry_vram(gpus, &entries);
        assert_eq!(
            gpus[0].vram_gb,
            Some(32.0),
            "a bogus value overwrote a good one"
        );
    }

    // The registry value is driver-published and 64-bit wide, so it wins over
    // both the capped AdapterRAM and the name-table estimate.
    #[test]
    fn test_registry_vram_overrides_wmi_estimate() {
        let gpus = SystemSpecs::parse_windows_gpu_list("AMD Radeon AI PRO R9700|4293918720\n");
        let registry = vec![("AMD Radeon AI PRO R9700".to_string(), 34359738368u64)];
        let gpus = SystemSpecs::apply_registry_vram(gpus, &registry);
        assert_eq!(gpus[0].vram_gb, Some(32.0));
    }

    // A card the name table does not know at all still gets the right size
    // once the registry is readable — this is the general fix, the R9700
    // table entry only covers hosts where the query fails.
    #[test]
    fn test_registry_vram_rescues_unknown_card() {
        let gpus = SystemSpecs::parse_windows_gpu_list("AMD Radeon PRO V710|4293918720\n");
        assert_eq!(gpus[0].vram_gb, Some(8.0), "expected the generic guess");
        let registry = vec![("AMD Radeon PRO V710".to_string(), 30064771072u64)];
        let gpus = SystemSpecs::apply_registry_vram(gpus, &registry);
        assert_eq!(gpus[0].vram_gb, Some(28.0));
    }

    // Drivers that expose the adapter under a slightly different description
    // in WMI than in the registry must still match.
    #[test]
    fn test_registry_vram_matches_on_substring() {
        let gpus = SystemSpecs::parse_windows_gpu_list("AMD Radeon AI PRO R9700|4293918720\n");
        let registry = vec![("Radeon AI PRO R9700".to_string(), 34359738368u64)];
        let gpus = SystemSpecs::apply_registry_vram(gpus, &registry);
        assert_eq!(gpus[0].vram_gb, Some(32.0));
    }

    // An unrelated registry entry must not be applied to the wrong adapter,
    // and an empty dump must leave detection untouched.
    #[test]
    fn test_registry_vram_leaves_unmatched_gpus_alone() {
        let gpus = SystemSpecs::parse_windows_gpu_list("NVIDIA GeForce RTX 4090|4293918720\n");
        assert_eq!(gpus[0].vram_gb, Some(24.0));

        let registry = vec![("AMD Radeon AI PRO R9700".to_string(), 34359738368u64)];
        let matched = SystemSpecs::apply_registry_vram(gpus.clone(), &registry);
        assert_eq!(matched[0].vram_gb, Some(24.0), "matched the wrong adapter");

        let untouched = SystemSpecs::apply_registry_vram(gpus, &[]);
        assert_eq!(untouched[0].vram_gb, Some(24.0));
    }

    // Multi-GPU hosts must pair each adapter with its own registry entry.
    #[test]
    fn test_registry_vram_multi_gpu_pairing() {
        let gpus = SystemSpecs::parse_windows_gpu_list(
            "AMD Radeon AI PRO R9700|4293918720\nNVIDIA GeForce RTX 4090|4293918720\n",
        );
        let registry = vec![
            ("NVIDIA GeForce RTX 4090".to_string(), 25769803776u64),
            ("AMD Radeon AI PRO R9700".to_string(), 34359738368u64),
        ];
        let gpus = SystemSpecs::apply_registry_vram(gpus, &registry);
        assert_eq!(gpus[0].vram_gb, Some(32.0));
        assert_eq!(gpus[1].vram_gb, Some(24.0));
    }

    // An adapter name that partially matches two registry entries is
    // ambiguous; binding it to either one would be a guess, so detection
    // keeps its own estimate instead.
    #[test]
    fn test_registry_vram_ignores_ambiguous_partial_match() {
        let registry = vec![
            ("NVIDIA GeForce RTX 4090 Ti".to_string(), 25769803776u64),
            (
                "NVIDIA GeForce RTX 4090 Laptop GPU".to_string(),
                17179869184,
            ),
        ];
        assert_eq!(
            SystemSpecs::match_registry_vram("NVIDIA GeForce RTX 4090", &registry),
            None
        );
        // With only one of them present the partial match is unambiguous.
        assert_eq!(
            SystemSpecs::match_registry_vram("NVIDIA GeForce RTX 4090", &registry[..1]),
            Some(25769803776)
        );
    }

    // Exact matching must win over a partial that would otherwise be
    // ambiguous, so a real 4090 alongside a 4090 Ti still resolves.
    #[test]
    fn test_registry_vram_exact_match_beats_partial() {
        let registry = vec![
            ("NVIDIA GeForce RTX 4090 Ti".to_string(), 34359738368u64),
            ("NVIDIA GeForce RTX 4090".to_string(), 25769803776),
        ];
        assert_eq!(
            SystemSpecs::match_registry_vram("NVIDIA GeForce RTX 4090", &registry),
            Some(25769803776)
        );
    }

    // WMI and the registry decorate the same adapter differently; neither
    // string contains the other, so normalization has to bridge them.
    #[test]
    fn test_registry_vram_matches_across_trademark_markers() {
        let registry = vec![("AMD Radeon Graphics".to_string(), 2147483648u64)];
        assert_eq!(
            SystemSpecs::match_registry_vram("AMD Radeon(TM)  Graphics", &registry),
            Some(2147483648)
        );
    }

    // The symbol forms of the same decoration. PowerShell 5.1 writes stdout in
    // the console OEM codepage, so a ™ or ® survives the lossy decode as
    // U+FFFD; matching must see through the replacement character too, or the
    // registry override is unreachable for those adapters.
    #[test]
    fn test_registry_vram_matches_across_symbol_and_mangled_markers() {
        let registry = vec![("AMD Radeon Graphics".to_string(), 2147483648u64)];
        for name in [
            "AMD Radeon\u{2122} Graphics",
            "AMD Radeon\u{00ae} Graphics",
            "AMD Radeon\u{fffd} Graphics",
            "AMD  Radeon Graphics",
        ] {
            assert_eq!(
                SystemSpecs::match_registry_vram(name, &registry),
                Some(2147483648),
                "failed to match {name:?}"
            );
        }
    }

    // Stripping punctuation must not fuse "(TM)" into the preceding word,
    // which would leave "radeontm graphics" and break the match it exists
    // to make.
    #[test]
    fn test_normalize_gpu_name_expands_markers_before_stripping() {
        assert_eq!(
            super::normalize_gpu_name_for_match("AMD Radeon(TM) Graphics"),
            "amd radeon graphics"
        );
        assert_eq!(
            super::normalize_gpu_name_for_match("Intel(R) Arc(TM) A770 Graphics"),
            "intel arc a770 graphics"
        );
        // Model identity must survive normalization.
        assert_eq!(
            super::normalize_gpu_name_for_match("AMD Radeon AI PRO R9700"),
            "amd radeon ai pro r9700"
        );
    }

    #[test]
    #[cfg(not(target_os = "windows"))]
    fn test_registry_vram_query_returns_empty_off_windows() {
        assert!(super::detect_windows_registry_vram().is_empty());
    }
}
