//! High-level public API for archive extraction, creation, and inspection.

use std::path::Path;

use crate::ExtractionError;
use crate::ExtractionReport;
use crate::NoopProgress;
use crate::ProgressCallback;
use crate::Result;
use crate::SecurityConfig;
use crate::config::ExtractionOptions;
use crate::creation::CreationConfig;
use crate::creation::CreationReport;
use crate::formats::detect::ArchiveType;
use crate::formats::detect::detect_format;
use crate::formats::detect::is_zip_family_alias;
use crate::inspection::ArchiveManifest;
use crate::inspection::VerificationReport;

/// Extracts an archive to the specified output directory.
///
/// This is the main high-level API for extracting archives with security
/// validation. The archive format is automatically detected.
///
/// # Arguments
///
/// * `archive_path` - Path to the archive file
/// * `output_dir` - Directory where files will be extracted
/// * `config` - Security configuration for the extraction
///
/// # Errors
///
/// Returns an error if:
/// - Archive file cannot be opened
/// - Archive format is unsupported
/// - Security validation fails
/// - I/O operations fail
///
/// # Examples
///
/// ```no_run
/// use exarch_core::SecurityConfig;
/// use exarch_core::extract_archive;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let config = SecurityConfig::default();
/// let report = extract_archive("archive.tar.gz", "/tmp/output", &config)?;
/// println!("Extracted {} files", report.files_extracted);
/// # Ok(())
/// # }
/// ```
pub fn extract_archive<P: AsRef<Path>, Q: AsRef<Path>>(
    archive_path: P,
    output_dir: Q,
    config: &SecurityConfig,
) -> Result<ExtractionReport> {
    let mut noop = NoopProgress;
    extract_archive_with_progress(archive_path, output_dir, config, &mut noop)
}

/// Extracts an archive with progress reporting.
///
/// Same as `extract_archive` but accepts a `ProgressCallback` for
/// real-time progress updates during extraction.
///
/// # Arguments
///
/// * `archive_path` - Path to the archive file
/// * `output_dir` - Directory where files will be extracted
/// * `config` - Security configuration for the extraction
/// * `progress` - Callback for progress updates
///
/// # Errors
///
/// Returns an error if:
/// - Archive file cannot be opened
/// - Archive format is unsupported
/// - Security validation fails
/// - I/O operations fail
///
/// # Examples
///
/// ```no_run
/// use exarch_core::NoopProgress;
/// use exarch_core::SecurityConfig;
/// use exarch_core::extract_archive_with_progress;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let config = SecurityConfig::default();
/// let mut progress = NoopProgress;
/// let report =
///     extract_archive_with_progress("archive.tar.gz", "/tmp/output", &config, &mut progress)?;
/// println!("Extracted {} files", report.files_extracted);
/// # Ok(())
/// # }
/// ```
pub fn extract_archive_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
    archive_path: P,
    output_dir: Q,
    config: &SecurityConfig,
    progress: &mut dyn ProgressCallback,
) -> Result<ExtractionReport> {
    let options = ExtractionOptions::default();
    extract_archive_with_progress_and_options(archive_path, output_dir, config, &options, progress)
}

fn extract_archive_with_progress_and_options<P: AsRef<Path>, Q: AsRef<Path>>(
    archive_path: P,
    output_dir: Q,
    config: &SecurityConfig,
    options: &ExtractionOptions,
    _progress: &mut dyn ProgressCallback,
) -> Result<ExtractionReport> {
    let archive_path = archive_path.as_ref();
    let output_dir = output_dir.as_ref();

    // Detect archive format from file extension
    let format = detect_format(archive_path)?;

    // Dispatch to format-specific extraction
    match format {
        ArchiveType::Tar => extract_tar(archive_path, output_dir, config, options),
        ArchiveType::TarGz => extract_tar_gz(archive_path, output_dir, config, options),
        ArchiveType::TarBz2 => extract_tar_bz2(archive_path, output_dir, config, options),
        ArchiveType::TarXz => extract_tar_xz(archive_path, output_dir, config, options),
        ArchiveType::TarZst => extract_tar_zst(archive_path, output_dir, config, options),
        ArchiveType::Zip => extract_zip(archive_path, output_dir, config, options),
        ArchiveType::SevenZ => extract_7z(archive_path, output_dir, config, options),
    }
}

/// Extracts an archive with extraction options and optional progress reporting.
///
/// This is the most flexible extraction API. Use this when you need both
/// `ExtractionOptions` (e.g., atomic mode) and progress reporting.
///
/// # Arguments
///
/// * `archive_path` - Path to the archive file
/// * `output_dir` - Directory where files will be extracted
/// * `config` - Security configuration for the extraction
/// * `options` - Extraction behavior options (e.g., atomic mode)
/// * `progress` - Callback for progress updates
///
/// # Errors
///
/// Returns an error if:
/// - Archive file cannot be opened
/// - Archive format is unsupported
/// - Security validation fails
/// - I/O operations fail
/// - Atomic temp dir creation or rename fails
pub fn extract_archive_full<P: AsRef<Path>, Q: AsRef<Path>>(
    archive_path: P,
    output_dir: Q,
    config: &SecurityConfig,
    options: &ExtractionOptions,
    progress: &mut dyn ProgressCallback,
) -> Result<ExtractionReport> {
    if options.atomic {
        extract_atomic(archive_path, output_dir, config, options, progress)
    } else {
        extract_archive_with_progress_and_options(
            archive_path,
            output_dir,
            config,
            options,
            progress,
        )
    }
}

/// Extracts an archive with extraction options (no progress reporting).
///
/// Same as `extract_archive_full` but uses a no-op progress callback.
///
/// # Errors
///
/// Returns an error if:
/// - Archive file cannot be opened
/// - Archive format is unsupported
/// - Security validation fails
/// - I/O operations fail
/// - Atomic temp dir creation or rename fails
pub fn extract_archive_with_options<P: AsRef<Path>, Q: AsRef<Path>>(
    archive_path: P,
    output_dir: Q,
    config: &SecurityConfig,
    options: &ExtractionOptions,
) -> Result<ExtractionReport> {
    let mut noop = NoopProgress;
    extract_archive_full(archive_path, output_dir, config, options, &mut noop)
}

fn extract_atomic<P: AsRef<Path>, Q: AsRef<Path>>(
    archive_path: P,
    output_dir: Q,
    config: &SecurityConfig,
    options: &ExtractionOptions,
    progress: &mut dyn ProgressCallback,
) -> Result<ExtractionReport> {
    let output_dir = output_dir.as_ref();

    // Canonicalize output_dir to resolve any symlinks in the path before
    // computing the parent, so temp dir lands on the same filesystem.
    // If output_dir doesn't exist yet, use its lexical parent.
    let canonical_output = if output_dir.exists() {
        output_dir.canonicalize().map_err(ExtractionError::Io)?
    } else {
        output_dir.to_path_buf()
    };

    let parent =
        canonical_output
            .parent()
            .ok_or_else(|| ExtractionError::InvalidConfiguration {
                reason: "output directory has no parent".into(),
            })?;

    std::fs::create_dir_all(parent).map_err(ExtractionError::Io)?;

    let temp_dir = tempfile::tempdir_in(parent).map_err(|e| {
        ExtractionError::Io(std::io::Error::new(
            e.kind(),
            format!(
                "failed to create temp directory in {}: {e}",
                parent.display()
            ),
        ))
    })?;

    let result = extract_archive_with_progress_and_options(
        archive_path,
        temp_dir.path(),
        config,
        options,
        progress,
    );

    match result {
        Ok(report) => {
            // Consume TempDir to prevent Drop cleanup, then rename.
            let temp_path = temp_dir.keep();
            std::fs::rename(&temp_path, output_dir).map_err(|e| {
                // Rename failed: clean up temp dir
                let _ = std::fs::remove_dir_all(&temp_path);
                // Map AlreadyExists to OutputExists for caller clarity
                if e.kind() == std::io::ErrorKind::AlreadyExists {
                    ExtractionError::OutputExists {
                        path: output_dir.to_path_buf(),
                    }
                } else {
                    ExtractionError::Io(std::io::Error::new(
                        e.kind(),
                        format!("failed to rename temp dir to {}: {e}", output_dir.display()),
                    ))
                }
            })?;

            Ok(report)
        }
        Err(e) => {
            // TempDir Drop runs here: cleans up temp dir automatically.
            Err(e)
        }
    }
}

fn extract_tar(
    archive_path: &Path,
    output_dir: &Path,
    config: &SecurityConfig,
    options: &ExtractionOptions,
) -> Result<ExtractionReport> {
    use crate::formats::TarArchive;
    use crate::formats::traits::ArchiveFormat;
    use std::fs::File;
    use std::io::BufReader;

    let file = File::open(archive_path)?;
    let reader = BufReader::new(file);
    let mut archive = TarArchive::new(reader);
    archive.extract(output_dir, config, options)
}

fn extract_tar_gz(
    archive_path: &Path,
    output_dir: &Path,
    config: &SecurityConfig,
    options: &ExtractionOptions,
) -> Result<ExtractionReport> {
    use crate::formats::TarArchive;
    use crate::formats::traits::ArchiveFormat;
    use flate2::read::GzDecoder;
    use std::fs::File;
    use std::io::BufReader;

    let file = File::open(archive_path)?;
    let reader = BufReader::new(file);
    let decoder = GzDecoder::new(reader);
    let mut archive = TarArchive::new(decoder);
    archive.extract(output_dir, config, options)
}

fn extract_tar_bz2(
    archive_path: &Path,
    output_dir: &Path,
    config: &SecurityConfig,
    options: &ExtractionOptions,
) -> Result<ExtractionReport> {
    use crate::formats::TarArchive;
    use crate::formats::traits::ArchiveFormat;
    use bzip2::read::BzDecoder;
    use std::fs::File;
    use std::io::BufReader;

    let file = File::open(archive_path)?;
    let reader = BufReader::new(file);
    let decoder = BzDecoder::new(reader);
    let mut archive = TarArchive::new(decoder);
    archive.extract(output_dir, config, options)
}

fn extract_tar_xz(
    archive_path: &Path,
    output_dir: &Path,
    config: &SecurityConfig,
    options: &ExtractionOptions,
) -> Result<ExtractionReport> {
    use crate::formats::TarArchive;
    use crate::formats::traits::ArchiveFormat;
    use std::fs::File;
    use std::io::BufReader;
    use xz2::read::XzDecoder;

    let file = File::open(archive_path)?;
    let reader = BufReader::new(file);
    let decoder = XzDecoder::new(reader);
    let mut archive = TarArchive::new(decoder);
    archive.extract(output_dir, config, options)
}

fn extract_tar_zst(
    archive_path: &Path,
    output_dir: &Path,
    config: &SecurityConfig,
    options: &ExtractionOptions,
) -> Result<ExtractionReport> {
    use crate::formats::TarArchive;
    use crate::formats::traits::ArchiveFormat;
    use std::fs::File;
    use std::io::BufReader;
    use zstd::stream::read::Decoder as ZstdDecoder;

    let file = File::open(archive_path)?;
    let reader = BufReader::new(file);
    let decoder = ZstdDecoder::new(reader)?;
    let mut archive = TarArchive::new(decoder);
    archive.extract(output_dir, config, options)
}

fn extract_zip(
    archive_path: &Path,
    output_dir: &Path,
    config: &SecurityConfig,
    options: &ExtractionOptions,
) -> Result<ExtractionReport> {
    use crate::formats::ZipArchive;
    use crate::formats::traits::ArchiveFormat;
    use std::fs::File;

    let file = File::open(archive_path)?;
    let mut archive = ZipArchive::new(file)?;
    archive.extract(output_dir, config, options)
}

fn extract_7z(
    archive_path: &Path,
    output_dir: &Path,
    config: &SecurityConfig,
    options: &ExtractionOptions,
) -> Result<ExtractionReport> {
    use crate::formats::SevenZArchive;
    use crate::formats::traits::ArchiveFormat;
    use std::fs::File;

    let file = File::open(archive_path)?;
    let mut archive = SevenZArchive::new(file)?;
    archive.extract(output_dir, config, options)
}

/// Creates an archive from source files and directories.
///
/// Format is auto-detected from output file extension, or can be
/// explicitly set via `config.format`.
///
/// # Arguments
///
/// * `output_path` - Path to the output archive file
/// * `sources` - Source files and directories to include
/// * `config` - Creation configuration
///
/// # Errors
///
/// Returns an error if:
/// - Cannot determine archive format
/// - Source files don't exist
/// - I/O operations fail
/// - Configuration is invalid
///
/// # Examples
///
/// ```no_run
/// use exarch_core::create_archive;
/// use exarch_core::creation::CreationConfig;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let config = CreationConfig::default();
/// let report = create_archive("output.tar.gz", &["src/", "Cargo.toml"], &config)?;
/// println!("Created archive with {} files", report.files_added);
/// # Ok(())
/// # }
/// ```
pub fn create_archive<P: AsRef<Path>, Q: AsRef<Path>>(
    output_path: P,
    sources: &[Q],
    config: &CreationConfig,
) -> Result<CreationReport> {
    let mut noop = NoopProgress;
    create_archive_with_progress(output_path, sources, config, &mut noop)
}

/// Creates an archive with progress reporting.
///
/// Same as `create_archive` but accepts a `ProgressCallback` for
/// real-time progress updates during creation.
///
/// # Arguments
///
/// * `output_path` - Path to the output archive file
/// * `sources` - Source files and directories to include
/// * `config` - Creation configuration
/// * `progress` - Callback for progress updates
///
/// # Errors
///
/// Returns an error if:
/// - Cannot determine archive format
/// - Source files don't exist
/// - I/O operations fail
/// - Configuration is invalid
///
/// # Examples
///
/// ```no_run
/// use exarch_core::NoopProgress;
/// use exarch_core::create_archive_with_progress;
/// use exarch_core::creation::CreationConfig;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let config = CreationConfig::default();
/// let mut progress = NoopProgress;
/// let report = create_archive_with_progress(
///     "output.tar.gz",
///     &["src/", "Cargo.toml"],
///     &config,
///     &mut progress,
/// )?;
/// println!("Created archive with {} files", report.files_added);
/// # Ok(())
/// # }
/// ```
pub fn create_archive_with_progress<P: AsRef<Path>, Q: AsRef<Path>>(
    output_path: P,
    sources: &[Q],
    config: &CreationConfig,
    progress: &mut dyn ProgressCallback,
) -> Result<CreationReport> {
    let output = output_path.as_ref();

    // Block creation for the ZIP-family extensions (mirrors the 7z block
    // below). They're all ZIP underneath but add extra requirements -
    // signing (apk/aab/ipa/appx/msix), checksum manifests (whl), ordering
    // and stored-compression rules (epub), descriptor files
    // (war/ear/vsix/nbm) - which exarch doesn't produce. Silently emitting
    // a bare ZIP with one of these extensions would be misleading, so we
    // error instead. Callers who need the override can set
    // CreationConfig::format = Some(ArchiveType::Zip).
    if config.format.is_none() {
        reject_zip_family_creation(output)?;
    }

    // Determine format from extension or config
    let format = determine_creation_format(output, config)?;

    // Dispatch to format-specific creator with progress
    match format {
        ArchiveType::Tar => {
            crate::creation::tar::create_tar_with_progress(output, sources, config, progress)
        }
        ArchiveType::TarGz => {
            crate::creation::tar::create_tar_gz_with_progress(output, sources, config, progress)
        }
        ArchiveType::TarBz2 => {
            crate::creation::tar::create_tar_bz2_with_progress(output, sources, config, progress)
        }
        ArchiveType::TarXz => {
            crate::creation::tar::create_tar_xz_with_progress(output, sources, config, progress)
        }
        ArchiveType::TarZst => {
            crate::creation::tar::create_tar_zst_with_progress(output, sources, config, progress)
        }
        ArchiveType::Zip => {
            crate::creation::zip::create_zip_with_progress(output, sources, config, progress)
        }
        ArchiveType::SevenZ => Err(ExtractionError::InvalidArchive(
            "7z archive creation not yet supported".into(),
        )),
    }
}

/// Lists archive contents without extracting.
///
/// Returns a manifest containing metadata for all entries in the archive.
/// No files are written to disk during this operation.
///
/// # Arguments
///
/// * `archive_path` - Path to archive file
/// * `config` - Security configuration (quota limits apply)
///
/// # Errors
///
/// Returns error if:
/// - Archive file cannot be opened
/// - Archive format is unsupported or corrupted
/// - Quota limits exceeded (file count, total size)
///
/// # Examples
///
/// ```no_run
/// use exarch_core::SecurityConfig;
/// use exarch_core::list_archive;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let config = SecurityConfig::default();
/// let manifest = list_archive("archive.tar.gz", &config)?;
///
/// println!("Archive contains {} files", manifest.total_entries);
/// for entry in manifest.entries {
///     println!("{}: {} bytes", entry.path.display(), entry.size);
/// }
/// # Ok(())
/// # }
/// ```
pub fn list_archive<P: AsRef<Path>>(
    archive_path: P,
    config: &SecurityConfig,
) -> Result<ArchiveManifest> {
    crate::inspection::list_archive(archive_path, config)
}

/// Verifies archive integrity and security without extracting.
///
/// Performs comprehensive validation:
/// - Integrity checks (structure, checksums)
/// - Security checks (path traversal, zip bombs, CVEs)
/// - Policy checks (file types, permissions)
///
/// # Arguments
///
/// * `archive_path` - Path to archive file
/// * `config` - Security configuration for validation
///
/// # Errors
///
/// Returns error if:
/// - Archive file cannot be opened
/// - Archive is severely corrupted (cannot read structure)
///
/// Security violations are reported in `VerificationReport.issues`,
/// not as errors.
///
/// # Examples
///
/// ```no_run
/// use exarch_core::SecurityConfig;
/// use exarch_core::VerificationStatus;
/// use exarch_core::verify_archive;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let config = SecurityConfig::default();
/// let report = verify_archive("archive.tar.gz", &config)?;
///
/// if report.status == VerificationStatus::Pass {
///     println!("Archive is safe to extract");
/// } else {
///     eprintln!("Security issues found:");
///     for issue in report.issues {
///         eprintln!("  [{}] {}", issue.severity, issue.message);
///     }
/// }
/// # Ok(())
/// # }
/// ```
pub fn verify_archive<P: AsRef<Path>>(
    archive_path: P,
    config: &SecurityConfig,
) -> Result<VerificationReport> {
    crate::inspection::verify_archive(archive_path, config)
}

/// Rejects creation for ZIP-family extensions that aren't plain `.zip`.
///
/// See the call site in `create_archive_with_progress` for the rationale.
/// Returns `Ok(())` for anything else - `.zip`, tar variants, unknown
/// extensions (those get caught later by `detect_format`).
fn reject_zip_family_creation(output: &Path) -> Result<()> {
    let Some(ext) = output.extension().and_then(|e| e.to_str()) else {
        return Ok(());
    };
    if is_zip_family_alias(ext) {
        let ext_lower = ext.to_ascii_lowercase();
        return Err(ExtractionError::InvalidArchive(format!(
            "creation for .{ext_lower} isn't supported: the format is ZIP-based but \
             requires extra structure (signing, manifests, ordering) that exarch \
             doesn't produce. Use .zip, or set CreationConfig::format = Some(\
             exarch_core::formats::detect::ArchiveType::Zip) to override."
        )));
    }
    Ok(())
}

/// Determines archive format from output path or config.
fn determine_creation_format(output: &Path, config: &CreationConfig) -> Result<ArchiveType> {
    // If format explicitly set in config, use it
    if let Some(format) = config.format {
        return Ok(format);
    }

    // Auto-detect from extension
    detect_format(output)
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    #[test]
    fn test_extract_archive_nonexistent_file() {
        let config = SecurityConfig::default();
        let result = extract_archive(
            PathBuf::from("nonexistent_test.tar"),
            PathBuf::from("/tmp/test"),
            &config,
        );
        // Should fail because file doesn't exist
        assert!(result.is_err());
    }

    #[test]
    fn test_determine_creation_format_tar() {
        let config = CreationConfig::default();
        let path = PathBuf::from("archive.tar");
        let format = determine_creation_format(&path, &config).unwrap();
        assert_eq!(format, ArchiveType::Tar);
    }

    #[test]
    fn test_determine_creation_format_tar_gz() {
        let config = CreationConfig::default();
        let path = PathBuf::from("archive.tar.gz");
        let format = determine_creation_format(&path, &config).unwrap();
        assert_eq!(format, ArchiveType::TarGz);

        let path2 = PathBuf::from("archive.tgz");
        let format2 = determine_creation_format(&path2, &config).unwrap();
        assert_eq!(format2, ArchiveType::TarGz);
    }

    #[test]
    fn test_determine_creation_format_tar_bz2() {
        let config = CreationConfig::default();
        let path = PathBuf::from("archive.tar.bz2");
        let format = determine_creation_format(&path, &config).unwrap();
        assert_eq!(format, ArchiveType::TarBz2);
    }

    #[test]
    fn test_determine_creation_format_tar_xz() {
        let config = CreationConfig::default();
        let path = PathBuf::from("archive.tar.xz");
        let format = determine_creation_format(&path, &config).unwrap();
        assert_eq!(format, ArchiveType::TarXz);
    }

    #[test]
    fn test_determine_creation_format_tar_zst() {
        let config = CreationConfig::default();
        let path = PathBuf::from("archive.tar.zst");
        let format = determine_creation_format(&path, &config).unwrap();
        assert_eq!(format, ArchiveType::TarZst);
    }

    #[test]
    fn test_determine_creation_format_zip() {
        let config = CreationConfig::default();
        let path = PathBuf::from("archive.zip");
        let format = determine_creation_format(&path, &config).unwrap();
        assert_eq!(format, ArchiveType::Zip);
    }

    #[test]
    fn test_determine_creation_format_explicit() {
        let config = CreationConfig::default().with_format(Some(ArchiveType::TarGz));
        let path = PathBuf::from("archive.xyz");
        let format = determine_creation_format(&path, &config).unwrap();
        assert_eq!(format, ArchiveType::TarGz);
    }

    #[test]
    fn test_determine_creation_format_unknown() {
        let config = CreationConfig::default();
        let path = PathBuf::from("archive.rar");
        let result = determine_creation_format(&path, &config);
        assert!(result.is_err());
    }

    #[test]
    fn test_extract_archive_7z_not_implemented() {
        let dest = tempfile::TempDir::new().unwrap();
        let path = PathBuf::from("test.7z");

        let result = extract_archive(&path, dest.path(), &SecurityConfig::default());

        assert!(result.is_err());
    }

    #[test]
    fn test_create_archive_zip_family_not_supported() {
        // Mirrors test_create_archive_7z_not_supported. Spot-checks a couple
        // of extensions rather than every one - the integration test
        // covers the full list.
        let dest = tempfile::TempDir::new().unwrap();
        for ext in ["apk", "whl", "EPUB"] {
            let archive_path = dest.path().join(format!("output.{ext}"));
            let result = create_archive(&archive_path, &[] as &[&str], &CreationConfig::default());
            assert!(
                matches!(result, Err(ExtractionError::InvalidArchive(_))),
                ".{ext} should be rejected, got {result:?}",
            );
        }
    }

    #[test]
    fn test_create_archive_zip_family_override_bypasses_guard() {
        // Explicit CreationConfig::format = Some(Zip) is the escape hatch -
        // skips the ZIP-family guard. Caller takes responsibility for the
        // resulting file not being spec-valid.
        let dest = tempfile::TempDir::new().unwrap();
        let src = dest.path().join("source.txt");
        std::fs::write(&src, b"hello").unwrap();
        let archive_path = dest.path().join("output.apk");
        let config = CreationConfig::default().with_format(Some(ArchiveType::Zip));
        let result = create_archive(&archive_path, &[&src], &config);
        assert!(
            result.is_ok(),
            "explicit format override should bypass the guard, got {result:?}",
        );
    }

    #[test]
    fn test_create_archive_7z_not_supported() {
        let dest = tempfile::TempDir::new().unwrap();
        let archive_path = dest.path().join("output.7z");

        let result = create_archive(&archive_path, &[] as &[&str], &CreationConfig::default());

        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            ExtractionError::InvalidArchive(_)
        ));
    }

    #[test]
    fn test_extract_archive_full_non_atomic_delegates_to_normal() {
        let dest = tempfile::TempDir::new().unwrap();
        let options = ExtractionOptions {
            atomic: false,
            skip_duplicates: true,
        };
        let result = extract_archive_full(
            PathBuf::from("nonexistent.tar.gz"),
            dest.path(),
            &SecurityConfig::default(),
            &options,
            &mut NoopProgress,
        );
        assert!(result.is_err());
    }

    #[test]
    fn test_extract_archive_with_options_delegates() {
        let dest = tempfile::TempDir::new().unwrap();
        let options = ExtractionOptions {
            atomic: false,
            skip_duplicates: true,
        };
        let result = extract_archive_with_options(
            PathBuf::from("nonexistent.tar.gz"),
            dest.path(),
            &SecurityConfig::default(),
            &options,
        );
        assert!(result.is_err());
    }

    #[test]
    fn test_extract_atomic_success() {
        use crate::create_archive;
        use crate::creation::CreationConfig;

        // Create a valid tar.gz to extract
        let archive_dir = tempfile::TempDir::new().unwrap();
        let archive_path = archive_dir.path().join("test.tar.gz");

        // Create a simple archive with one file
        let src_dir = tempfile::TempDir::new().unwrap();
        std::fs::write(src_dir.path().join("hello.txt"), b"hello world").unwrap();
        create_archive(&archive_path, &[src_dir.path()], &CreationConfig::default()).unwrap();

        let parent = tempfile::TempDir::new().unwrap();
        let output_dir = parent.path().join("extracted");

        let options = ExtractionOptions {
            atomic: true,
            skip_duplicates: true,
        };
        let result = extract_archive_with_options(
            &archive_path,
            &output_dir,
            &SecurityConfig::default(),
            &options,
        );

        assert!(result.is_ok());
        assert!(output_dir.exists());
        // No temp dir remnants
        let temp_entries: Vec<_> = std::fs::read_dir(parent.path()).unwrap().collect();
        assert_eq!(
            temp_entries.len(),
            1,
            "Expected only the output dir, found temp remnants"
        );
    }

    #[test]
    fn test_extract_atomic_failure_cleans_up() {
        let parent = tempfile::TempDir::new().unwrap();
        let output_dir = parent.path().join("extracted");

        let options = ExtractionOptions {
            atomic: true,
            skip_duplicates: true,
        };
        let result = extract_archive_with_options(
            PathBuf::from("nonexistent_archive.tar.gz"),
            &output_dir,
            &SecurityConfig::default(),
            &options,
        );

        assert!(result.is_err());
        // Output dir must not exist
        assert!(!output_dir.exists());
        // No temp dir remnants in parent
        let temp_entries: Vec<_> = std::fs::read_dir(parent.path()).unwrap().collect();
        assert!(
            temp_entries.is_empty(),
            "Temp dir not cleaned up after failure"
        );
    }

    #[test]
    fn test_extract_atomic_output_already_exists_fails() {
        use crate::create_archive;
        use crate::creation::CreationConfig;

        let parent = tempfile::TempDir::new().unwrap();
        let output_dir = parent.path().join("extracted");
        std::fs::create_dir_all(&output_dir).unwrap();
        // Create a file in output_dir so it's non-empty (rename over non-empty dir
        // fails on most OSes)
        std::fs::write(output_dir.join("existing.txt"), b"old content").unwrap();

        let archive_dir = tempfile::TempDir::new().unwrap();
        let archive_path = archive_dir.path().join("test.tar.gz");
        let src_dir = tempfile::TempDir::new().unwrap();
        std::fs::write(src_dir.path().join("new.txt"), b"new content").unwrap();
        create_archive(&archive_path, &[src_dir.path()], &CreationConfig::default()).unwrap();

        let options = ExtractionOptions {
            atomic: true,
            skip_duplicates: true,
        };
        let result = extract_archive_with_options(
            &archive_path,
            &output_dir,
            &SecurityConfig::default(),
            &options,
        );

        // Should fail with OutputExists or Io (platform dependent rename semantics)
        assert!(result.is_err());
        // Output dir must still have old content (not corrupted)
        assert!(output_dir.join("existing.txt").exists());
    }
}
