//! `.aphoriaignore` file parser.
//!
//! This module handles loading and parsing `.aphoriaignore` files, which use
//! gitignore-style syntax for excluding paths from scanning.
//!
//! ## Syntax
//!
//! The `.aphoriaignore` file follows gitignore conventions:
//!
//! - Lines starting with `#` are comments
//! - Empty lines are ignored
//! - Trailing whitespace is stripped
//! - Patterns use glob syntax (see `walker/mod.rs` for details)
//!
//! ## Example
//!
//! ```text
//! # .aphoriaignore
//!
//! # Test fixtures - intentionally vulnerable
//! uat/fixtures/
//! tests/fixtures/
//!
//! # Demo apps
//! docs/demo/vulnbank/
//!
//! # Pattern definitions in extractor source
//! applications/aphoria/src/extractors/*.rs
//!
//! # Vendor code
//! vendor/
//! third_party/
//! ```

use std::path::Path;

use crate::AphoriaError;

/// File name for the ignore file.
pub const IGNORE_FILE_NAME: &str = ".aphoriaignore";

/// Patterns loaded from an `.aphoriaignore` file.
#[derive(Debug, Default)]
pub struct IgnorePatterns {
    /// The parsed patterns.
    patterns: Vec<String>,
}

impl IgnorePatterns {
    /// Create an empty set of patterns.
    pub fn empty() -> Self {
        Self { patterns: Vec::new() }
    }

    /// Load patterns from `.aphoriaignore` in the given directory.
    ///
    /// If the file doesn't exist, returns an empty pattern set.
    /// If the file exists but can't be read, returns an error.
    pub fn load(root: &Path) -> Result<Self, AphoriaError> {
        let ignore_path = root.join(IGNORE_FILE_NAME);

        if !ignore_path.exists() {
            return Ok(Self::empty());
        }

        let content = std::fs::read_to_string(&ignore_path)
            .map_err(|e| AphoriaError::Walker(format!("Failed to read {IGNORE_FILE_NAME}: {e}")))?;

        Ok(Self::parse(&content))
    }

    /// Parse patterns from a string (the contents of `.aphoriaignore`).
    pub fn parse(content: &str) -> Self {
        let patterns = content
            .lines()
            .map(|line| line.trim())
            .filter(|line| !line.is_empty() && !line.starts_with('#'))
            .map(|line| line.to_string())
            .collect();

        Self { patterns }
    }

    /// Get the parsed patterns.
    pub fn patterns(&self) -> &[String] {
        &self.patterns
    }

    /// Check if the pattern set is empty.
    #[allow(dead_code)]
    pub fn is_empty(&self) -> bool {
        self.patterns.is_empty()
    }

    /// Get the number of patterns.
    #[allow(dead_code)]
    pub fn len(&self) -> usize {
        self.patterns.len()
    }
}

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

    #[test]
    fn test_parse_empty() {
        let patterns = IgnorePatterns::parse("");
        assert!(patterns.is_empty());
    }

    #[test]
    fn test_parse_comments_only() {
        let content = r#"
# This is a comment
# Another comment
        "#;
        let patterns = IgnorePatterns::parse(content);
        assert!(patterns.is_empty());
    }

    #[test]
    fn test_parse_simple_patterns() {
        let content = r#"
# Test fixtures
uat/fixtures/
tests/fixtures/

# Demo apps
docs/demo/vulnbank/
        "#;
        let patterns = IgnorePatterns::parse(content);
        assert_eq!(patterns.len(), 3);
        assert!(patterns.patterns().contains(&"uat/fixtures/".to_string()));
        assert!(patterns.patterns().contains(&"tests/fixtures/".to_string()));
        assert!(patterns.patterns().contains(&"docs/demo/vulnbank/".to_string()));
    }

    #[test]
    fn test_parse_glob_patterns() {
        let content = r#"
**/test_fixtures/**
**/*_test.rs
applications/aphoria/src/extractors/*.rs
        "#;
        let patterns = IgnorePatterns::parse(content);
        assert_eq!(patterns.len(), 3);
        assert!(patterns.patterns().contains(&"**/test_fixtures/**".to_string()));
        assert!(patterns.patterns().contains(&"**/*_test.rs".to_string()));
    }

    #[test]
    fn test_load_nonexistent() {
        let temp_dir = TempDir::new().expect("create temp dir");
        let patterns = IgnorePatterns::load(temp_dir.path()).expect("load patterns");
        assert!(patterns.is_empty());
    }

    #[test]
    fn test_load_existing() {
        let temp_dir = TempDir::new().expect("create temp dir");
        let content = "uat/fixtures/\ntests/";
        fs::write(temp_dir.path().join(IGNORE_FILE_NAME), content).expect("write file");

        let patterns = IgnorePatterns::load(temp_dir.path()).expect("load patterns");
        assert_eq!(patterns.len(), 2);
    }

    #[test]
    fn test_trim_whitespace() {
        let content = "  uat/fixtures/  \n  tests/  ";
        let patterns = IgnorePatterns::parse(content);
        assert_eq!(patterns.len(), 2);
        assert!(patterns.patterns().contains(&"uat/fixtures/".to_string()));
        assert!(patterns.patterns().contains(&"tests/".to_string()));
    }
}
