//! Checks whether paths are ignored during incremental project indexing.

use crate::system::SystemPath;
use crate::system::walk_directory::IgnoreIncremental;

pub(super) struct IgnoreFiles {
    pub(super) root_matchers: Vec<ignore::IncrementalIgnore>,
}

impl IgnoreIncremental for IgnoreFiles {
    fn is_ignored(&mut self, path: &SystemPath, is_directory: bool) -> bool {
        let Some((root, relative)) = self
            .root_matchers
            .iter_mut()
            .filter_map(|root| {
                let relative = path.as_std_path().strip_prefix(root.root()).ok()?;
                Some((root, relative))
            })
            .max_by_key(|(root, _)| root.root().as_os_str().len())
        else {
            return false;
        };
        root.matched(relative, is_directory).is_ignore()
    }
}

#[cfg(test)]
mod tests {
    use tempfile::TempDir;

    use crate::system::{OsSystem, System, SystemPath, SystemPathBuf};

    struct TestProject {
        _temp_dir: TempDir,
        system: OsSystem,
        root: SystemPathBuf,
    }

    impl TestProject {
        fn new() -> Self {
            Self::with_root("project")
        }

        fn with_root(root: &str) -> Self {
            let temp_dir = TempDir::new().unwrap();
            let temp_dir_path = SystemPath::from_std_path(temp_dir.path()).unwrap();
            let root = temp_dir_path.join(root);
            std::fs::create_dir_all(root.as_std_path()).unwrap();
            let system = OsSystem::new(&root);

            Self {
                _temp_dir: temp_dir,
                system,
                root,
            }
        }

        fn path(&self, relative_path: &str) -> SystemPathBuf {
            self.root.join(relative_path)
        }

        fn write_files<'a>(&self, files: impl IntoIterator<Item = (SystemPathBuf, &'a str)>) {
            for (path, contents) in files {
                std::fs::create_dir_all(path.parent().unwrap().as_std_path()).unwrap();
                std::fs::write(path.as_std_path(), contents).unwrap();
            }
        }

        fn create_directory(&self, path: impl AsRef<SystemPath>) {
            std::fs::create_dir_all(path.as_ref().as_std_path()).unwrap();
        }

        fn is_ignored(&self, path: &SystemPath) -> bool {
            self.is_ignored_from(std::slice::from_ref(&self.root), path)
        }

        fn is_ignored_from(&self, walk_roots: &[SystemPathBuf], path: &SystemPath) -> bool {
            let (first, additional) = walk_roots.split_first().unwrap();
            let mut builder = self.system.walk_directory(first);

            for root in additional {
                builder = builder.add(root);
            }

            builder.incremental_matcher().is_ignored(path, false)
        }
    }

    #[test]
    fn root_ignore_file_prunes_top_level_directory() {
        let project = TestProject::new();
        let path = project.path("build/keep.py");
        project.write_files([
            (project.path(".ignore"), "build/\n"),
            (project.path("build/.ignore"), "!keep.py\n"),
        ]);

        assert!(project.is_ignored(&path));
    }

    #[test]
    fn root_gitignore_file_requires_repository() {
        let project = TestProject::new();

        let path = project.path("build/keep.py");
        project.write_files([(project.path(".gitignore"), "build/\n")]);

        assert!(!project.is_ignored(&path));
    }

    #[test]
    fn bom() {
        let project = TestProject::new();
        let path = project.path("build/keep.py");
        project.write_files([(project.path(".ignore"), "\u{feff}build/\n")]);

        assert!(project.is_ignored(&path));
    }

    #[test]
    fn root_ignore_file_allowlist_overrides_root_gitignore_file() {
        let project = TestProject::new();
        let path = project.path("build/keep.py");
        project.write_files([
            (project.path(".git/HEAD"), "ref: refs/heads/main\n"),
            (project.path(".ignore"), "!build/\n"),
            (project.path(".gitignore"), "build/\n"),
        ]);

        assert!(!project.is_ignored(&path));
    }

    #[test]
    fn parent_ignore_file_disables_root_gitignore_pruning() {
        let project = TestProject::with_root("workspace/project");
        let path = project.path("build/keep.py");
        project.write_files([
            (project.path(".git/HEAD"), "ref: refs/heads/main\n"),
            (project.path(".gitignore"), "build/\n"),
            (
                project.root.parent().unwrap().join(".ignore"),
                "!project/build/\n",
            ),
        ]);

        assert!(!project.is_ignored(&path));
    }

    #[test]
    fn root_ignore_file_cannot_prune_deeper_file_match() {
        let project = TestProject::new();
        let path = project.path("pkg/keep.py");
        project.write_files([(project.path(".ignore"), "pkg/keep.py\n")]);

        assert!(project.is_ignored(&path));
    }

    #[test]
    fn unreadable_root_ignore_file_cannot_prune_path() {
        let project = TestProject::new();
        let path = project.path("build/ignored.py");
        project.create_directory(project.path(".ignore"));
        project.write_files([
            (project.path(".git/HEAD"), "ref: refs/heads/main\n"),
            (project.path(".gitignore"), "build/\n"),
        ]);

        assert!(project.is_ignored(&path));
    }

    #[test]
    fn explicit_file_walk_root_cannot_be_pruned_by_parent_root() {
        let project = TestProject::new();
        let path = project.path("build/keep.py");
        project.write_files([(project.path(".ignore"), "build/\n")]);

        assert!(!project.is_ignored_from(&[project.root.clone(), path.clone()], &path));
    }

    #[test]
    fn nested_directory_walk_root_uses_its_own_ignore_file() {
        let project = TestProject::new();
        let path = project.path("pkg/build/ignored.py");
        let nested_root = project.path("pkg");
        project.write_files([
            (project.path(".ignore"), "pkg/\n"),
            (project.path("pkg/.ignore"), "build/\n"),
        ]);

        assert!(project.is_ignored_from(&[project.root.clone(), nested_root], &path));
    }
}
