//! Shared gitignore matching utility.
//!
//! Single source of truth for checking whether a path is ignored by
//! `.gitignore` rules. Used by both the initial AGENTS.md discovery
//! (`kigi-agent::prompt::ignore`) and the runtime tracker
//! (`AgentsMdTracker`).

use ignore::gitignore::Gitignore;
use std::path::Path;

/// Check if a path is ignored by the given gitignore rules.
///
/// Strips `git_root` prefix before matching — gitignore patterns are
/// repo-relative, so `/repo/build/out.o` becomes `build/out.o` when
/// `git_root` is `/repo`.
///
/// This is a pure function — no filesystem access, just `Gitignore::matched()`.
pub fn is_ignored(gitignore: &Gitignore, path: &Path, git_root: Option<&Path>) -> bool {
    let check_path = match git_root {
        Some(root) => match path.strip_prefix(root) {
            Ok(relative) => relative,
            // Outside the repo (e.g. ~/.kigi/Agents.md) → not ignored.
            Err(_) => return false,
        },
        None => {
            // Absolute path + no git root → can't strip to repo-relative;
            // the `ignore` crate panics on absolute paths not under root.
            if path.is_absolute() {
                return false;
            }
            path
        }
    };
    // matched_path_or_any_parents checks parent dirs too, so
    // `build/AGENTS.md` correctly matches a `build/` pattern.
    gitignore
        .matched_path_or_any_parents(check_path, check_path.is_dir())
        .is_ignore()
}

#[cfg(test)]
mod tests {
    use super::*;
    use ignore::gitignore::GitignoreBuilder;

    fn build_gitignore(root: &Path, patterns: &[&str]) -> Gitignore {
        let mut builder = GitignoreBuilder::new(root);
        for pattern in patterns {
            builder.add_line(None, pattern).unwrap();
        }
        builder.build().unwrap()
    }

    #[test]
    fn is_ignored_matches_gitignored_paths() {
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        // Canonicalize to handle macOS /tmp → /private/tmp
        let root = &dunce::canonicalize(root).unwrap();
        let gi = build_gitignore(root, &["build/"]);
        assert!(is_ignored(&gi, &root.join("build/out.o"), Some(root)));
        assert!(is_ignored(&gi, &root.join("build/sub/file.rs"), Some(root)));
    }

    #[test]
    fn is_ignored_does_not_match_non_gitignored_paths() {
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let root = &dunce::canonicalize(root).unwrap();
        let gi = build_gitignore(root, &["build/"]);
        assert!(!is_ignored(&gi, &root.join("src/main.rs"), Some(root)));
        assert!(!is_ignored(&gi, &root.join("AGENTS.md"), Some(root)));
    }

    #[test]
    fn is_ignored_strips_git_root_prefix() {
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let root = &dunce::canonicalize(root).unwrap();
        let gi = build_gitignore(root, &["build/"]);
        // With root: strips prefix, matches build/out.o
        assert!(is_ignored(&gi, &root.join("build/out.o"), Some(root)));
        // Without root: relative path still matches
        assert!(is_ignored(
            &gi,
            &std::path::PathBuf::from("build/out.o"),
            None
        ));
    }

    #[test]
    fn is_ignored_returns_false_for_path_outside_git_root() {
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        let root = &dunce::canonicalize(root).unwrap();
        let gi = build_gitignore(root, &["build/", "*.md"]);
        // A path completely outside the git root should not be checked
        // against the repo's .gitignore (e.g., ~/.kigi/Agents.md).
        let outside_path = std::path::PathBuf::from("/some/other/path/Agents.md");
        assert!(!is_ignored(&gi, &outside_path, Some(root)));
    }

    /// Regression: running outside a git repo panicked with
    /// "path is expected to be under the root" (ignore crate assert).
    #[test]
    fn regression_no_panic_on_absolute_path_without_git_root() {
        let gi = build_gitignore(Path::new("."), &["node_modules/", "*.log"]);
        let abs_path = Path::new("/Users/someone/home/AGENTS.md");

        // Proves the raw crate panics with these inputs.
        assert!(
            std::panic::catch_unwind(|| {
                gi.matched_path_or_any_parents(abs_path, false);
            })
            .is_err()
        );

        // Our wrapper guards against it.
        assert!(!is_ignored(&gi, abs_path, None));
    }
}
