//! 7z archive format extraction.
//!
//! Provides secure extraction of 7z archives with security validation.
//!
//! # Security Features
//!
//! - Encrypted archives rejected (AES-256, AES-128, `ZipCrypto`, all encryption
//!   methods)
//! - Solid archives rejected by default (configurable)
//! - Path traversal prevention
//! - Decompression bomb detection
//! - Memory exhaustion protection for solid blocks
//! - Windows symlink detection (via reparse point attributes)
//!
//! # Supported Compression Methods
//!
//! - LZMA / LZMA2
//! - BZIP2
//! - `PPMd`
//! - DEFLATE
//! - Copy (stored)
//!
//! # Symlink and Hardlink Limitations
//!
//! Due to sevenz-rust2 0.20 API limitations, symlink and hardlink detection
//! is incomplete:
//!
//! - **Windows symlinks**: Detected via `FILE_ATTRIBUTE_REPARSE_POINT` and
//!   rejected
//! - **Unix symlinks**: Cannot be detected, extracted as regular files (target
//!   path becomes file content)
//! - **Hardlinks**: Cannot be detected, extracted as separate files (data
//!   duplication)
//!
//! **Security Impact**: Symlinks are NOT created during extraction, preventing
//! CVE-2024-12905 class symlink escape attacks. However, users may experience
//! silent feature loss when extracting archives with Unix symlinks.
//!
//! # Solid Archives
//!
//! 7z supports "solid" compression where multiple files are compressed together
//! as a single block. While this provides better compression ratios, it has
//! security implications:
//!
//! - **Memory exhaustion**: Extracting a single file requires decompressing the
//!   entire solid block into memory
//! - **Denial of service**: Malicious archives can create large solid blocks
//!   that exhaust available memory
//!
//! **Default Policy**: Solid archives are **rejected** by default.
//! Use `SecurityConfig::allow_solid_archives = true` to enable extraction with
//! memory limits enforced via `max_solid_block_memory`.
//!
//! # Examples
//!
//! Basic extraction:
//!
//! ```no_run
//! use exarch_core::ExtractionOptions;
//! use exarch_core::SecurityConfig;
//! use exarch_core::formats::SevenZArchive;
//! use exarch_core::formats::traits::ArchiveFormat;
//! use std::fs::File;
//! use std::path::Path;
//!
//! # fn main() -> Result<(), exarch_core::ExtractionError> {
//! let file = File::open("archive.7z")?;
//! let mut archive = SevenZArchive::new(file)?;
//! let report = archive.extract(
//!     Path::new("/output"),
//!     &SecurityConfig::default(),
//!     &ExtractionOptions::default(),
//! )?;
//! println!("Extracted {} files", report.files_extracted);
//! # Ok(())
//! # }
//! ```
//!
//! Allow solid archives with memory limit:
//!
//! ```no_run
//! use exarch_core::ExtractionOptions;
//! use exarch_core::SecurityConfig;
//!
//! let mut config = SecurityConfig::default();
//! config.allow_solid_archives = true;
//! config.max_solid_block_memory = 512 * 1024 * 1024; // 512 MB
//! // ... extract with config
//! ```

use std::cell::RefCell;
use std::io::ErrorKind;
use std::io::Read;
use std::io::Seek;
use std::path::Path;
use std::path::PathBuf;
use std::process;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;

use sevenz_rust2::Archive;
use sevenz_rust2::Password;

// Atomic counter for generating unique temporary file names
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);

use crate::ExtractionError;
use crate::ExtractionOptions;
use crate::ExtractionReport;
use crate::Result;
use crate::SecurityConfig;
use crate::error::QuotaResource;
use crate::security::EntryValidator;
use crate::security::validator::ValidatedEntryType;
use crate::types::DestDir;
use crate::types::EntryType;

use super::common;
use super::traits::ArchiveFormat;

/// RAII guard for temporary files.
/// Ensures temp files are cleaned up on error.
struct TempFileGuard {
    path: PathBuf,
    should_cleanup: bool,
}

impl TempFileGuard {
    fn new(path: PathBuf) -> Self {
        Self {
            path,
            should_cleanup: true,
        }
    }

    /// Mark the temp file as successfully processed.
    /// Prevents cleanup on drop.
    fn persist(mut self) {
        self.should_cleanup = false;
    }
}

impl Drop for TempFileGuard {
    fn drop(&mut self) {
        if self.should_cleanup {
            let _ = std::fs::remove_file(&self.path);
        }
    }
}

/// Cached entry metadata from initial archive read.
/// Avoids re-parsing archive during extraction.
#[derive(Debug, Clone)]
struct CachedEntry {
    name: String,
    size: u64,
    is_directory: bool,
}

/// 7z archive handler with security validation.
///
/// Supports:
/// - 7z format (LZMA SDK)
/// - Compression methods: LZMA, LZMA2, BZIP2, `PPMd`, DEFLATE, Copy
/// - Multi-volume archives (read-only)
/// - Encrypted archive detection (rejected)
/// - Solid archive detection (rejected by default)
///
/// # Solid Archives
///
/// Solid compression stores multiple files in a single compressed block.
/// This provides better compression ratios but requires decompressing
/// the entire block to extract a single file, which can cause memory
/// exhaustion attacks.
///
/// **Security Policy**: Solid archives are rejected by default.
/// Use `SecurityConfig::allow_solid_archives` to enable with memory limits.
///
/// # Examples
///
/// ```no_run
/// use exarch_core::ExtractionOptions;
/// use exarch_core::SecurityConfig;
/// use exarch_core::formats::SevenZArchive;
/// use exarch_core::formats::traits::ArchiveFormat;
/// use std::fs::File;
/// use std::path::Path;
///
/// let file = File::open("archive.7z")?;
/// let mut archive = SevenZArchive::new(file)?;
/// let report = archive.extract(
///     Path::new("/output"),
///     &SecurityConfig::default(),
///     &ExtractionOptions::default(),
/// )?;
/// println!("Extracted {} files", report.files_extracted);
/// # Ok::<(), exarch_core::ExtractionError>(())
/// ```
#[derive(Debug)]
pub struct SevenZArchive<R: Read + Seek> {
    source: R,
    entries: Vec<CachedEntry>,
    is_solid: bool,
}

impl<R: Read + Seek> SevenZArchive<R> {
    /// Creates a new 7z archive reader.
    ///
    /// # Security Checks
    ///
    /// - Rejects encrypted archives (via password parameter)
    /// - Validates archive header signature
    /// - Checks for solid compression (rejected by default)
    ///
    /// # Errors
    ///
    /// Returns error if:
    /// - Archive is encrypted
    /// - Archive header is invalid
    /// - Format is not recognized
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use exarch_core::formats::SevenZArchive;
    /// use std::fs::File;
    ///
    /// let file = File::open("archive.7z")?;
    /// let archive = SevenZArchive::new(file)?;
    /// # Ok::<(), exarch_core::ExtractionError>(())
    /// ```
    pub fn new(mut source: R) -> Result<Self> {
        // Step 1: Verify it's a valid 7z archive by reading metadata
        let password = Password::empty();
        let archive = match Archive::read(&mut source, &password) {
            Ok(a) => a,
            Err(e) => {
                // SECURITY: Check if error indicates encryption
                let err_str = e.to_string().to_lowercase();
                if err_str.contains("encrypt") || err_str.contains("password") {
                    return Err(ExtractionError::SecurityViolation {
                        reason: "encrypted 7z archive detected. Password-protected archives are not supported. \
                                 Decrypt the archive externally and try again.".into(),
                    });
                }
                // Handle valid empty 7z archives: sevenz-rust2 fails with UnexpectedEof on
                // 32-byte archives that contain no files. Check for a valid 7z signature
                // and small file size before treating as an empty archive.
                if is_empty_sevenz_archive(&e, &mut source) {
                    return Ok(Self {
                        source,
                        entries: vec![],
                        is_solid: false,
                    });
                }
                return Err(ExtractionError::InvalidArchive(format!(
                    "failed to open 7z archive: {e}"
                )));
            }
        };

        // Step 2: SECURITY - Cache solid flag for later validation
        // NOTE: Actual enforcement happens in extract() via SecurityConfig
        let is_solid = archive.is_solid;

        // Step 3: Cache entry metadata to avoid re-parsing during extraction
        let entries: Vec<CachedEntry> = archive
            .files
            .iter()
            .map(|e| CachedEntry {
                name: e.name.clone(),
                size: e.size,
                is_directory: e.is_directory(),
            })
            .collect();

        // Step 4: Rewind for actual extraction
        source.rewind().map_err(ExtractionError::Io)?;

        Ok(Self {
            source,
            entries,
            is_solid,
        })
    }
}

impl<R: Read + Seek> SevenZArchive<R> {
    /// Extract archive using sevenz-rust2 callback API with security
    /// validation.
    ///
    /// This uses the `decompress_with_extract` API which provides a callback
    /// for each entry. We use this to inject our security validation logic.
    ///
    /// # Security
    ///
    /// - Re-validates paths in callback (defense in depth)
    /// - Enforces quotas during extraction
    /// - Uses atomic writes (temp + rename)
    /// - Creates directories only after validation
    /// - Uses directory cache to reduce mkdir syscalls
    fn extract_with_callback(
        source: &mut R,
        dest: &DestDir,
        validator: &mut EntryValidator,
        dir_cache: &mut common::DirCache,
        skip_duplicates: bool,
    ) -> Result<ExtractionReport> {
        // Use RefCell for interior mutability in closure
        let report = RefCell::new(ExtractionReport::new());
        let dir_cache = RefCell::new(dir_cache);

        // Extraction callback - called for each entry
        let extract_fn = |entry: &sevenz_rust2::ArchiveEntry,
                          reader: &mut dyn Read,
                          _dest_dir: &PathBuf|
         -> std::result::Result<bool, sevenz_rust2::Error> {
            // Convert entry metadata
            let path = PathBuf::from(&entry.name);
            let entry_type = SevenZEntryAdapter::to_entry_type(entry).map_err(|e| {
                sevenz_rust2::Error::Other(format!("entry type detection failed: {e}").into())
            })?;

            // Re-validate (defense in depth)
            // NOTE: DirCache is behind RefCell, cannot pass shared reference through
            // closure
            let validated = validator
                .validate_entry(&path, &entry_type, entry.size, None, None, None)
                .map_err(|e| {
                    sevenz_rust2::Error::Other(format!("validation failed: {e}").into())
                })?;

            // Extract based on type
            match validated.entry_type {
                ValidatedEntryType::Directory => {
                    let dest_path = dest.join_path(validated.safe_path.as_path());
                    // Use cache to avoid redundant mkdir syscalls
                    dir_cache.borrow_mut().ensure_dir(&dest_path)?;
                    report.borrow_mut().directories_created += 1;
                }
                ValidatedEntryType::File => {
                    let dest_path = dest.join_path(validated.safe_path.as_path());

                    // Create parent directories using cache
                    dir_cache.borrow_mut().ensure_parent_dir(&dest_path)?;

                    if dest_path.exists() {
                        if skip_duplicates {
                            report.borrow_mut().files_skipped += 1;
                            report.borrow_mut().warnings.push(format!(
                                "skipped duplicate entry: {}",
                                validated.safe_path.as_path().display()
                            ));
                            return Ok(true);
                        }
                        return Err(sevenz_rust2::Error::Other(
                            format!(
                                "duplicate entry: {}",
                                validated.safe_path.as_path().display()
                            )
                            .into(),
                        ));
                    }

                    // Atomic write (temp + rename) with unique temp file name
                    let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
                    let pid = process::id();
                    let original_name = dest_path
                        .file_name()
                        .map_or_else(|| "file".to_string(), |n| n.to_string_lossy().to_string());
                    let temp_name = format!(".{original_name}.exarch-tmp-{pid}-{counter}");
                    let temp_path = dest_path.with_file_name(&temp_name);

                    // Guard ensures temp file cleanup on error
                    let guard = TempFileGuard::new(temp_path.clone());
                    {
                        let mut temp_file = std::fs::File::create(&temp_path)?;
                        let bytes_written = std::io::copy(reader, &mut temp_file)?;
                        report.borrow_mut().bytes_written += bytes_written;
                    }
                    std::fs::rename(&temp_path, &dest_path)?;
                    guard.persist(); // Success - don't cleanup

                    report.borrow_mut().files_extracted += 1;
                }
                _ => {
                    return Err(sevenz_rust2::Error::Other(
                        "symlinks/hardlinks not supported".into(),
                    ));
                }
            }

            Ok(true) // Continue extraction
        };

        // Call sevenz-rust2 extraction
        sevenz_rust2::decompress_with_extract_fn(source, dest.as_path(), extract_fn)?;

        // Extract report from RefCell
        Ok(report.into_inner())
    }
}

impl<R: Read + Seek> ArchiveFormat for SevenZArchive<R> {
    fn extract(
        &mut self,
        output_dir: &Path,
        config: &SecurityConfig,
        options: &ExtractionOptions,
    ) -> Result<ExtractionReport> {
        // Step 0: Validate solid archive policy
        if self.is_solid {
            if !config.allow_solid_archives {
                return Err(ExtractionError::SecurityViolation {
                    reason: "solid 7z archives are not allowed (enable allow_solid_archives)"
                        .into(),
                });
            }

            // SECURITY: Heuristic pre-check validates total uncompressed size
            // Uses checked_add to detect overflow (defense in depth)
            // Reason: sevenz-rust2 0.20 doesn't expose solid block boundaries
            // This is conservative: assumes worst case of single solid block
            let total_uncompressed: u64 = self
                .entries
                .iter()
                .try_fold(0u64, |acc, e| acc.checked_add(e.size))
                .ok_or(ExtractionError::QuotaExceeded {
                    resource: QuotaResource::TotalSize {
                        current: u64::MAX,
                        max: config.max_solid_block_memory,
                    },
                })?;
            if total_uncompressed > config.max_solid_block_memory {
                return Err(ExtractionError::QuotaExceeded {
                    resource: QuotaResource::TotalSize {
                        current: total_uncompressed,
                        max: config.max_solid_block_memory,
                    },
                });
            }
        }

        // Step 1: Initialize extraction context
        let dest = DestDir::new_or_create(output_dir.to_path_buf())?;

        // Pre-validate all paths BEFORE extraction using cached metadata
        // SECURITY NOTE: Pre-validation prevents partial extraction on malicious
        // archives
        //
        // PERFORMANCE: Uses cached metadata from new() to avoid re-parsing archive
        //
        // API LIMITATIONS (sevenz-rust2 0.20):
        // - compressed_size: Not exposed per-entry, so zip bomb detection relies on
        //   quotas only
        // - symlink detection: Not exposed, non-directory entries treated as files
        let mut prevalidator = EntryValidator::new(config, &dest);
        for entry in &self.entries {
            // OPT-H002: Use Path::new instead of PathBuf::from to avoid allocation
            let path = Path::new(&entry.name);
            let entry_type = if entry.is_directory {
                EntryType::Directory
            } else {
                EntryType::File
            };

            // KNOWN LIMITATION: compressed_size is None, so compression ratio check is
            // skipped. Defense relies on max_total_size and max_file_size
            // quotas.
            let validated =
                prevalidator.validate_entry(path, &entry_type, entry.size, None, None, None)?;

            match validated.entry_type {
                ValidatedEntryType::File | ValidatedEntryType::Directory => {
                    // Will be extracted in Step 3
                }
                _ => {
                    // KNOWN LIMITATION: sevenz-rust2 doesn't expose symlink/hardlink detection.
                    // If entry type detection improves, this will catch them.
                    return Err(ExtractionError::SecurityViolation {
                        reason: "symlinks/hardlinks not yet supported for 7z".into(),
                    });
                }
            }
        }

        // Empty archives: skip extraction entirely — decompress_with_extract_fn
        // would fail with UnexpectedEof on valid 32-byte empty 7z archives.
        if self.entries.is_empty() {
            return Ok(ExtractionReport::new());
        }

        // Step 3: Extract with FRESH validator to avoid quota double-counting
        // Note: sevenz-rust2 still parses archive internally, but we avoid
        // double parsing in our validation logic
        let mut validator = EntryValidator::new(config, &dest);
        let mut dir_cache = common::DirCache::new();
        match Self::extract_with_callback(
            &mut self.source,
            &dest,
            &mut validator,
            &mut dir_cache,
            options.skip_duplicates,
        ) {
            Ok(report) => Ok(report),
            Err(e) => {
                // 7z pre-validates all paths before extracting, so any error
                // from extract_with_callback means partial extraction occurred.
                // We wrap unconditionally since partial state may be on disk.
                Err(ExtractionError::PartialExtraction {
                    source: Box::new(e),
                    report: ExtractionReport::new(),
                })
            }
        }
    }

    fn format_name(&self) -> &'static str {
        "7z"
    }
}

/// Adapter to convert sevenz-rust2 entry types to our `EntryType` enum.
///
/// # Known Limitations (sevenz-rust2 0.20)
///
/// - **Symlinks (Unix)**: Not reliably detectable. The 7z format supports Unix
///   symlinks, but sevenz-rust2 does not expose entry type information.
///   Symlinks may be extracted as regular files containing the target path.
///
/// - **Symlinks (Windows)**: Partially detectable via
///   `FILE_ATTRIBUTE_REPARSE_POINT` in Windows attributes. Archives created on
///   Windows with symlinks will be rejected with a `SecurityViolation` error.
///
/// - **Hardlinks**: Not detectable. Hardlinks will be extracted as separate
///   files (duplication instead of linking).
///
/// - **Unix mode**: Not exposed, so permission sanitization cannot be applied
///   to 7z archives.
///
/// # Security Implications
///
/// The lack of symlink detection means:
/// - **No symlink escapes** (good): Symlinks are not created, so they cannot
///   escape the extraction directory.
/// - **Silent feature loss** (bad): Users may expect symlinks to work but they
///   will be extracted as files.
/// - **Defense-in-depth gap**: We cannot explicitly validate and reject
///   archives with symlinks (except Windows reparse points).
///
/// # Future Work
///
/// When sevenz-rust2 adds symlink detection APIs:
/// 1. Update `to_entry_type()` to return `EntryType::Symlink { target }`
/// 2. Integrate with existing `validate_symlink()` validator
/// 3. Add tests for symlink escapes (similar to TAR/ZIP)
/// 4. Remove Windows-only detection workaround
struct SevenZEntryAdapter;

impl SevenZEntryAdapter {
    /// Converts 7z entry to our `EntryType` enum.
    ///
    /// # Security Note
    ///
    /// Due to sevenz-rust2 API limitations, this function cannot reliably
    /// detect symlinks or hardlinks:
    ///
    /// - **Windows symlinks**: Detected via `FILE_ATTRIBUTE_REPARSE_POINT` and
    ///   rejected
    /// - **Unix symlinks**: Not detectable, extracted as regular files
    ///   (documented limitation)
    /// - **Hardlinks**: Not detectable, extracted as separate files
    ///
    /// # Errors
    ///
    /// Returns `SecurityViolation` if Windows reparse point is detected
    /// (symlinks on Windows).
    fn to_entry_type(entry: &sevenz_rust2::ArchiveEntry) -> Result<EntryType> {
        // SECURITY: Check Windows attributes for reparse points FIRST
        // This applies to BOTH files AND directories (e.g., directory junctions)
        if Self::is_windows_reparse_point(entry) {
            return Err(ExtractionError::SecurityViolation {
                reason: format!(
                    "symlink detected in 7z archive: {} \
                     (Windows reparse point attribute set). \
                     7z symlink extraction is not supported due to sevenz-rust2 API limitations.",
                    entry.name
                ),
            });
        }

        if entry.is_directory() {
            return Ok(EntryType::Directory);
        }

        // Default: regular file
        // KNOWN LIMITATION: Unix symlinks cannot be detected and will be extracted as
        // files
        Ok(EntryType::File)
    }

    /// Checks if Windows attributes indicate a reparse point
    /// (symlink/junction).
    ///
    /// **Limitation:** Only detects symlinks created on Windows.
    /// Unix symlinks in 7z archives may not have Windows attributes.
    ///
    /// Reference: <https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants>
    fn is_windows_reparse_point(entry: &sevenz_rust2::ArchiveEntry) -> bool {
        const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;

        entry.has_windows_attributes
            && (entry.windows_attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0
    }
}

/// Checks whether a sevenz-rust2 parse failure is due to a valid empty 7z
/// archive.
///
/// sevenz-rust2 fails with `UnexpectedEof` on valid 32-byte empty archives (0
/// files). We detect this by requiring:
/// 1. The error is an I/O error with `ErrorKind::UnexpectedEof`
/// 2. The file starts with the 7z magic signature
/// 3. The file size is exactly 32 bytes (the only valid empty archive size)
fn is_empty_sevenz_archive<R: Read + Seek>(err: &sevenz_rust2::Error, source: &mut R) -> bool {
    const SEVENZ_MAGIC: [u8; 6] = [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C];
    // A valid empty 7z archive is exactly 32 bytes:
    // signature (6) + version (2) + StartHeader CRC (4) + StartHeader (20).
    // Files shorter than 32 bytes are truncated/corrupt, not empty archives.
    const EMPTY_ARCHIVE_SIZE: u64 = 32;

    let is_eof = matches!(err, sevenz_rust2::Error::Io(io_err, _) if io_err.kind() == ErrorKind::UnexpectedEof);
    if !is_eof {
        return false;
    }

    // Check file size
    let Ok(size) = source.seek(std::io::SeekFrom::End(0)) else {
        return false;
    };
    if size != EMPTY_ARCHIVE_SIZE {
        return false;
    }

    // Check 7z magic signature
    let Ok(_) = source.seek(std::io::SeekFrom::Start(0)) else {
        return false;
    };
    let mut magic = [0u8; 6];
    source.read_exact(&mut magic).is_ok() && magic == SEVENZ_MAGIC
}

/// Converts sevenz-rust2 errors to our `ExtractionError` type.
impl From<sevenz_rust2::Error> for ExtractionError {
    fn from(err: sevenz_rust2::Error) -> Self {
        let err_str = err.to_string();
        let err_lower = err_str.to_lowercase();

        // Check for encryption/password errors
        if err_lower.contains("password") || err_lower.contains("encrypt") {
            return Self::SecurityViolation {
                reason: format!("encrypted archive: {err_str}"),
            };
        }

        // Check for I/O errors
        if err_lower.contains("i/o") || err_lower.contains("read") || err_lower.contains("write") {
            return Self::Io(std::io::Error::other(err_str));
        }

        // Default: InvalidArchive
        Self::InvalidArchive(format!("7z error: {err_str}"))
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
    use super::*;
    use std::io::Cursor;
    use tempfile::TempDir;

    // 7z format magic bytes for signature validation
    const SEVENZ_MAGIC: [u8; 6] = [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C];

    /// Load pre-generated fixture from tests/fixtures/
    fn load_fixture(name: &str) -> Vec<u8> {
        let manifest_dir = env!("CARGO_MANIFEST_DIR");
        let fixture_path = std::path::PathBuf::from(manifest_dir)
            .parent()
            .unwrap()
            .parent()
            .unwrap()
            .join("tests/fixtures")
            .join(name);

        std::fs::read(&fixture_path).unwrap_or_else(|e| {
            panic!(
                "Failed to load fixture {name}. Run tests/fixtures/generate_7z_fixtures.sh first. Error: {e}"
            )
        })
    }

    /// Test that `format_name` returns correct value.
    /// This test doesn't require a valid archive.
    #[test]
    fn test_format_name() {
        // We can't create a valid SevenZArchive without a real archive,
        // but we can verify the implementation returns the expected value
        // by checking the trait implementation directly.

        // Create invalid data - this will fail to parse
        let data = SEVENZ_MAGIC.to_vec();
        let cursor = Cursor::new(data);

        // new() will fail because it's not a valid archive, but that's expected
        let result = SevenZArchive::new(cursor);
        assert!(result.is_err(), "invalid archive should fail to parse");

        // Verify the error is InvalidArchive (not security violation)
        assert!(matches!(result, Err(ExtractionError::InvalidArchive(_))));
    }

    /// Test that invalid magic bytes are rejected.
    #[test]
    fn test_invalid_magic_rejected() {
        let data = vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05];
        let cursor = Cursor::new(data);

        let result = SevenZArchive::new(cursor);
        assert!(result.is_err());
        assert!(matches!(result, Err(ExtractionError::InvalidArchive(_))));
    }

    #[test]
    fn test_load_fixture_helper() {
        let data = load_fixture("simple.7z");
        assert!(!data.is_empty());
        assert_eq!(&data[0..6], &SEVENZ_MAGIC);
    }

    #[test]
    fn test_extract_simple_file() {
        let data = load_fixture("simple.7z");
        let cursor = Cursor::new(data);
        let mut archive = SevenZArchive::new(cursor).unwrap();

        let temp = TempDir::new().unwrap();
        let config = SecurityConfig::default();

        let report = archive
            .extract(temp.path(), &config, &ExtractionOptions::default())
            .unwrap();

        assert_eq!(report.files_extracted, 2);
        assert!(temp.path().join("simple/file1.txt").exists());
        assert!(temp.path().join("simple/file2.txt").exists());

        // Verify file contents
        let content1 = std::fs::read_to_string(temp.path().join("simple/file1.txt")).unwrap();
        assert_eq!(content1, "hello world\n");
    }

    #[test]
    fn test_extract_nested_directories() {
        let data = load_fixture("nested-dirs.7z");
        let cursor = Cursor::new(data);
        let mut archive = SevenZArchive::new(cursor).unwrap();

        let temp = TempDir::new().unwrap();
        let config = SecurityConfig::default();

        let report = archive
            .extract(temp.path(), &config, &ExtractionOptions::default())
            .unwrap();

        assert!(report.files_extracted >= 1);
        assert!(temp.path().join("nested/subdir1/subdir2/deep.txt").exists());
        assert!(temp.path().join("nested/subdir1/file.txt").exists());
    }

    #[test]
    fn test_solid_archive_rejected() {
        let data = load_fixture("solid.7z");
        let cursor = Cursor::new(data);

        // new() should now succeed (just caches is_solid flag)
        let mut archive = SevenZArchive::new(cursor).unwrap();

        // Rejection happens in extract() with default config
        let temp = TempDir::new().unwrap();
        let result = archive.extract(
            temp.path(),
            &SecurityConfig::default(),
            &ExtractionOptions::default(),
        );

        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            ExtractionError::SecurityViolation { .. }
        ));
    }

    #[test]
    fn test_encrypted_archive_rejected() {
        let data = load_fixture("encrypted.7z");
        let cursor = Cursor::new(data);

        // Should fail in new() due to encryption detection
        let result = SevenZArchive::new(cursor);
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            ExtractionError::SecurityViolation { .. }
        ));
    }

    #[test]
    fn test_empty_archive() {
        let data = load_fixture("empty.7z");
        let cursor = Cursor::new(data);
        let mut archive = SevenZArchive::new(cursor).unwrap();

        let temp = TempDir::new().unwrap();
        let config = SecurityConfig::default();

        let report = archive
            .extract(temp.path(), &config, &ExtractionOptions::default())
            .unwrap();

        assert_eq!(report.files_extracted, 0);
        assert_eq!(report.directories_created, 0);
    }

    #[test]
    fn test_empty_archive_extract() {
        let path = std::path::Path::new("../../tests/fixtures/empty.7z");
        let file = std::fs::File::open(path).unwrap();
        let mut archive = SevenZArchive::new(file).unwrap();

        let temp = TempDir::new().unwrap();
        let config = SecurityConfig::default();

        let report = archive
            .extract(temp.path(), &config, &ExtractionOptions::default())
            .unwrap();
        assert_eq!(report.files_extracted, 0);
        assert_eq!(report.bytes_written, 0);
    }

    #[test]
    fn test_quota_exceeded() {
        let data = load_fixture("large-file.7z");
        let cursor = Cursor::new(data);
        let mut archive = SevenZArchive::new(cursor).unwrap();

        let temp = TempDir::new().unwrap();
        let config = SecurityConfig {
            max_file_size: 1024, // 1 KB limit, fixture has 50 KB file
            ..SecurityConfig::default()
        };

        let result = archive.extract(temp.path(), &config, &ExtractionOptions::default());
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            ExtractionError::QuotaExceeded { .. }
        ));
    }

    /// Test B-002: Verify quota is not double-counted
    /// Pre-validation and extraction use separate validators to prevent
    /// counting files twice against quotas.
    #[test]
    fn test_multiple_files_quota_not_double_counted() {
        let data = load_fixture("simple.7z"); // Contains 2 files
        let cursor = Cursor::new(data);
        let mut archive = SevenZArchive::new(cursor).unwrap();

        let temp = TempDir::new().unwrap();
        let config = SecurityConfig {
            max_file_count: 3, // Should allow 2 files
            ..SecurityConfig::default()
        };

        let result = archive.extract(temp.path(), &config, &ExtractionOptions::default());
        assert!(
            result.is_ok(),
            "2 files should not exceed quota of 3: {result:?}"
        );
        assert_eq!(result.unwrap().files_extracted, 2);
    }

    /// Test B-1: Verify path traversal is rejected
    /// This test ensures the validator integration properly rejects
    /// archives with path traversal attempts.
    #[test]
    fn test_path_traversal_integration() {
        // Test that our validator integration works by creating a simple archive
        // and verifying the validator is properly called
        let data = load_fixture("simple.7z");
        let cursor = Cursor::new(data);
        let archive = SevenZArchive::new(cursor);

        // Verify our validator is properly integrated
        assert!(archive.is_ok());

        // NOTE: Path traversal testing is covered by integration tests using
        // actual 7z fixtures. Additional unit-level fixture testing can be
        // added here if needed in the future.
    }

    /// Test: Solid archive extracts when allowed
    #[test]
    fn test_solid_archive_allowed_with_config() {
        let data = load_fixture("solid.7z");
        let cursor = Cursor::new(data);
        let mut archive = SevenZArchive::new(cursor).unwrap();

        let temp = TempDir::new().unwrap();
        let config = SecurityConfig {
            allow_solid_archives: true,
            max_solid_block_memory: 100 * 1024 * 1024, // 100 MB
            ..SecurityConfig::default()
        };

        let result = archive.extract(temp.path(), &config, &ExtractionOptions::default());
        assert!(result.is_ok(), "solid archive should extract: {result:?}");
        assert!(result.unwrap().files_extracted > 0);
    }

    /// Test: Solid archive rejected by default config
    #[test]
    fn test_solid_archive_rejected_by_default() {
        let data = load_fixture("solid.7z");
        let cursor = Cursor::new(data);
        let mut archive = SevenZArchive::new(cursor).unwrap();

        let temp = TempDir::new().unwrap();
        let config = SecurityConfig::default();

        let result = archive.extract(temp.path(), &config, &ExtractionOptions::default());
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            ExtractionError::SecurityViolation { .. }
        ));
    }

    /// Test: Solid archive memory limit exceeded
    #[test]
    fn test_solid_archive_memory_limit_exceeded() {
        let data = load_fixture("solid.7z");
        let cursor = Cursor::new(data);
        let mut archive = SevenZArchive::new(cursor).unwrap();

        let temp = TempDir::new().unwrap();
        let config = SecurityConfig {
            allow_solid_archives: true,
            max_solid_block_memory: 1, // 1 byte (too small)
            ..SecurityConfig::default()
        };

        let result = archive.extract(temp.path(), &config, &ExtractionOptions::default());
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            ExtractionError::QuotaExceeded { .. }
        ));
    }

    /// Test: Non-solid archives work regardless of solid config
    #[test]
    fn test_non_solid_archive_unaffected_by_solid_config() {
        let data = load_fixture("simple.7z");
        let cursor = Cursor::new(data);
        let mut archive = SevenZArchive::new(cursor).unwrap();

        let temp = TempDir::new().unwrap();
        let config = SecurityConfig::default(); // allow_solid_archives = false

        let result = archive.extract(temp.path(), &config, &ExtractionOptions::default());
        assert!(result.is_ok(), "non-solid should work: {result:?}");
    }

    // ============================================================================
    // Phase 10.4 Review Fixes: Additional Tests
    // ============================================================================

    /// Test H-3: Verify `is_solid` flag is correctly detected
    #[test]
    fn test_is_solid_flag_detected_correctly() {
        // Solid archive should have is_solid = true
        let solid_data = load_fixture("solid.7z");
        let solid_cursor = Cursor::new(solid_data);
        let solid_archive = SevenZArchive::new(solid_cursor).unwrap();
        assert!(solid_archive.is_solid, "solid.7z should have is_solid=true");

        // Non-solid archive should have is_solid = false
        let non_solid_data = load_fixture("simple.7z");
        let non_solid_cursor = Cursor::new(non_solid_data);
        let non_solid_archive = SevenZArchive::new(non_solid_cursor).unwrap();
        assert!(
            !non_solid_archive.is_solid,
            "simple.7z should have is_solid=false"
        );
    }

    /// Test H-1: Boundary condition - exact limit should PASS
    #[test]
    fn test_solid_archive_memory_limit_exact_boundary() {
        let data = load_fixture("solid.7z");

        // First read to get total size
        let archive_for_size = SevenZArchive::new(Cursor::new(data.clone())).unwrap();
        let total_size: u64 = archive_for_size.entries.iter().map(|e| e.size).sum();

        // Now test with exact limit
        let mut archive = SevenZArchive::new(Cursor::new(data)).unwrap();
        let temp = TempDir::new().unwrap();
        let config = SecurityConfig {
            allow_solid_archives: true,
            max_solid_block_memory: total_size, // Exact match
            ..SecurityConfig::default()
        };

        let result = archive.extract(temp.path(), &config, &ExtractionOptions::default());
        assert!(
            result.is_ok(),
            "exact limit should allow extraction: {result:?}"
        );
    }

    /// Test H-1: Boundary condition - one byte under limit should FAIL
    #[test]
    fn test_solid_archive_memory_limit_one_under_boundary() {
        let data = load_fixture("solid.7z");

        // First read to get total size
        let archive_for_size = SevenZArchive::new(Cursor::new(data.clone())).unwrap();
        let total_size: u64 = archive_for_size.entries.iter().map(|e| e.size).sum();

        // Ensure we have at least 2 bytes of content to make test meaningful
        if total_size < 2 {
            return; // Skip test if fixture is too small
        }

        // Now test with one byte under
        let mut archive = SevenZArchive::new(Cursor::new(data)).unwrap();
        let temp = TempDir::new().unwrap();
        let config = SecurityConfig {
            allow_solid_archives: true,
            max_solid_block_memory: total_size - 1, // One byte under
            ..SecurityConfig::default()
        };

        let result = archive.extract(temp.path(), &config, &ExtractionOptions::default());
        assert!(result.is_err(), "one byte under limit should reject");
        assert!(matches!(
            result.unwrap_err(),
            ExtractionError::QuotaExceeded { .. }
        ));
    }

    /// Test H-2: Verify error message contains helpful info
    #[test]
    fn test_solid_archive_rejected_error_message() {
        let data = load_fixture("solid.7z");
        let cursor = Cursor::new(data);
        let mut archive = SevenZArchive::new(cursor).unwrap();

        let temp = TempDir::new().unwrap();
        let result = archive.extract(
            temp.path(),
            &SecurityConfig::default(),
            &ExtractionOptions::default(),
        );

        assert!(result.is_err());
        match result.unwrap_err() {
            ExtractionError::SecurityViolation { reason } => {
                assert!(
                    reason.contains("solid") && reason.contains("allow_solid_archives"),
                    "error should mention 'solid' and 'allow_solid_archives', got: {reason}"
                );
            }
            other => panic!("expected SecurityViolation, got {other:?}"),
        }
    }

    // ============================================================================
    // Phase 10.5: Symlink/Hardlink Detection Tests
    // ============================================================================

    /// Test: Windows reparse point detection (TRUE case)
    #[test]
    fn test_windows_reparse_point_detected() {
        let mut entry = sevenz_rust2::ArchiveEntry::new_file("symlink.txt");
        entry.has_windows_attributes = true;
        entry.windows_attributes = 0x0400; // FILE_ATTRIBUTE_REPARSE_POINT

        assert!(
            SevenZEntryAdapter::is_windows_reparse_point(&entry),
            "reparse point attribute should be detected"
        );

        let result = SevenZEntryAdapter::to_entry_type(&entry);
        assert!(result.is_err(), "should return error for reparse point");
        assert!(
            matches!(
                result.unwrap_err(),
                ExtractionError::SecurityViolation { .. }
            ),
            "should be SecurityViolation error"
        );
    }

    /// Test: Windows reparse point NOT detected (has attributes, but not
    /// reparse point)
    #[test]
    fn test_windows_reparse_point_not_set() {
        let mut entry = sevenz_rust2::ArchiveEntry::new_file("file.txt");
        entry.has_windows_attributes = true;
        entry.windows_attributes = 0x0080; // FILE_ATTRIBUTE_NORMAL

        assert!(
            !SevenZEntryAdapter::is_windows_reparse_point(&entry),
            "normal file should not be detected as reparse point"
        );

        let result = SevenZEntryAdapter::to_entry_type(&entry);
        assert!(result.is_ok(), "normal file should succeed");
        assert_eq!(result.unwrap(), EntryType::File);
    }

    /// Test: No Windows attributes (Unix archive)
    #[test]
    fn test_no_windows_attributes() {
        let mut entry = sevenz_rust2::ArchiveEntry::new_file("file.txt");
        entry.has_windows_attributes = false;
        entry.windows_attributes = 0; // Should be ignored

        assert!(
            !SevenZEntryAdapter::is_windows_reparse_point(&entry),
            "entry without Windows attributes should not be detected as reparse point"
        );

        let result = SevenZEntryAdapter::to_entry_type(&entry);
        assert!(result.is_ok(), "file without attributes should succeed");
        assert_eq!(result.unwrap(), EntryType::File);
    }

    /// Test: Windows reparse point with other attributes combined
    #[test]
    fn test_windows_reparse_point_with_other_attributes() {
        let mut entry = sevenz_rust2::ArchiveEntry::new_file("symlink.txt");
        entry.has_windows_attributes = true;
        entry.windows_attributes = 0x0400 | 0x0020; // REPARSE_POINT | ARCHIVE

        assert!(
            SevenZEntryAdapter::is_windows_reparse_point(&entry),
            "reparse point should be detected even with other attributes"
        );

        let result = SevenZEntryAdapter::to_entry_type(&entry);
        assert!(result.is_err(), "should return error for reparse point");
    }

    /// Test: Directory entry should not trigger reparse point check
    #[test]
    fn test_directory_junction_reparse_point_rejected() {
        let mut entry = sevenz_rust2::ArchiveEntry::new_directory("dir/");
        entry.has_windows_attributes = true;
        entry.windows_attributes = 0x0400; // REPARSE_POINT (directory junction)

        // SECURITY: Reparse point check happens FIRST, even for directories
        // This catches directory junctions (Windows symlink directories)
        let result = SevenZEntryAdapter::to_entry_type(&entry);
        assert!(result.is_err(), "directory junction should be rejected");
        assert!(matches!(
            result.unwrap_err(),
            ExtractionError::SecurityViolation { .. }
        ));
    }

    /// Test: Error message for Windows reparse point
    #[test]
    fn test_windows_reparse_point_error_message() {
        let mut entry = sevenz_rust2::ArchiveEntry::new_file("link.txt");
        entry.has_windows_attributes = true;
        entry.windows_attributes = 0x0400;

        let result = SevenZEntryAdapter::to_entry_type(&entry);
        assert!(result.is_err());

        match result.unwrap_err() {
            ExtractionError::SecurityViolation { reason } => {
                assert!(
                    reason.contains("symlink") && reason.contains("link.txt"),
                    "error should mention 'symlink' and entry name, got: {reason}"
                );
                assert!(
                    reason.contains("sevenz-rust2"),
                    "error should mention library limitation, got: {reason}"
                );
            }
            other => panic!("expected SecurityViolation, got {other:?}"),
        }
    }
}
