use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::Command;

use super::discovery::AppImageType;

#[derive(Debug)]
pub enum InspectorError {
    IoError(std::io::Error),
    NoOffset,
    UnsquashfsNotFound,
    UnsquashfsFailed(String),
    NoDesktopEntry,
}

impl std::fmt::Display for InspectorError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::IoError(e) => write!(f, "I/O error: {}", e),
            Self::NoOffset => write!(f, "Could not determine squashfs offset"),
            Self::UnsquashfsNotFound => write!(f, "A system tool needed to read app contents is missing. Install it by running: sudo apt install squashfs-tools"),
            Self::UnsquashfsFailed(msg) => write!(f, "unsquashfs failed: {}", msg),
            Self::NoDesktopEntry => write!(f, "No .desktop file found in AppImage"),
        }
    }
}

impl From<std::io::Error> for InspectorError {
    fn from(e: std::io::Error) -> Self {
        Self::IoError(e)
    }
}

#[derive(Debug, Clone, Default)]
pub struct AppImageMetadata {
    pub app_name: Option<String>,
    pub app_version: Option<String>,
    pub description: Option<String>,
    pub developer: Option<String>,
    pub icon_name: Option<String>,
    pub categories: Vec<String>,
    pub desktop_entry_content: String,
    pub architecture: Option<String>,
    pub cached_icon_path: Option<PathBuf>,
    // Extended metadata from AppStream XML and desktop entry
    pub appstream_id: Option<String>,
    pub appstream_description: Option<String>,
    pub generic_name: Option<String>,
    pub license: Option<String>,
    pub homepage_url: Option<String>,
    pub bugtracker_url: Option<String>,
    pub donation_url: Option<String>,
    pub help_url: Option<String>,
    pub vcs_url: Option<String>,
    pub keywords: Vec<String>,
    pub mime_types: Vec<String>,
    pub content_rating: Option<String>,
    pub project_group: Option<String>,
    pub releases: Vec<crate::core::appstream::ReleaseInfo>,
    pub desktop_actions: Vec<String>,
    pub has_signature: bool,
    pub screenshot_urls: Vec<String>,
    pub startup_wm_class: Option<String>,
}

#[derive(Debug, Default)]
struct DesktopEntryFields {
    name: Option<String>,
    icon: Option<String>,
    comment: Option<String>,
    categories: Vec<String>,
    exec: Option<String>,
    version: Option<String>,
    generic_name: Option<String>,
    keywords: Vec<String>,
    mime_types: Vec<String>,
    terminal: bool,
    x_appimage_name: Option<String>,
    startup_wm_class: Option<String>,
    actions: Vec<String>,
}

fn icons_cache_dir() -> PathBuf {
    let dir = crate::config::data_dir_fallback()
        .join("driftwood")
        .join("icons");
    fs::create_dir_all(&dir).ok();
    dir
}

/// Check if unsquashfs is available.
fn has_unsquashfs() -> bool {
    Command::new("unsquashfs")
        .arg("--help")
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()
        .is_ok()
}

/// Public wrapper for binary squashfs offset detection.
/// Used by other modules (e.g. wayland) to avoid executing the AppImage.
pub fn find_squashfs_offset_for(path: &Path) -> Option<u64> {
    find_squashfs_offset(path).ok()
}

/// Find the squashfs offset by scanning for a valid superblock in the binary.
/// This avoids executing the AppImage, which can hang for apps with custom AppRun scripts.
/// Uses buffered chunk-based reading to avoid loading entire files into memory
/// (critical for large AppImages like Affinity at 1.5GB+).
fn find_squashfs_offset(path: &Path) -> Result<u64, InspectorError> {
    use std::io::{BufReader, Seek, SeekFrom};

    let file = fs::File::open(path)?;
    let file_len = file.metadata()?.len();
    let mut reader = BufReader::with_capacity(256 * 1024, file);

    // Skip first 4KB to avoid false matches in ELF header
    let start: u64 = 4096.min(file_len);
    reader.seek(SeekFrom::Start(start))?;

    // Read in 256KB chunks with 96-byte overlap to catch magic spanning boundaries
    let chunk_size: usize = 256 * 1024;
    let overlap: usize = 96;
    let mut buf = vec![0u8; chunk_size];
    let mut file_pos = start;

    loop {
        if file_pos >= file_len {
            break;
        }

        let to_read = chunk_size.min((file_len - file_pos) as usize);
        let mut total_read = 0;
        while total_read < to_read {
            let n = Read::read(&mut reader, &mut buf[total_read..to_read])?;
            if n == 0 {
                break;
            }
            total_read += n;
        }
        if total_read < 32 {
            break;
        }

        // Scan this chunk for squashfs magic
        let scan_end = total_read.saturating_sub(31);
        for i in 0..scan_end {
            if buf[i..i + 4] == *b"hsqs" {
                let major = u16::from_le_bytes([buf[i + 28], buf[i + 29]]);
                let minor = u16::from_le_bytes([buf[i + 30], buf[i + 31]]);
                if major == 4 && minor == 0 {
                    let block_size = u32::from_le_bytes([
                        buf[i + 12], buf[i + 13], buf[i + 14], buf[i + 15],
                    ]);
                    if block_size.is_power_of_two()
                        && block_size >= 4096
                        && block_size <= 1_048_576
                    {
                        return Ok(file_pos + i as u64);
                    }
                }
            }
        }

        // Advance, keeping overlap to catch magic spanning chunks
        let advance = if total_read > overlap {
            total_read - overlap
        } else {
            total_read
        };
        file_pos += advance as u64;
        reader.seek(SeekFrom::Start(file_pos))?;
    }

    Err(InspectorError::NoOffset)
}

/// Get the squashfs offset from the AppImage by running it with --appimage-offset.
/// Falls back to binary scanning if execution times out or fails.
fn get_squashfs_offset(path: &Path) -> Result<u64, InspectorError> {
    // First try the fast binary scan approach (no execution needed)
    if let Ok(offset) = find_squashfs_offset(path) {
        return Ok(offset);
    }

    // Fallback: run the AppImage with a timeout
    let child = Command::new(path)
        .arg("--appimage-offset")
        .env("APPIMAGE_EXTRACT_AND_RUN", "0")
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::null())
        .spawn();

    let mut child = match child {
        Ok(c) => c,
        Err(e) => return Err(InspectorError::IoError(e)),
    };

    // Wait up to 5 seconds
    let start = std::time::Instant::now();
    loop {
        match child.try_wait() {
            Ok(Some(_)) => break,
            Ok(None) => {
                if start.elapsed() > std::time::Duration::from_secs(5) {
                    let _ = child.kill();
                    let _ = child.wait();
                    return Err(InspectorError::NoOffset);
                }
                std::thread::sleep(std::time::Duration::from_millis(50));
            }
            Err(e) => return Err(InspectorError::IoError(e)),
        }
    }

    let output = child.wait_with_output()?;
    let stdout = String::from_utf8_lossy(&output.stdout);
    stdout
        .trim()
        .parse::<u64>()
        .map_err(|_| InspectorError::NoOffset)
}

/// Extract specific files from the AppImage squashfs into a temp directory.
fn extract_metadata_files(
    appimage_path: &Path,
    offset: u64,
    dest: &Path,
) -> Result<(), InspectorError> {
    let status = Command::new("unsquashfs")
        .arg("-offset")
        .arg(offset.to_string())
        .arg("-no-progress")
        .arg("-force")
        .arg("-dest")
        .arg(dest)
        .arg(appimage_path)
        .arg("*.desktop")
        .arg("usr/share/applications/*.desktop")
        .arg(".DirIcon")
        .arg("*.png")
        .arg("*.svg")
        .arg("usr/share/icons/*")
        .arg("usr/share/metainfo/*.xml")
        .arg("usr/share/appdata/*.xml")
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status();

    match status {
        Ok(s) if s.success() => Ok(()),
        Ok(s) => Err(InspectorError::UnsquashfsFailed(
            format!("exit code {}", s.code().unwrap_or(-1)),
        )),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            Err(InspectorError::UnsquashfsNotFound)
        }
        Err(e) => Err(InspectorError::IoError(e)),
    }
}

/// Try extraction without offset (for cases where --appimage-offset fails).
fn extract_metadata_files_direct(
    appimage_path: &Path,
    dest: &Path,
) -> Result<(), InspectorError> {
    let status = Command::new("unsquashfs")
        .arg("-no-progress")
        .arg("-force")
        .arg("-dest")
        .arg(dest)
        .arg(appimage_path)
        .arg("*.desktop")
        .arg("usr/share/applications/*.desktop")
        .arg(".DirIcon")
        .arg("*.png")
        .arg("*.svg")
        .arg("usr/share/icons/*")
        .arg("usr/share/metainfo/*.xml")
        .arg("usr/share/appdata/*.xml")
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status();

    match status {
        Ok(s) if s.success() => Ok(()),
        Ok(_) => Err(InspectorError::UnsquashfsFailed(
            "direct extraction failed".into(),
        )),
        Err(e) => Err(InspectorError::IoError(e)),
    }
}

/// Find the first .desktop file in the extract directory.
/// Checks root level first, then usr/share/applications/.
fn find_desktop_file(dir: &Path) -> Option<PathBuf> {
    // Check root of extract dir
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().and_then(|e| e.to_str()) == Some("desktop") {
                return Some(path);
            }
        }
    }
    // Check usr/share/applications/
    let apps_dir = dir.join("usr/share/applications");
    if let Ok(entries) = fs::read_dir(&apps_dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().and_then(|e| e.to_str()) == Some("desktop") {
                return Some(path);
            }
        }
    }
    None
}

/// Parse a .desktop file into structured fields.
fn parse_desktop_entry(content: &str) -> DesktopEntryFields {
    let mut fields = DesktopEntryFields::default();
    let mut in_section = false;

    for line in content.lines() {
        let line = line.trim();
        if line == "[Desktop Entry]" {
            in_section = true;
            continue;
        }
        if line.starts_with('[') {
            in_section = false;
            continue;
        }
        if !in_section {
            continue;
        }
        if let Some((key, value)) = line.split_once('=') {
            let key = key.trim();
            let value = value.trim();
            match key {
                "Name" => fields.name = Some(value.to_string()),
                "Icon" => fields.icon = Some(value.to_string()),
                "Comment" => fields.comment = Some(value.to_string()),
                "Categories" => {
                    fields.categories = value
                        .split(';')
                        .filter(|s| !s.is_empty())
                        .map(String::from)
                        .collect();
                }
                "Exec" => fields.exec = Some(value.to_string()),
                "X-AppImage-Version" => fields.version = Some(value.to_string()),
                "GenericName" => fields.generic_name = Some(value.to_string()),
                "Keywords" => {
                    fields.keywords = value
                        .split(';')
                        .filter(|s| !s.is_empty())
                        .map(String::from)
                        .collect();
                }
                "MimeType" => {
                    fields.mime_types = value
                        .split(';')
                        .filter(|s| !s.is_empty())
                        .map(String::from)
                        .collect();
                }
                "Terminal" => fields.terminal = value == "true",
                "X-AppImage-Name" => fields.x_appimage_name = Some(value.to_string()),
                "StartupWMClass" => fields.startup_wm_class = Some(value.to_string()),
                "Actions" => {
                    fields.actions = value
                        .split(';')
                        .filter(|s| !s.is_empty())
                        .map(String::from)
                        .collect();
                }
                _ => {}
            }
        }
    }

    fields
}

/// Try to extract a version from the filename.
/// Common patterns: App-1.2.3-x86_64.AppImage, App_v1.2.3.AppImage
fn extract_version_from_filename(filename: &str) -> Option<String> {
    // Strip .AppImage extension
    let stem = filename.strip_suffix(".AppImage")
        .or_else(|| filename.strip_suffix(".appimage"))
        .unwrap_or(filename);

    // Look for version-like patterns: digits.digits or digits.digits.digits
    let re_like = |s: &str| -> Option<String> {
        let mut best: Option<(usize, &str)> = None;
        for (i, _) in s.match_indices(|c: char| c.is_ascii_digit()) {
            // Walk back to find start of version (might have leading 'v')
            let start = if i > 0 && s.as_bytes()[i - 1] == b'v' {
                i - 1
            } else {
                i
            };

            // Walk forward to consume version string
            let rest = &s[i..];
            let end = rest
                .find(|c: char| !c.is_ascii_digit() && c != '.')
                .unwrap_or(rest.len());
            let candidate = &rest[..end];

            // Must contain at least one dot (to be a version, not just a number)
            if candidate.contains('.') && candidate.len() > 2 {
                let full = &s[start..i + end];
                if best.is_none() || full.len() > best.unwrap().1.len() {
                    best = Some((start, full));
                }
            }
        }
        best.map(|(_, v)| v.to_string())
    };

    re_like(stem)
}

/// Read the ELF architecture from the header.
fn detect_architecture(path: &Path) -> Option<String> {
    let mut file = fs::File::open(path).ok()?;
    let mut header = [0u8; 20];
    file.read_exact(&mut header).ok()?;

    // Validate ELF magic
    if &header[0..4] != b"\x7FELF" {
        return None;
    }

    // ELF e_machine at offset 18, endianness from byte 5
    let machine = if header[5] == 2 {
        // Big-endian
        u16::from_be_bytes([header[18], header[19]])
    } else {
        // Little-endian (default)
        u16::from_le_bytes([header[18], header[19]])
    };

    match machine {
        0x03 => Some("i386".to_string()),
        0x3E => Some("x86_64".to_string()),
        0xB7 => Some("aarch64".to_string()),
        0x28 => Some("armhf".to_string()),
        _ => Some(format!("unknown(0x{:02X})", machine)),
    }
}

/// Find an icon file in the extracted squashfs directory.
fn find_icon(extract_dir: &Path, icon_name: Option<&str>) -> Option<PathBuf> {
    // First try .DirIcon (skip if it's a broken symlink)
    let dir_icon = extract_dir.join(".DirIcon");
    if dir_icon.exists() && dir_icon.metadata().is_ok() {
        return Some(dir_icon);
    }

    // Try icon by name from .desktop
    if let Some(name) = icon_name {
        // Check root of extract dir
        for ext in &["png", "svg", "xpm"] {
            let candidate = extract_dir.join(format!("{}.{}", name, ext));
            if candidate.exists() {
                return Some(candidate);
            }
        }

        // Check usr/share/icons recursively (prefer largest resolution)
        let icons_dir = extract_dir.join("usr/share/icons");
        if icons_dir.exists() {
            if let Some(found) = find_icon_recursive(&icons_dir, name) {
                return Some(found);
            }
        }
    }

    // Fallback: grab any .png or .svg at the root level
    if let Ok(entries) = fs::read_dir(extract_dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_file() {
                let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
                if ext == "png" || ext == "svg" {
                    return Some(path);
                }
            }
        }
    }

    None
}

fn find_icon_recursive(dir: &Path, name: &str) -> Option<PathBuf> {
    let entries = fs::read_dir(dir).ok()?;
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            if let Some(found) = find_icon_recursive(&path, name) {
                return Some(found);
            }
        } else {
            let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
            if stem == name {
                return Some(path);
            }
        }
    }
    None
}

/// Find an AppStream metainfo XML file in the extract directory.
fn find_appstream_file(extract_dir: &Path) -> Option<PathBuf> {
    // Check modern path first
    let metainfo_dir = extract_dir.join("usr/share/metainfo");
    if let Ok(entries) = fs::read_dir(&metainfo_dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().and_then(|e| e.to_str()) == Some("xml") {
                return Some(path);
            }
        }
    }
    // Check legacy path
    let appdata_dir = extract_dir.join("usr/share/appdata");
    if let Ok(entries) = fs::read_dir(&appdata_dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().and_then(|e| e.to_str()) == Some("xml") {
                return Some(path);
            }
        }
    }
    None
}

/// Check if an AppImage has a GPG signature by looking for the .sha256_sig section name.
fn detect_signature(path: &Path) -> bool {
    use std::io::{BufReader, Read};
    let file = match fs::File::open(path) {
        Ok(f) => f,
        Err(_) => return false,
    };
    let needle = b".sha256_sig";
    let mut reader = BufReader::new(file);
    let mut buf = vec![0u8; 64 * 1024];
    let mut carry = Vec::new();

    loop {
        let n = match reader.read(&mut buf) {
            Ok(0) => break,
            Ok(n) => n,
            Err(_) => break,
        };
        // Prepend carry bytes from previous chunk to handle needle spanning chunks
        let search_buf = if carry.is_empty() {
            &buf[..n]
        } else {
            carry.extend_from_slice(&buf[..n]);
            carry.as_slice()
        };
        if search_buf.windows(needle.len()).any(|w| w == needle) {
            return true;
        }
        // Keep the last (needle.len - 1) bytes as carry for the next iteration
        let keep = needle.len() - 1;
        carry.clear();
        if n >= keep {
            carry.extend_from_slice(&buf[n - keep..n]);
        } else {
            carry.extend_from_slice(&buf[..n]);
        }
    }
    false
}

/// Cache an icon file to the driftwood icons directory.
fn cache_icon(source: &Path, app_id: &str) -> Option<PathBuf> {
    let ext = source
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("png");
    let dest = icons_cache_dir().join(format!("{}.{}", app_id, ext));
    fs::copy(source, &dest).ok()?;
    Some(dest)
}

/// Make a filesystem-safe app ID from a name.
fn make_app_id(name: &str) -> String {
    name.chars()
        .map(|c| {
            if c.is_alphanumeric() || c == '-' || c == '_' {
                c.to_ascii_lowercase()
            } else {
                '-'
            }
        })
        .collect::<String>()
        .trim_matches('-')
        .to_string()
}

/// Quickly extract just the icon from an AppImage (for preview).
/// Only extracts .DirIcon and root-level .png/.svg files.
/// Returns the path to the cached icon if successful.
pub fn extract_icon_fast(appimage_path: &Path) -> Option<PathBuf> {
    if !has_unsquashfs() {
        return None;
    }

    let offset = find_squashfs_offset(appimage_path).ok()?;
    let tmp = tempfile::tempdir().ok()?;
    let dest = tmp.path().join("icon_extract");

    let status = Command::new("unsquashfs")
        .arg("-offset")
        .arg(offset.to_string())
        .arg("-no-progress")
        .arg("-force")
        .arg("-dest")
        .arg(&dest)
        .arg(appimage_path)
        .arg(".DirIcon")
        .arg("*.png")
        .arg("*.svg")
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()
        .ok()?;

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

    let icon_path = find_icon(&dest, None)?;

    // Generate app_id from filename
    let stem = appimage_path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("unknown");
    let app_id = make_app_id(stem);

    cache_icon(&icon_path, &app_id)
}

/// Inspect an AppImage and extract its metadata.
pub fn inspect_appimage(
    path: &Path,
    appimage_type: &AppImageType,
) -> Result<AppImageMetadata, InspectorError> {
    if !has_unsquashfs() {
        return Err(InspectorError::UnsquashfsNotFound);
    }

    let temp_dir = tempfile::tempdir()?;
    let extract_dir = temp_dir.path().join("squashfs-root");

    // Try to extract metadata files
    let extracted = match appimage_type {
        AppImageType::Type2 => {
            match get_squashfs_offset(path) {
                Ok(offset) => extract_metadata_files(path, offset, &extract_dir),
                Err(_) => {
                    log::warn!(
                        "Could not get offset for {}, trying direct extraction",
                        path.display()
                    );
                    extract_metadata_files_direct(path, &extract_dir)
                }
            }
        }
        AppImageType::Type1 => extract_metadata_files_direct(path, &extract_dir),
    };

    if let Err(e) = extracted {
        log::warn!("Extraction failed for {}: {}", path.display(), e);
        // Return minimal metadata from filename/ELF
        let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
        return Ok(AppImageMetadata {
            app_name: Some(
                filename
                    .strip_suffix(".AppImage")
                    .or_else(|| filename.strip_suffix(".appimage"))
                    .unwrap_or(filename)
                    .split(|c: char| c == '-' || c == '_')
                    .next()
                    .unwrap_or(filename)
                    .to_string(),
            ),
            app_version: extract_version_from_filename(filename),
            architecture: detect_architecture(path),
            ..Default::default()
        });
    }

    // Find and parse .desktop file
    let desktop_path = find_desktop_file(&extract_dir)
        .ok_or(InspectorError::NoDesktopEntry)?;
    let desktop_content = fs::read_to_string(&desktop_path)?;
    let fields = parse_desktop_entry(&desktop_content);

    // Parse AppStream metainfo XML if available
    let appstream = find_appstream_file(&extract_dir)
        .and_then(|p| crate::core::appstream::parse_appstream_file(&p));

    // Merge: AppStream takes priority for overlapping fields
    let final_name = appstream
        .as_ref()
        .and_then(|a| a.name.clone())
        .or(fields.name);
    let final_description = appstream
        .as_ref()
        .and_then(|a| a.description.clone())
        .or(appstream.as_ref().and_then(|a| a.summary.clone()))
        .or(fields.comment);
    let final_developer = appstream.as_ref().and_then(|a| a.developer.clone());
    let final_categories = if let Some(ref a) = appstream {
        if !a.categories.is_empty() {
            a.categories.clone()
        } else {
            fields.categories
        }
    } else {
        fields.categories
    };

    // Determine version (desktop entry > filename heuristic)
    let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
    let version = fields
        .version
        .or_else(|| extract_version_from_filename(filename));

    // Find and cache icon
    let icon = find_icon(&extract_dir, fields.icon.as_deref());
    let app_id = make_app_id(
        final_name.as_deref().unwrap_or(
            filename
                .strip_suffix(".AppImage")
                .unwrap_or(filename),
        ),
    );
    let cached_icon = icon.and_then(|icon_path| cache_icon(&icon_path, &app_id));

    // Merge keywords from both sources
    let mut all_keywords = fields.keywords;
    if let Some(ref a) = appstream {
        for kw in &a.keywords {
            if !all_keywords.contains(kw) {
                all_keywords.push(kw.clone());
            }
        }
    }

    // Merge MIME types from both sources
    let mut all_mime_types = fields.mime_types;
    if let Some(ref a) = appstream {
        for mt in &a.mime_types {
            if !all_mime_types.contains(mt) {
                all_mime_types.push(mt.clone());
            }
        }
    }

    let has_sig = detect_signature(path);

    Ok(AppImageMetadata {
        app_name: final_name,
        app_version: version,
        description: final_description,
        developer: final_developer,
        icon_name: fields.icon,
        categories: final_categories,
        desktop_entry_content: desktop_content,
        architecture: detect_architecture(path),
        cached_icon_path: cached_icon,
        appstream_id: appstream.as_ref().and_then(|a| a.id.clone()),
        appstream_description: appstream.as_ref().and_then(|a| a.description.clone()),
        generic_name: fields
            .generic_name
            .or_else(|| appstream.as_ref().and_then(|a| a.summary.clone())),
        license: appstream.as_ref().and_then(|a| a.project_license.clone()),
        homepage_url: appstream
            .as_ref()
            .and_then(|a| a.urls.get("homepage").cloned()),
        bugtracker_url: appstream
            .as_ref()
            .and_then(|a| a.urls.get("bugtracker").cloned()),
        donation_url: appstream
            .as_ref()
            .and_then(|a| a.urls.get("donation").cloned()),
        help_url: appstream
            .as_ref()
            .and_then(|a| a.urls.get("help").cloned()),
        vcs_url: appstream
            .as_ref()
            .and_then(|a| a.urls.get("vcs-browser").cloned()),
        keywords: all_keywords,
        mime_types: all_mime_types,
        content_rating: appstream
            .as_ref()
            .and_then(|a| a.content_rating_summary.clone()),
        project_group: appstream.as_ref().and_then(|a| a.project_group.clone()),
        releases: appstream
            .as_ref()
            .map(|a| a.releases.clone())
            .unwrap_or_default(),
        desktop_actions: fields.actions,
        has_signature: has_sig,
        screenshot_urls: appstream
            .as_ref()
            .map(|a| a.screenshot_urls.clone())
            .unwrap_or_default(),
        startup_wm_class: fields.startup_wm_class,
    })
}

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

    #[test]
    fn test_parse_desktop_entry() {
        let content = "[Desktop Entry]
Type=Application
Name=Test App
Icon=test-icon
Comment=A test application
Categories=Utility;Development;
Exec=test %U
X-AppImage-Version=1.2.3

[Desktop Action New]
Name=New Window
";
        let fields = parse_desktop_entry(content);
        assert_eq!(fields.name.as_deref(), Some("Test App"));
        assert_eq!(fields.icon.as_deref(), Some("test-icon"));
        assert_eq!(fields.comment.as_deref(), Some("A test application"));
        assert_eq!(fields.categories, vec!["Utility", "Development"]);
        assert_eq!(fields.exec.as_deref(), Some("test %U"));
        assert_eq!(fields.version.as_deref(), Some("1.2.3"));
    }

    #[test]
    fn test_version_from_filename() {
        assert_eq!(
            extract_version_from_filename("Firefox-124.0.1-x86_64.AppImage"),
            Some("124.0.1".to_string())
        );
        assert_eq!(
            extract_version_from_filename("Kdenlive-24.02.1-x86_64.AppImage"),
            Some("24.02.1".to_string())
        );
        assert_eq!(
            extract_version_from_filename("SimpleApp.AppImage"),
            None
        );
        assert_eq!(
            extract_version_from_filename("App_v2.0.0.AppImage"),
            Some("v2.0.0".to_string())
        );
    }

    #[test]
    fn test_make_app_id() {
        assert_eq!(make_app_id("Firefox"), "firefox");
        assert_eq!(make_app_id("My Cool App"), "my-cool-app");
        assert_eq!(make_app_id("App 2.0"), "app-2-0");
    }

    #[test]
    fn test_detect_architecture() {
        // Create a minimal ELF header for x86_64
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("test_elf");
        let mut header = vec![0u8; 20];
        // ELF magic
        header[0..4].copy_from_slice(&[0x7F, 0x45, 0x4C, 0x46]);
        // e_machine = 0x3E (x86_64) at offset 18, little-endian
        header[18] = 0x3E;
        header[19] = 0x00;
        fs::write(&path, &header).unwrap();

        assert_eq!(detect_architecture(&path), Some("x86_64".to_string()));
    }
}
