//! Verification report types for security checks.

use std::path::PathBuf;

use crate::error::ExtractionError;
use crate::formats::detect::ArchiveType;

/// Result of archive verification.
///
/// Generated by `verify_archive()`, contains security and integrity checks
/// performed without extracting files to disk.
///
/// # 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(())
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct VerificationReport {
    /// Overall verification status
    pub status: VerificationStatus,

    /// Integrity check result
    pub integrity_status: CheckStatus,

    /// Security check result
    pub security_status: CheckStatus,

    /// List of all issues found (sorted by severity)
    pub issues: Vec<VerificationIssue>,

    /// Total entries scanned
    pub total_entries: usize,

    /// Entries flagged as suspicious
    pub suspicious_entries: usize,

    /// Total uncompressed size
    pub total_size: u64,

    /// Archive format
    pub format: ArchiveType,
}

impl VerificationReport {
    /// Returns true if the archive is safe (no critical or high severity
    /// issues).
    #[must_use]
    pub fn is_safe(&self) -> bool {
        self.status == VerificationStatus::Pass
    }

    /// Returns true if there are any critical severity issues.
    #[must_use]
    pub fn has_critical_issues(&self) -> bool {
        self.issues
            .iter()
            .any(|i| i.severity == IssueSeverity::Critical)
    }

    /// Returns issues of a specific severity level.
    #[must_use]
    pub fn issues_by_severity(&self, severity: IssueSeverity) -> Vec<&VerificationIssue> {
        self.issues
            .iter()
            .filter(|i| i.severity == severity)
            .collect()
    }
}

/// Overall verification status.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerificationStatus {
    /// All checks passed
    Pass,

    /// One or more checks failed
    Fail,

    /// Checks completed with warnings
    Warning,
}

impl std::fmt::Display for VerificationStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Pass => write!(f, "PASS"),
            Self::Fail => write!(f, "FAIL"),
            Self::Warning => write!(f, "WARNING"),
        }
    }
}

/// Status of a specific check category.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckStatus {
    /// Check passed
    Pass,

    /// Check failed
    Fail,

    /// Check completed with warnings
    Warning,

    /// Check was skipped
    Skipped,
}

impl std::fmt::Display for CheckStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Pass => write!(f, "OK"),
            Self::Fail => write!(f, "FAILED"),
            Self::Warning => write!(f, "WARNING"),
            Self::Skipped => write!(f, "SKIPPED"),
        }
    }
}

/// Single verification issue.
#[derive(Debug, Clone)]
pub struct VerificationIssue {
    /// Issue severity level
    pub severity: IssueSeverity,

    /// Issue category
    pub category: IssueCategory,

    /// Entry path that triggered issue (if applicable)
    pub entry_path: Option<PathBuf>,

    /// Human-readable description
    pub message: String,

    /// Optional context (compression ratio, target path, etc.)
    pub context: Option<String>,
}

impl VerificationIssue {
    /// Creates a verification issue from an extraction error.
    #[must_use]
    #[allow(clippy::too_many_lines)]
    pub fn from_error(error: &ExtractionError, entry_path: Option<PathBuf>) -> Self {
        let (severity, category, message) = match error {
            ExtractionError::PathTraversal { path } => (
                IssueSeverity::Critical,
                IssueCategory::PathTraversal,
                format!("Path traversal detected: {}", path.display()),
            ),
            ExtractionError::SymlinkEscape { path } => (
                IssueSeverity::Critical,
                IssueCategory::SymlinkEscape,
                format!("Symlink escape: {}", path.display()),
            ),
            ExtractionError::HardlinkEscape { path } => (
                IssueSeverity::Critical,
                IssueCategory::HardlinkEscape,
                format!("Hardlink escape: {}", path.display()),
            ),
            ExtractionError::ZipBomb {
                compressed,
                uncompressed,
                ratio,
            } => (
                IssueSeverity::Critical,
                IssueCategory::ZipBomb,
                format!(
                    "Potential zip bomb: {ratio:.1}x compression ratio (compressed={compressed}, uncompressed={uncompressed})"
                ),
            ),
            ExtractionError::QuotaExceeded { resource } => (
                IssueSeverity::High,
                IssueCategory::QuotaExceeded,
                format!("{resource}"),
            ),
            ExtractionError::InvalidPermissions { path, mode } => (
                IssueSeverity::Medium,
                IssueCategory::InvalidPermissions,
                format!(
                    "Invalid permissions: {} (mode: {:#o})",
                    path.display(),
                    mode
                ),
            ),
            ExtractionError::Io(io_err) => (
                IssueSeverity::High,
                IssueCategory::InvalidArchive,
                format!("I/O error: {io_err}"),
            ),
            ExtractionError::UnsupportedFormat => (
                IssueSeverity::High,
                IssueCategory::InvalidArchive,
                "Unsupported archive format".to_string(),
            ),
            ExtractionError::InvalidArchive(msg) => (
                IssueSeverity::High,
                IssueCategory::InvalidArchive,
                format!("Invalid archive: {msg}"),
            ),
            ExtractionError::SecurityViolation { reason } => (
                IssueSeverity::High,
                IssueCategory::SuspiciousPath,
                format!("Security violation: {reason}"),
            ),
            ExtractionError::SourceNotFound { path } => (
                IssueSeverity::High,
                IssueCategory::InvalidArchive,
                format!("Source not found: {}", path.display()),
            ),
            ExtractionError::SourceNotAccessible { path } => (
                IssueSeverity::High,
                IssueCategory::InvalidArchive,
                format!("Source not accessible: {}", path.display()),
            ),
            ExtractionError::OutputExists { path } => (
                IssueSeverity::Medium,
                IssueCategory::InvalidArchive,
                format!("Output already exists: {}", path.display()),
            ),
            ExtractionError::InvalidCompressionLevel { level } => (
                IssueSeverity::Medium,
                IssueCategory::InvalidArchive,
                format!("Invalid compression level: {level}"),
            ),
            ExtractionError::UnknownFormat { path } => (
                IssueSeverity::High,
                IssueCategory::InvalidArchive,
                format!("Unknown format: {}", path.display()),
            ),
            ExtractionError::InvalidConfiguration { reason } => (
                IssueSeverity::High,
                IssueCategory::InvalidArchive,
                format!("Invalid configuration: {reason}"),
            ),
            ExtractionError::PartialExtraction { source, .. } => {
                return Self::from_error(source, entry_path);
            }
        };

        Self {
            severity,
            category,
            entry_path,
            message,
            context: None,
        }
    }
}

/// Issue severity levels (ordered from least to most severe for Ord).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum IssueSeverity {
    /// Informational
    Info,

    /// Policy violation
    Low,

    /// Security concern
    Medium,

    /// Exploitable security issue
    High,

    /// CVE-level vulnerability
    Critical,
}

impl std::fmt::Display for IssueSeverity {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Critical => write!(f, "CRITICAL"),
            Self::High => write!(f, "HIGH"),
            Self::Medium => write!(f, "MEDIUM"),
            Self::Low => write!(f, "LOW"),
            Self::Info => write!(f, "INFO"),
        }
    }
}

/// Issue categories (maps to security checks).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IssueCategory {
    /// Path traversal attack
    PathTraversal,

    /// Symlink escape attack
    SymlinkEscape,

    /// Hardlink escape attack
    HardlinkEscape,

    /// Zip bomb (excessive compression)
    ZipBomb,

    /// Invalid or unsafe permissions
    InvalidPermissions,

    /// Quota exceeded (file count or size)
    QuotaExceeded,

    /// Invalid or corrupted archive
    InvalidArchive,

    /// Suspicious path or filename
    SuspiciousPath,

    /// Executable file detected
    ExecutableFile,
}

impl std::fmt::Display for IssueCategory {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::PathTraversal => write!(f, "Path Traversal"),
            Self::SymlinkEscape => write!(f, "Symlink Escape"),
            Self::HardlinkEscape => write!(f, "Hardlink Escape"),
            Self::ZipBomb => write!(f, "Zip Bomb"),
            Self::InvalidPermissions => write!(f, "Invalid Permissions"),
            Self::QuotaExceeded => write!(f, "Quota Exceeded"),
            Self::InvalidArchive => write!(f, "Invalid Archive"),
            Self::SuspiciousPath => write!(f, "Suspicious Path"),
            Self::ExecutableFile => write!(f, "Executable File"),
        }
    }
}

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

    #[test]
    fn test_verification_status_display() {
        assert_eq!(VerificationStatus::Pass.to_string(), "PASS");
        assert_eq!(VerificationStatus::Fail.to_string(), "FAIL");
        assert_eq!(VerificationStatus::Warning.to_string(), "WARNING");
    }

    #[test]
    fn test_check_status_display() {
        assert_eq!(CheckStatus::Pass.to_string(), "OK");
        assert_eq!(CheckStatus::Fail.to_string(), "FAILED");
        assert_eq!(CheckStatus::Warning.to_string(), "WARNING");
        assert_eq!(CheckStatus::Skipped.to_string(), "SKIPPED");
    }

    #[test]
    fn test_issue_severity_display() {
        assert_eq!(IssueSeverity::Critical.to_string(), "CRITICAL");
        assert_eq!(IssueSeverity::High.to_string(), "HIGH");
        assert_eq!(IssueSeverity::Medium.to_string(), "MEDIUM");
        assert_eq!(IssueSeverity::Low.to_string(), "LOW");
        assert_eq!(IssueSeverity::Info.to_string(), "INFO");
    }

    #[test]
    fn test_issue_severity_ordering() {
        assert!(IssueSeverity::Critical > IssueSeverity::High);
        assert!(IssueSeverity::High > IssueSeverity::Medium);
        assert!(IssueSeverity::Medium > IssueSeverity::Low);
        assert!(IssueSeverity::Low > IssueSeverity::Info);
    }

    #[test]
    fn test_issue_category_display() {
        assert_eq!(IssueCategory::PathTraversal.to_string(), "Path Traversal");
        assert_eq!(IssueCategory::SymlinkEscape.to_string(), "Symlink Escape");
    }

    #[test]
    fn test_verification_issue_from_path_traversal() {
        let error = ExtractionError::PathTraversal {
            path: PathBuf::from("../../etc/passwd"),
        };
        let issue = VerificationIssue::from_error(&error, None);

        assert_eq!(issue.severity, IssueSeverity::Critical);
        assert_eq!(issue.category, IssueCategory::PathTraversal);
        assert!(issue.message.contains("Path traversal"));
    }

    #[test]
    fn test_verification_issue_from_symlink_escape() {
        let error = ExtractionError::SymlinkEscape {
            path: PathBuf::from("link"),
        };
        let issue = VerificationIssue::from_error(&error, Some(PathBuf::from("link")));

        assert_eq!(issue.severity, IssueSeverity::Critical);
        assert_eq!(issue.category, IssueCategory::SymlinkEscape);
        assert!(issue.message.contains("Symlink escape"));
    }

    #[test]
    fn test_verification_issue_from_zip_bomb() {
        let error = ExtractionError::ZipBomb {
            compressed: 1000,
            uncompressed: 1_000_000,
            ratio: 1000.0,
        };
        let issue = VerificationIssue::from_error(&error, None);

        assert_eq!(issue.severity, IssueSeverity::Critical);
        assert_eq!(issue.category, IssueCategory::ZipBomb);
        assert!(issue.message.contains("zip bomb"));
    }

    #[test]
    fn test_verification_issue_from_io_error() {
        let error = ExtractionError::Io(io::Error::new(io::ErrorKind::NotFound, "not found"));
        let issue = VerificationIssue::from_error(&error, None);

        assert_eq!(issue.severity, IssueSeverity::High);
        assert_eq!(issue.category, IssueCategory::InvalidArchive);
    }

    #[test]
    fn test_verification_report_is_safe() {
        let report = VerificationReport {
            status: VerificationStatus::Pass,
            integrity_status: CheckStatus::Pass,
            security_status: CheckStatus::Pass,
            issues: Vec::new(),
            total_entries: 10,
            suspicious_entries: 0,
            total_size: 1024,
            format: ArchiveType::TarGz,
        };

        assert!(report.is_safe());
    }

    #[test]
    fn test_verification_report_not_safe() {
        let report = VerificationReport {
            status: VerificationStatus::Fail,
            integrity_status: CheckStatus::Pass,
            security_status: CheckStatus::Fail,
            issues: vec![VerificationIssue {
                severity: IssueSeverity::Critical,
                category: IssueCategory::PathTraversal,
                entry_path: None,
                message: "Test issue".to_string(),
                context: None,
            }],
            total_entries: 10,
            suspicious_entries: 1,
            total_size: 1024,
            format: ArchiveType::TarGz,
        };

        assert!(!report.is_safe());
        assert!(report.has_critical_issues());
    }

    #[test]
    fn test_verification_issue_from_hardlink_escape() {
        let error = ExtractionError::HardlinkEscape {
            path: PathBuf::from("link"),
        };
        let issue = VerificationIssue::from_error(&error, None);
        assert_eq!(issue.severity, IssueSeverity::Critical);
        assert_eq!(issue.category, IssueCategory::HardlinkEscape);
        assert!(issue.message.contains("Hardlink escape"));
    }

    #[test]
    fn test_verification_issue_from_quota_exceeded() {
        let error = ExtractionError::QuotaExceeded {
            resource: crate::error::QuotaResource::FileCount {
                current: 11,
                max: 10,
            },
        };
        let issue = VerificationIssue::from_error(&error, None);
        assert_eq!(issue.severity, IssueSeverity::High);
        assert_eq!(issue.category, IssueCategory::QuotaExceeded);
    }

    #[test]
    fn test_verification_issue_from_invalid_permissions() {
        let error = ExtractionError::InvalidPermissions {
            path: PathBuf::from("file.txt"),
            mode: 0o777,
        };
        let issue = VerificationIssue::from_error(&error, None);
        assert_eq!(issue.severity, IssueSeverity::Medium);
        assert_eq!(issue.category, IssueCategory::InvalidPermissions);
        assert!(issue.message.contains("Invalid permissions"));
    }

    #[test]
    fn test_verification_issue_from_unsupported_format() {
        let error = ExtractionError::UnsupportedFormat;
        let issue = VerificationIssue::from_error(&error, None);
        assert_eq!(issue.severity, IssueSeverity::High);
        assert_eq!(issue.category, IssueCategory::InvalidArchive);
        assert!(issue.message.contains("Unsupported archive format"));
    }

    #[test]
    fn test_verification_issue_from_invalid_archive() {
        let error = ExtractionError::InvalidArchive("bad header".into());
        let issue = VerificationIssue::from_error(&error, None);
        assert_eq!(issue.severity, IssueSeverity::High);
        assert_eq!(issue.category, IssueCategory::InvalidArchive);
        assert!(issue.message.contains("Invalid archive"));
    }

    #[test]
    fn test_verification_issue_from_security_violation() {
        let error = ExtractionError::SecurityViolation {
            reason: "encrypted".into(),
        };
        let issue = VerificationIssue::from_error(&error, None);
        assert_eq!(issue.severity, IssueSeverity::High);
        assert_eq!(issue.category, IssueCategory::SuspiciousPath);
        assert!(issue.message.contains("Security violation"));
    }

    #[test]
    fn test_verification_issue_from_source_not_found() {
        let error = ExtractionError::SourceNotFound {
            path: PathBuf::from("/missing"),
        };
        let issue = VerificationIssue::from_error(&error, None);
        assert_eq!(issue.severity, IssueSeverity::High);
        assert_eq!(issue.category, IssueCategory::InvalidArchive);
        assert!(issue.message.contains("Source not found"));
    }

    #[test]
    fn test_verification_issue_from_source_not_accessible() {
        let error = ExtractionError::SourceNotAccessible {
            path: PathBuf::from("/restricted"),
        };
        let issue = VerificationIssue::from_error(&error, None);
        assert_eq!(issue.severity, IssueSeverity::High);
        assert_eq!(issue.category, IssueCategory::InvalidArchive);
        assert!(issue.message.contains("Source not accessible"));
    }

    #[test]
    fn test_verification_issue_from_output_exists() {
        let error = ExtractionError::OutputExists {
            path: PathBuf::from("out/"),
        };
        let issue = VerificationIssue::from_error(&error, None);
        assert_eq!(issue.severity, IssueSeverity::Medium);
        assert_eq!(issue.category, IssueCategory::InvalidArchive);
        assert!(issue.message.contains("Output already exists"));
    }

    #[test]
    fn test_verification_issue_from_invalid_compression_level() {
        let error = ExtractionError::InvalidCompressionLevel { level: 0 };
        let issue = VerificationIssue::from_error(&error, None);
        assert_eq!(issue.severity, IssueSeverity::Medium);
        assert_eq!(issue.category, IssueCategory::InvalidArchive);
        assert!(issue.message.contains("Invalid compression level"));
    }

    #[test]
    fn test_verification_issue_from_unknown_format() {
        let error = ExtractionError::UnknownFormat {
            path: PathBuf::from("archive.rar"),
        };
        let issue = VerificationIssue::from_error(&error, None);
        assert_eq!(issue.severity, IssueSeverity::High);
        assert_eq!(issue.category, IssueCategory::InvalidArchive);
        assert!(issue.message.contains("Unknown format"));
    }

    #[test]
    fn test_verification_issue_from_invalid_configuration() {
        let error = ExtractionError::InvalidConfiguration {
            reason: "bad config".into(),
        };
        let issue = VerificationIssue::from_error(&error, None);
        assert_eq!(issue.severity, IssueSeverity::High);
        assert_eq!(issue.category, IssueCategory::InvalidArchive);
        assert!(issue.message.contains("Invalid configuration"));
    }

    #[test]
    fn test_verification_report_issues_by_severity() {
        let report = VerificationReport {
            status: VerificationStatus::Warning,
            integrity_status: CheckStatus::Pass,
            security_status: CheckStatus::Warning,
            issues: vec![
                VerificationIssue {
                    severity: IssueSeverity::Critical,
                    category: IssueCategory::PathTraversal,
                    entry_path: None,
                    message: "Critical issue".to_string(),
                    context: None,
                },
                VerificationIssue {
                    severity: IssueSeverity::Low,
                    category: IssueCategory::ExecutableFile,
                    entry_path: None,
                    message: "Low issue".to_string(),
                    context: None,
                },
            ],
            total_entries: 10,
            suspicious_entries: 2,
            total_size: 1024,
            format: ArchiveType::TarGz,
        };

        let critical_issues = report.issues_by_severity(IssueSeverity::Critical);
        assert_eq!(critical_issues.len(), 1);

        let low_issues = report.issues_by_severity(IssueSeverity::Low);
        assert_eq!(low_issues.len(), 1);
    }
}
