use ignore::gitignore::{Gitignore, GitignoreBuilder};
use std::{
    collections::HashSet,
    path::{Path, PathBuf},
};

use crate::config::paths::Paths;
use crate::hints::import_files::read_referenced_files;

pub const GOOSE_HINTS_FILENAME: &str = ".goosehints";
pub const AGENTS_MD_FILENAME: &str = "AGENTS.md";

pub fn get_context_filenames() -> Vec<String> {
    use crate::config::Config;

    Config::global()
        .get_param::<Vec<String>>("CONTEXT_FILE_NAMES")
        .unwrap_or_else(|_| {
            vec![
                GOOSE_HINTS_FILENAME.to_string(),
                AGENTS_MD_FILENAME.to_string(),
            ]
        })
}

#[derive(Default)]
pub struct SubdirectoryHintTracker {
    loaded_dirs: HashSet<PathBuf>,
    pending_dirs: Vec<PathBuf>,
    hints_filenames: Vec<String>,
}

impl SubdirectoryHintTracker {
    pub fn new() -> Self {
        Self {
            loaded_dirs: HashSet::new(),
            pending_dirs: Vec::new(),
            hints_filenames: get_context_filenames(),
        }
    }

    pub fn record_tool_arguments(
        &mut self,
        arguments: &Option<serde_json::Map<String, serde_json::Value>>,
        working_dir: &Path,
    ) {
        let args = match arguments.as_ref() {
            Some(a) => a,
            None => return,
        };

        if let Some(path_str) = args.get("path").and_then(|v| v.as_str()) {
            if let Some(dir) = resolve_to_parent_dir(path_str, working_dir) {
                self.pending_dirs.push(dir);
            }
        }

        if let Some(cmd) = args.get("command").and_then(|v| v.as_str()) {
            for token in shell_words::split(cmd).unwrap_or_default() {
                if token.starts_with('-') {
                    continue;
                }
                if token.contains(std::path::MAIN_SEPARATOR) || token.contains('.') {
                    if let Some(dir) = resolve_to_parent_dir(&token, working_dir) {
                        self.pending_dirs.push(dir);
                    }
                }
            }
        }
    }

    pub fn load_new_hints(&mut self, working_dir: &Path) -> Vec<(String, String)> {
        let pending = std::mem::take(&mut self.pending_dirs);
        if pending.is_empty() {
            return Vec::new();
        }

        let mut results = Vec::new();
        for dir in pending {
            if !dir.starts_with(working_dir) || dir == working_dir {
                continue;
            }
            if self.loaded_dirs.contains(&dir) {
                continue;
            }
            if let Some(content) =
                load_hints_from_directory(&dir, working_dir, &self.hints_filenames)
            {
                let key = format!("subdir_hints:{}", dir.display());
                results.push((key, content));
            }
            self.loaded_dirs.insert(dir);
        }
        results
    }
}

fn resolve_to_parent_dir(token: &str, working_dir: &Path) -> Option<PathBuf> {
    let path = Path::new(token);
    let resolved = if path.is_absolute() {
        path.to_path_buf()
    } else {
        working_dir.join(path)
    };
    resolved.parent().map(|d| d.to_path_buf())
}

fn load_hints_from_directory(
    directory: &Path,
    working_dir: &Path,
    hints_filenames: &[String],
) -> Option<String> {
    if !directory.is_dir() || !directory.is_absolute() {
        return None;
    }

    if !directory.starts_with(working_dir) || directory == working_dir {
        return None;
    }

    let git_root = find_git_root(working_dir);
    let import_boundary = git_root.unwrap_or(working_dir);
    let gitignore = Gitignore::empty();

    let mut directories: Vec<PathBuf> = directory
        .ancestors()
        .take_while(|d| d.starts_with(working_dir) && *d != working_dir)
        .map(|d| d.to_path_buf())
        .collect();
    directories.reverse();

    let mut contents = Vec::new();
    for dir in &directories {
        for hints_filename in hints_filenames {
            let hints_path = dir.join(hints_filename);
            if hints_path.is_file() {
                let mut visited = HashSet::new();
                let expanded = read_referenced_files(
                    &hints_path,
                    import_boundary,
                    &mut visited,
                    0,
                    &gitignore,
                );
                if !expanded.is_empty() {
                    contents.push(expanded);
                }
            }
        }
    }

    if contents.is_empty() {
        None
    } else {
        Some(format!(
            "### Subdirectory Hints ({})\n{}",
            directory.display(),
            contents.join("\n")
        ))
    }
}

fn find_git_root(start_dir: &Path) -> Option<&Path> {
    let mut check_dir = start_dir;

    loop {
        if check_dir.join(".git").exists() {
            return Some(check_dir);
        }
        if let Some(parent) = check_dir.parent() {
            check_dir = parent;
        } else {
            break;
        }
    }

    None
}

fn get_local_directories(git_root: Option<&Path>, cwd: &Path) -> Vec<PathBuf> {
    match git_root {
        Some(git_root) => {
            let mut directories = Vec::new();
            let mut current_dir = cwd;

            loop {
                directories.push(current_dir.to_path_buf());
                if current_dir == git_root {
                    break;
                }
                if let Some(parent) = current_dir.parent() {
                    current_dir = parent;
                } else {
                    break;
                }
            }
            directories.reverse();
            directories
        }
        None => vec![cwd.to_path_buf()],
    }
}

/// Build a `Gitignore` that includes `.gitignore` files from the git root
/// down to `cwd`, matching git's hierarchical ignore semantics. When there
/// is no git root, only `cwd/.gitignore` is loaded.
pub fn build_gitignore(cwd: &Path) -> Gitignore {
    let git_root = find_git_root(cwd);
    let directories = get_local_directories(git_root, cwd);

    let mut builder = GitignoreBuilder::new(cwd);
    for dir in &directories {
        let gitignore_path = dir.join(".gitignore");
        if gitignore_path.is_file() {
            builder.add(&gitignore_path);
        }
    }
    builder.build().unwrap_or_else(|_| {
        GitignoreBuilder::new(cwd)
            .build()
            .expect("Failed to build default gitignore")
    })
}

pub fn load_hint_files(
    cwd: &Path,
    hints_filenames: &[String],
    ignore_patterns: &Gitignore,
) -> String {
    let mut global_hints_contents = Vec::with_capacity(hints_filenames.len());
    let mut local_hints_contents = Vec::with_capacity(hints_filenames.len());

    for hints_filename in hints_filenames {
        let global_hints_path = Paths::in_config_dir(hints_filename);
        if global_hints_path.is_file() {
            let mut visited = HashSet::new();
            let hints_dir = global_hints_path.parent().unwrap();
            let expanded_content = read_referenced_files(
                &global_hints_path,
                hints_dir,
                &mut visited,
                0,
                ignore_patterns,
            );
            if !expanded_content.is_empty() {
                global_hints_contents.push(expanded_content);
            }
        }
    }
    let git_root = find_git_root(cwd);
    let local_directories = get_local_directories(git_root, cwd);

    let import_boundary = git_root.unwrap_or(cwd);

    for directory in &local_directories {
        for hints_filename in hints_filenames {
            let hints_path = directory.join(hints_filename);
            if hints_path.is_file() {
                let mut visited = HashSet::new();
                let expanded_content = read_referenced_files(
                    &hints_path,
                    import_boundary,
                    &mut visited,
                    0,
                    ignore_patterns,
                );
                if !expanded_content.is_empty() {
                    local_hints_contents.push(expanded_content);
                }
            }
        }
    }

    let mut hints = String::new();
    if !global_hints_contents.is_empty() {
        hints.push_str("\n### Global Hints\nThese are my global goose hints.\n");
        hints.push_str(&global_hints_contents.join("\n"));
    }

    if !local_hints_contents.is_empty() {
        if !hints.is_empty() {
            hints.push_str("\n\n");
        }
        hints.push_str(
            "### Project Hints\nThese are hints for working on the project in this directory.\n",
        );
        hints.push_str(&local_hints_contents.join("\n"));
    }

    hints
}

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

    fn create_dummy_gitignore() -> Gitignore {
        let temp_dir = tempfile::tempdir().expect("failed to create tempdir");
        let builder = GitignoreBuilder::new(temp_dir.path());
        builder.build().expect("failed to build gitignore")
    }

    #[test]
    fn test_goosehints_when_present() {
        let dir = TempDir::new().unwrap();

        fs::write(dir.path().join(GOOSE_HINTS_FILENAME), "Test hint content").unwrap();
        let gitignore = create_dummy_gitignore();
        let hints = load_hint_files(dir.path(), &[GOOSE_HINTS_FILENAME.to_string()], &gitignore);

        assert!(hints.contains("Test hint content"));
    }

    #[test]
    fn test_goosehints_when_missing() {
        let dir = TempDir::new().unwrap();

        let gitignore = create_dummy_gitignore();
        let hints = load_hint_files(dir.path(), &[GOOSE_HINTS_FILENAME.to_string()], &gitignore);

        assert!(!hints.contains("Project Hints"));
    }

    #[test]
    fn test_goosehints_multiple_filenames() {
        let dir = TempDir::new().unwrap();

        fs::write(
            dir.path().join("CLAUDE.md"),
            "Custom hints file content from CLAUDE.md",
        )
        .unwrap();
        fs::write(
            dir.path().join(GOOSE_HINTS_FILENAME),
            "Custom hints file content from .goosehints",
        )
        .unwrap();

        let gitignore = create_dummy_gitignore();
        let hints = load_hint_files(
            dir.path(),
            &["CLAUDE.md".to_string(), GOOSE_HINTS_FILENAME.to_string()],
            &gitignore,
        );

        assert!(hints.contains("Custom hints file content from CLAUDE.md"));
        assert!(hints.contains("Custom hints file content from .goosehints"));
    }

    #[test]
    fn test_goosehints_configurable_filename() {
        let dir = TempDir::new().unwrap();

        fs::write(dir.path().join("CLAUDE.md"), "Custom hints file content").unwrap();
        let gitignore = create_dummy_gitignore();
        let hints = load_hint_files(dir.path(), &["CLAUDE.md".to_string()], &gitignore);

        assert!(hints.contains("Custom hints file content"));
        assert!(!hints.contains(".goosehints")); // Make sure it's not loading the default
    }

    #[test]
    fn test_nested_goosehints_with_git_root() {
        let temp_dir = TempDir::new().unwrap();
        let project_root = temp_dir.path();

        fs::create_dir(project_root.join(".git")).unwrap();
        fs::write(
            project_root.join(GOOSE_HINTS_FILENAME),
            "Root hints content",
        )
        .unwrap();

        let subdir = project_root.join("subdir");
        fs::create_dir(&subdir).unwrap();
        fs::write(subdir.join(GOOSE_HINTS_FILENAME), "Subdir hints content").unwrap();
        let current_dir = subdir.join("current_dir");
        fs::create_dir(&current_dir).unwrap();
        fs::write(
            current_dir.join(GOOSE_HINTS_FILENAME),
            "current_dir hints content",
        )
        .unwrap();

        let gitignore = create_dummy_gitignore();
        let hints = load_hint_files(
            &current_dir,
            &[GOOSE_HINTS_FILENAME.to_string()],
            &gitignore,
        );

        assert!(
            hints.contains("Root hints content\nSubdir hints content\ncurrent_dir hints content")
        );
    }

    #[test]
    fn test_nested_goosehints_without_git_root() {
        let temp_dir = TempDir::new().unwrap();
        let base_dir = temp_dir.path();

        fs::write(base_dir.join(GOOSE_HINTS_FILENAME), "Base hints content").unwrap();

        let subdir = base_dir.join("subdir");
        fs::create_dir(&subdir).unwrap();
        fs::write(subdir.join(GOOSE_HINTS_FILENAME), "Subdir hints content").unwrap();

        let current_dir = subdir.join("current_dir");
        fs::create_dir(&current_dir).unwrap();
        fs::write(
            current_dir.join(GOOSE_HINTS_FILENAME),
            "Current dir hints content",
        )
        .unwrap();

        let gitignore = create_dummy_gitignore();
        let hints = load_hint_files(
            &current_dir,
            &[GOOSE_HINTS_FILENAME.to_string()],
            &gitignore,
        );

        // Without .git, should only find hints in current directory
        assert!(hints.contains("Current dir hints content"));
        assert!(!hints.contains("Base hints content"));
        assert!(!hints.contains("Subdir hints content"));
    }

    #[test]
    fn test_nested_goosehints_mixed_filenames() {
        let temp_dir = TempDir::new().unwrap();
        let project_root = temp_dir.path();

        fs::create_dir(project_root.join(".git")).unwrap();
        fs::write(project_root.join("CLAUDE.md"), "Root CLAUDE.md content").unwrap();

        let subdir = project_root.join("subdir");
        fs::create_dir(&subdir).unwrap();
        fs::write(
            subdir.join(GOOSE_HINTS_FILENAME),
            "Subdir .goosehints content",
        )
        .unwrap();

        let current_dir = subdir.join("current_dir");
        fs::create_dir(&current_dir).unwrap();

        let gitignore = create_dummy_gitignore();
        let hints = load_hint_files(
            &current_dir,
            &["CLAUDE.md".to_string(), GOOSE_HINTS_FILENAME.to_string()],
            &gitignore,
        );

        assert!(hints.contains("Root CLAUDE.md content"));
        assert!(hints.contains("Subdir .goosehints content"));
    }

    #[test]
    fn test_hints_with_basic_imports() {
        let temp_dir = TempDir::new().unwrap();
        let project_root = temp_dir.path();

        fs::create_dir(project_root.join(".git")).unwrap();

        fs::write(project_root.join("README.md"), "# Project README").unwrap();
        fs::write(project_root.join("config.md"), "Configuration details").unwrap();

        let hints_content = r#"Project hints content
@README.md
@config.md
Additional instructions here."#;
        fs::write(project_root.join(GOOSE_HINTS_FILENAME), hints_content).unwrap();

        let gitignore = create_dummy_gitignore();
        let hints = load_hint_files(
            project_root,
            &[GOOSE_HINTS_FILENAME.to_string()],
            &gitignore,
        );

        assert!(hints.contains("Project hints content"));
        assert!(hints.contains("Additional instructions here"));

        assert!(hints.contains("--- Content from README.md ---"));
        assert!(hints.contains("# Project README"));
        assert!(hints.contains("--- End of README.md ---"));

        assert!(hints.contains("--- Content from config.md ---"));
        assert!(hints.contains("Configuration details"));
        assert!(hints.contains("--- End of config.md ---"));
    }

    #[test]
    fn test_hints_with_git_import_boundary() {
        let temp_dir = TempDir::new().unwrap();
        let project_root = temp_dir.path();

        fs::create_dir(project_root.join(".git")).unwrap();

        fs::write(project_root.join("root_file.md"), "Root file content").unwrap();
        fs::write(
            project_root.join("shared_docs.md"),
            "Shared documentation content",
        )
        .unwrap();

        let docs_dir = project_root.join("docs");
        fs::create_dir_all(&docs_dir).unwrap();
        fs::write(docs_dir.join("api.md"), "API documentation content").unwrap();

        let utils_dir = project_root.join("src").join("utils");
        fs::create_dir_all(&utils_dir).unwrap();
        fs::write(
            utils_dir.join("helpers.md"),
            "Helper utilities content @../../shared_docs.md",
        )
        .unwrap();

        let components_dir = project_root.join("src").join("components");
        fs::create_dir_all(&components_dir).unwrap();
        fs::write(components_dir.join("local_file.md"), "Local file content").unwrap();

        let outside_dir = temp_dir.path().parent().unwrap();
        fs::write(outside_dir.join("forbidden.md"), "Forbidden content").unwrap();

        let root_hints_content = r#"Project root hints
@docs/api.md
Root level instructions"#;
        fs::write(project_root.join(GOOSE_HINTS_FILENAME), root_hints_content).unwrap();

        let nested_hints_content = r#"Nested directory hints
@local_file.md
@../utils/helpers.md
@../../docs/api.md
@../../root_file.md
@../../../forbidden.md
End of nested hints"#;
        fs::write(
            components_dir.join(GOOSE_HINTS_FILENAME),
            nested_hints_content,
        )
        .unwrap();

        let gitignore = create_dummy_gitignore();
        let hints = load_hint_files(
            &components_dir,
            &[GOOSE_HINTS_FILENAME.to_string()],
            &gitignore,
        );
        println!("======{}", hints);
        assert!(hints.contains("Project root hints"));
        assert!(hints.contains("Root level instructions"));

        assert!(hints.contains("API documentation content"));
        assert!(hints.contains("--- Content from docs/api.md ---"));

        assert!(hints.contains("Nested directory hints"));
        assert!(hints.contains("End of nested hints"));

        assert!(hints.contains("Local file content"));
        assert!(hints.contains("--- Content from local_file.md ---"));

        assert!(hints.contains("Helper utilities content"));
        assert!(hints.contains("--- Content from ../utils/helpers.md ---"));
        assert!(hints.contains("Shared documentation content"));
        assert!(hints.contains("--- Content from ../../shared_docs.md ---"));

        let api_content_count = hints.matches("API documentation content").count();
        assert_eq!(
            api_content_count, 2,
            "API content should appear twice - from root and nested hints"
        );

        assert!(hints.contains("Root file content"));
        assert!(hints.contains("--- Content from ../../root_file.md ---"));

        assert!(!hints.contains("Forbidden content"));
        assert!(hints.contains("@../../../forbidden.md"));
    }

    #[test]
    fn test_hints_without_git_import_boundary() {
        let temp_dir = TempDir::new().unwrap();
        let base_dir = temp_dir.path();

        let current_dir = base_dir.join("current");
        fs::create_dir(&current_dir).unwrap();
        fs::write(current_dir.join("local.md"), "Local content").unwrap();

        fs::write(base_dir.join("parent.md"), "Parent content").unwrap();

        let hints_content = r#"Current directory hints
@local.md
@../parent.md
End of hints"#;
        fs::write(current_dir.join(GOOSE_HINTS_FILENAME), hints_content).unwrap();

        let gitignore = create_dummy_gitignore();
        let hints = load_hint_files(
            &current_dir,
            &[GOOSE_HINTS_FILENAME.to_string()],
            &gitignore,
        );

        assert!(hints.contains("Local content"));
        assert!(hints.contains("--- Content from local.md ---"));

        assert!(!hints.contains("Parent content"));
        assert!(hints.contains("@../parent.md"));
    }

    #[test]
    fn test_import_boundary_respects_nested_setting() {
        let temp_dir = TempDir::new().unwrap();
        let project_root = temp_dir.path();
        fs::create_dir(project_root.join(".git")).unwrap();
        fs::write(project_root.join("root_file.md"), "Root file content").unwrap();
        let subdir = project_root.join("subdir");
        fs::create_dir(&subdir).unwrap();
        fs::write(subdir.join("local_file.md"), "Local file content").unwrap();
        let hints_content = r#"Subdir hints
@local_file.md
@../root_file.md
End of hints"#;
        fs::write(subdir.join(GOOSE_HINTS_FILENAME), hints_content).unwrap();
        let gitignore = create_dummy_gitignore();

        let hints = load_hint_files(&subdir, &[GOOSE_HINTS_FILENAME.to_string()], &gitignore);

        assert!(hints.contains("Local file content"));
        assert!(hints.contains("--- Content from local_file.md ---"));

        assert!(hints.contains("Root file content"));
        assert!(hints.contains("--- Content from ../root_file.md ---"));
    }

    #[test]
    fn resolve_to_parent_dir_relative() {
        let wd = Path::new("/home/user/project");
        assert_eq!(
            resolve_to_parent_dir("src/main.rs", wd),
            Some(PathBuf::from("/home/user/project/src"))
        );
    }

    #[test]
    fn resolve_to_parent_dir_absolute() {
        let wd = Path::new("/home/user/project");
        assert_eq!(
            resolve_to_parent_dir("/tmp/foo.rs", wd),
            Some(PathBuf::from("/tmp"))
        );
    }

    #[test]
    fn tracker_records_path_argument() {
        let wd = PathBuf::from("/home/user/project");
        let mut tracker = SubdirectoryHintTracker::new();
        let args: serde_json::Map<String, serde_json::Value> =
            serde_json::from_str(r#"{"path": "src/main.rs"}"#).unwrap();
        tracker.record_tool_arguments(&Some(args), &wd);
        let hints = tracker.load_new_hints(&wd);
        assert!(hints.is_empty());
        assert!(tracker
            .loaded_dirs
            .contains(&PathBuf::from("/home/user/project/src")));
    }

    #[test]
    fn tracker_records_command_argument() {
        let wd = PathBuf::from("/home/user/project");
        let mut tracker = SubdirectoryHintTracker::new();
        let args: serde_json::Map<String, serde_json::Value> =
            serde_json::from_str(r#"{"command": "cat nested/doc.md"}"#).unwrap();
        tracker.record_tool_arguments(&Some(args), &wd);
        let hints = tracker.load_new_hints(&wd);
        assert!(hints.is_empty());
        assert!(tracker
            .loaded_dirs
            .contains(&PathBuf::from("/home/user/project/nested")));
    }

    #[test]
    fn tracker_skips_flags_in_command() {
        let wd = PathBuf::from("/home/user/project");
        let mut tracker = SubdirectoryHintTracker::new();
        let args: serde_json::Map<String, serde_json::Value> =
            serde_json::from_str(r#"{"command": "grep -rn pattern src/lib.rs"}"#).unwrap();
        tracker.record_tool_arguments(&Some(args), &wd);
        let _ = tracker.load_new_hints(&wd);
        assert!(tracker
            .loaded_dirs
            .contains(&PathBuf::from("/home/user/project/src")));
        assert_eq!(tracker.loaded_dirs.len(), 1);
    }

    #[test]
    fn tracker_loads_subdirectory_hints() {
        let temp_dir = TempDir::new().unwrap();
        let project_root = temp_dir.path().to_path_buf();
        let subdir = project_root.join("nested");
        fs::create_dir_all(&subdir).unwrap();
        fs::write(
            subdir.join(GOOSE_HINTS_FILENAME),
            "nested subdirectory hints",
        )
        .unwrap();

        let mut tracker = SubdirectoryHintTracker::new();
        let args: serde_json::Map<String, serde_json::Value> =
            serde_json::from_str(r#"{"path": "nested/foo.rs"}"#).unwrap();
        tracker.record_tool_arguments(&Some(args), &project_root);
        let hints = tracker.load_new_hints(&project_root);
        assert_eq!(hints.len(), 1);
        assert!(hints[0].0.contains("nested"));
        assert!(hints[0].1.contains("nested subdirectory hints"));
    }

    #[test]
    fn tracker_deduplicates_directories() {
        let temp_dir = TempDir::new().unwrap();
        let project_root = temp_dir.path().to_path_buf();
        let subdir = project_root.join("nested");
        fs::create_dir_all(&subdir).unwrap();
        fs::write(subdir.join(GOOSE_HINTS_FILENAME), "nested hints").unwrap();

        let mut tracker = SubdirectoryHintTracker::new();
        let args: serde_json::Map<String, serde_json::Value> =
            serde_json::from_str(r#"{"path": "nested/foo.rs"}"#).unwrap();
        tracker.record_tool_arguments(&Some(args.clone()), &project_root);
        let hints = tracker.load_new_hints(&project_root);
        assert_eq!(hints.len(), 1);

        tracker.record_tool_arguments(&Some(args), &project_root);
        let hints = tracker.load_new_hints(&project_root);
        assert!(hints.is_empty());
    }
}

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

    #[test]
    fn test_hints_with_gitignore_filters_referenced_files() {
        let dir = TempDir::new().unwrap();
        let project_root = dir.path();

        fs::create_dir(project_root.join(".git")).unwrap();
        fs::write(project_root.join("allowed.md"), "Allowed content").unwrap();
        fs::write(project_root.join("secret.env"), "SECRET_KEY=abc123").unwrap();
        fs::write(project_root.join(".gitignore"), "*.env\n").unwrap();

        let hints_content = "Project hints\n@allowed.md\n@secret.env\nEnd of hints";
        fs::write(project_root.join(GOOSE_HINTS_FILENAME), hints_content).unwrap();

        let gitignore = build_gitignore(project_root);

        let hints = load_hint_files(
            project_root,
            &[GOOSE_HINTS_FILENAME.to_string()],
            &gitignore,
        );

        assert!(hints.contains("Allowed content"));
        assert!(!hints.contains("SECRET_KEY=abc123"));
        assert!(hints.contains("@secret.env"));
    }

    #[test]
    fn test_build_gitignore_loads_from_git_root_in_subdirectory() {
        let dir = TempDir::new().unwrap();
        let project_root = dir.path();

        fs::create_dir(project_root.join(".git")).unwrap();
        // Root .gitignore ignores .env files
        fs::write(project_root.join(".gitignore"), "*.env\n").unwrap();
        fs::write(project_root.join("secret.env"), "SECRET_KEY=abc123").unwrap();
        fs::write(project_root.join("allowed.md"), "Allowed content").unwrap();

        let subdir = project_root.join("subdir");
        fs::create_dir(&subdir).unwrap();

        let hints_content = "Subdir hints\n@../allowed.md\n@../secret.env\nEnd of hints";
        fs::write(subdir.join(GOOSE_HINTS_FILENAME), hints_content).unwrap();

        // Build gitignore from the subdirectory — should still pick up root .gitignore
        let gitignore = build_gitignore(&subdir);

        let hints = load_hint_files(&subdir, &[GOOSE_HINTS_FILENAME.to_string()], &gitignore);

        assert!(hints.contains("Allowed content"));
        assert!(!hints.contains("SECRET_KEY=abc123"));
        assert!(hints.contains("@../secret.env"));
    }

    #[test]
    fn test_build_gitignore_merges_nested_gitignores() {
        let dir = TempDir::new().unwrap();
        let project_root = dir.path();

        fs::create_dir(project_root.join(".git")).unwrap();
        // Root ignores *.log
        fs::write(project_root.join(".gitignore"), "*.log\n").unwrap();

        let subdir = project_root.join("subdir");
        fs::create_dir(&subdir).unwrap();
        // Subdir ignores *.tmp
        fs::write(subdir.join(".gitignore"), "*.tmp\n").unwrap();

        fs::write(project_root.join("debug.log"), "debug log").unwrap();
        fs::write(subdir.join("cache.tmp"), "temp data").unwrap();
        fs::write(subdir.join("readme.md"), "Readme content").unwrap();

        let hints_content = "Hints\n@../debug.log\n@cache.tmp\n@readme.md\nEnd";
        fs::write(subdir.join(GOOSE_HINTS_FILENAME), hints_content).unwrap();

        let gitignore = build_gitignore(&subdir);
        let hints = load_hint_files(&subdir, &[GOOSE_HINTS_FILENAME.to_string()], &gitignore);

        assert!(hints.contains("Readme content"));
        assert!(!hints.contains("debug log"));
        assert!(!hints.contains("temp data"));
        assert!(hints.contains("@../debug.log"));
        assert!(hints.contains("@cache.tmp"));
    }
}
