//! Tests to verify .dockerignore excludes sensitive paths from Docker build context.
//!
//! These tests validate that:
//! 1. The .dockerignore file exists
//! 2. All security-critical paths are excluded
//! 3. All build-essential paths are NOT excluded
//! 4. Pattern syntax is valid

use std::path::Path;

/// Paths that MUST be excluded from Docker build context (security/performance)
const MUST_EXCLUDE: &[&str] = &[
    ".git",
    ".githooks",
    "target",
    "docs",
    "examples",
    "tests",
    "*.md",
    "*.png",
    "*.db",
    "*.db-journal",
    ".DS_Store",
    ".github",
    "deny.toml",
    "LICENSE",
    ".env",
    ".tmp_*",
];

/// Paths that MUST NOT be excluded (required for build)
const MUST_INCLUDE: &[&str] = &["Cargo.toml", "Cargo.lock", "src/"];

/// Parse .dockerignore and return all non-comment, non-empty lines
fn parse_dockerignore(content: &str) -> Vec<String> {
    content
        .lines()
        .map(|line| line.trim())
        .filter(|line| !line.is_empty() && !line.starts_with('#'))
        .map(|line| line.to_string())
        .collect()
}

/// Check if a pattern would match a given path
fn pattern_matches(pattern: &str, path: &str) -> bool {
    // Handle negation patterns
    if pattern.starts_with('!') {
        return false; // Negation re-includes, so it doesn't "exclude"
    }

    // Handle glob patterns
    if pattern.starts_with("*.") {
        let ext = &pattern[1..]; // e.g., ".md"
        return path.ends_with(ext);
    }

    // Handle directory patterns (with or without trailing slash)
    let pattern_normalized = pattern.trim_end_matches('/');
    let path_normalized = path.trim_end_matches('/');

    // Exact match
    if path_normalized == pattern_normalized {
        return true;
    }

    // Pattern is a prefix (directory match)
    if path_normalized.starts_with(&format!("{}/", pattern_normalized)) {
        return true;
    }

    // Wildcard prefix patterns like ".tmp_*"
    if pattern.contains('*') && !pattern.starts_with("*.") {
        let prefix = pattern.split('*').next().unwrap_or("");
        if !prefix.is_empty() && path.starts_with(prefix) {
            return true;
        }
    }

    false
}

/// Check if any pattern in the list would exclude the given path
fn is_excluded(patterns: &[String], path: &str) -> bool {
    let mut excluded = false;
    for pattern in patterns {
        if let Some(negated) = pattern.strip_prefix('!') {
            // Negation pattern - re-include
            if pattern_matches(negated, path) {
                excluded = false;
            }
        } else if pattern_matches(pattern, path) {
            excluded = true;
        }
    }
    excluded
}

#[tokio::test]
async fn dockerignore_file_exists() {
    let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
    assert!(
        path.exists(),
        ".dockerignore file must exist at project root"
    );
}

#[tokio::test]
async fn dockerignore_excludes_security_critical_paths() {
    let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
    let content = tokio::fs::read_to_string(&path)
        .await
        .expect("Failed to read .dockerignore");
    let patterns = parse_dockerignore(&content);

    for must_exclude in MUST_EXCLUDE {
        // For glob patterns, test with a sample file
        let test_path = if must_exclude.starts_with("*.") {
            format!("sample{}", &must_exclude[1..])
        } else {
            must_exclude.to_string()
        };

        assert!(
            is_excluded(&patterns, &test_path),
            "Path '{}' (tested as '{}') MUST be excluded by .dockerignore but is not. \
             This is a security/performance issue.",
            must_exclude,
            test_path
        );
    }
}

#[tokio::test]
async fn dockerignore_does_not_exclude_build_essentials() {
    let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
    let content = tokio::fs::read_to_string(&path)
        .await
        .expect("Failed to read .dockerignore");
    let patterns = parse_dockerignore(&content);

    for must_include in MUST_INCLUDE {
        assert!(
            !is_excluded(&patterns, must_include),
            "Path '{}' MUST NOT be excluded by .dockerignore (required for build)",
            must_include
        );
    }
}

#[tokio::test]
async fn dockerignore_excludes_git_directory() {
    let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
    let content = tokio::fs::read_to_string(&path)
        .await
        .expect("Failed to read .dockerignore");
    let patterns = parse_dockerignore(&content);

    // .git directory and its contents must be excluded
    assert!(is_excluded(&patterns, ".git"), ".git must be excluded");
    assert!(
        is_excluded(&patterns, ".git/config"),
        ".git/config must be excluded"
    );
    assert!(
        is_excluded(&patterns, ".git/objects/pack/pack-abc123.pack"),
        ".git subdirectories must be excluded"
    );
}

#[tokio::test]
async fn dockerignore_excludes_target_directory() {
    let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
    let content = tokio::fs::read_to_string(&path)
        .await
        .expect("Failed to read .dockerignore");
    let patterns = parse_dockerignore(&content);

    assert!(is_excluded(&patterns, "target"), "target must be excluded");
    assert!(
        is_excluded(&patterns, "target/debug/kylinbot"),
        "target/debug must be excluded"
    );
    assert!(
        is_excluded(&patterns, "target/release/kylinbot"),
        "target/release must be excluded"
    );
}

#[tokio::test]
async fn dockerignore_excludes_database_files() {
    let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
    let content = tokio::fs::read_to_string(&path)
        .await
        .expect("Failed to read .dockerignore");
    let patterns = parse_dockerignore(&content);

    assert!(
        is_excluded(&patterns, "brain.db"),
        "*.db files must be excluded"
    );
    assert!(
        is_excluded(&patterns, "memory.db"),
        "*.db files must be excluded"
    );
    assert!(
        is_excluded(&patterns, "brain.db-journal"),
        "*.db-journal files must be excluded"
    );
}

#[tokio::test]
async fn dockerignore_excludes_markdown_files() {
    let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
    let content = tokio::fs::read_to_string(&path)
        .await
        .expect("Failed to read .dockerignore");
    let patterns = parse_dockerignore(&content);

    assert!(
        is_excluded(&patterns, "README.md"),
        "*.md files must be excluded"
    );
    assert!(
        is_excluded(&patterns, "CHANGELOG.md"),
        "*.md files must be excluded"
    );
    assert!(
        is_excluded(&patterns, "CONTRIBUTING.md"),
        "*.md files must be excluded"
    );
}

#[tokio::test]
async fn dockerignore_excludes_image_files() {
    let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
    let content = tokio::fs::read_to_string(&path)
        .await
        .expect("Failed to read .dockerignore");
    let patterns = parse_dockerignore(&content);

    assert!(
        is_excluded(&patterns, "kylinbot.png"),
        "*.png files must be excluded"
    );
    assert!(
        is_excluded(&patterns, "kylin-bot.svg"),
        "*.png files must be excluded"
    );
}

#[tokio::test]
async fn dockerignore_excludes_env_files() {
    let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
    let content = tokio::fs::read_to_string(&path)
        .await
        .expect("Failed to read .dockerignore");
    let patterns = parse_dockerignore(&content);

    assert!(
        is_excluded(&patterns, ".env"),
        ".env must be excluded (contains secrets)"
    );
}

#[tokio::test]
async fn dockerignore_excludes_ci_configs() {
    let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
    let content = tokio::fs::read_to_string(&path)
        .await
        .expect("Failed to read .dockerignore");
    let patterns = parse_dockerignore(&content);

    assert!(
        is_excluded(&patterns, ".github"),
        ".github must be excluded"
    );
    assert!(
        is_excluded(&patterns, ".github/workflows/ci.yml"),
        ".github/workflows must be excluded"
    );
}

#[tokio::test]
async fn dockerignore_has_valid_syntax() {
    let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
    let content = tokio::fs::read_to_string(&path)
        .await
        .expect("Failed to read .dockerignore");

    for (line_num, line) in content.lines().enumerate() {
        let trimmed = line.trim();

        // Skip empty lines and comments
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }

        // Check for invalid patterns
        assert!(
            !trimmed.contains("**") || trimmed.matches("**").count() <= 2,
            "Line {}: Too many ** in pattern '{}'",
            line_num + 1,
            trimmed
        );

        // Check for trailing spaces (can cause issues)
        assert!(
            line.trim_end() == line.trim_start().trim_end(),
            "Line {}: Pattern '{}' has leading whitespace which may cause issues",
            line_num + 1,
            line
        );
    }
}

#[tokio::test]
async fn dockerignore_pattern_matching_edge_cases() {
    // Test the pattern matching logic itself
    let patterns = vec![
        ".git".to_string(),
        ".githooks".to_string(),
        "target".to_string(),
        "*.md".to_string(),
        "*.db".to_string(),
        ".tmp_*".to_string(),
        ".env".to_string(),
    ];

    // Should match
    assert!(is_excluded(&patterns, ".git"));
    assert!(is_excluded(&patterns, ".git/config"));
    assert!(is_excluded(&patterns, ".githooks"));
    assert!(is_excluded(&patterns, "target"));
    assert!(is_excluded(&patterns, "target/debug/build"));
    assert!(is_excluded(&patterns, "README.md"));
    assert!(is_excluded(&patterns, "brain.db"));
    assert!(is_excluded(&patterns, ".env"));

    // Should NOT match
    assert!(!is_excluded(&patterns, "src"));
    assert!(!is_excluded(&patterns, "src/main.rs"));
    assert!(!is_excluded(&patterns, "Cargo.toml"));
    assert!(!is_excluded(&patterns, "Cargo.lock"));
}
