mod ancestor {
    use git_hash::{oid, ObjectId};
    use git_odb::{linked::Store, pack::FindExt};
    use git_traverse::commit;

    use crate::hex_to_id;

    fn db() -> crate::Result<Store> {
        let dir = git_testtools::scripted_fixture_repo_read_only("make_traversal_repo_for_commits.sh")?;
        let db = Store::at(dir.join(".git").join("objects"))?;
        Ok(db)
    }

    fn filtered_iter(
        tips: impl IntoIterator<Item = impl Into<ObjectId>>,
        predicate: impl FnMut(&oid) -> bool,
    ) -> impl Iterator<Item = Result<ObjectId, commit::ancestors::Error>> {
        let db = db().expect("db instantiation works as its definitely valid");
        commit::Ancestors::filtered(
            tips,
            commit::ancestors::State::default(),
            move |oid, buf| db.find_commit_iter(oid, buf).ok().map(|t| t.0),
            predicate,
        )
    }

    fn check_filtered_traversal_with_shared_reference(
        tips: &[&str],
        expected: &[&str],
        predicate: impl FnMut(&oid) -> bool,
    ) -> crate::Result {
        let tips: Vec<_> = tips.iter().copied().map(hex_to_id).collect();
        let oids: Result<Vec<_>, _> = filtered_iter(tips.iter().cloned(), predicate).collect();
        let expected: Vec<_> = tips
            .into_iter()
            .chain(expected.iter().map(|hex_id| hex_to_id(hex_id)))
            .collect();
        assert_eq!(oids?, expected);
        Ok(())
    }

    fn new_iter(
        tips: impl IntoIterator<Item = impl Into<ObjectId>>,
        mode: commit::Parents,
    ) -> impl Iterator<Item = Result<ObjectId, commit::ancestors::Error>> {
        let db = db().expect("db instantiation works as its definitely valid");
        commit::Ancestors::new(tips, commit::ancestors::State::default(), move |oid, buf| {
            db.find_commit_iter(oid, buf).ok().map(|t| t.0)
        })
        .mode(mode)
    }

    fn check_traversal(tips: &[&str], expected: &[&str]) -> crate::Result {
        check_traversal_with_mode(tips, expected, Default::default())
    }

    fn check_traversal_with_mode(tips: &[&str], expected: &[&str], mode: commit::Parents) -> crate::Result {
        let tips: Vec<_> = tips.iter().copied().map(hex_to_id).collect();
        let oids: Result<Vec<_>, _> = new_iter(tips.iter().cloned(), mode).collect();
        let expected: Vec<_> = tips
            .into_iter()
            .chain(expected.iter().map(|hex_id| hex_to_id(hex_id)))
            .collect();
        assert_eq!(oids?, expected);
        Ok(())
    }

    #[test]
    fn instantiate_with_arc() -> crate::Result {
        let _ = new_iter(vec![git_hash::ObjectId::null_sha1()], Default::default());
        Ok(())
    }

    #[test]
    fn instantiate_with_box() -> crate::Result {
        let _ = new_iter(vec![git_hash::ObjectId::null_sha1()], Default::default());
        Ok(())
    }

    #[test]
    fn linear_history_no_branch() -> crate::Result {
        check_traversal(
            &["9556057aee5abb06912922e9f26c46386a816822"],
            &[
                "17d78c64cef6c33a10a604573fd2c429e477fd63",
                "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7",
                "134385f6d781b7e97062102c6a483440bfda2a03",
            ],
        )
    }

    #[test]
    fn simple_branch_with_merge() -> crate::Result {
        check_traversal(
            &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"],
            &[
                "efd9a841189668f1bab5b8ebade9cd0a1b139a37",
                "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353",
                "9556057aee5abb06912922e9f26c46386a816822",
                "9152eeee2328073cf23dcf8e90c949170b711659",
                "17d78c64cef6c33a10a604573fd2c429e477fd63",
                "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7",
                "134385f6d781b7e97062102c6a483440bfda2a03",
            ],
        )
    }

    #[test]
    fn simple_branch_first_parent_only() -> crate::Result {
        check_traversal_with_mode(
            &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"],
            &[
                "efd9a841189668f1bab5b8ebade9cd0a1b139a37",
                "9556057aee5abb06912922e9f26c46386a816822",
                "17d78c64cef6c33a10a604573fd2c429e477fd63",
                "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7",
                "134385f6d781b7e97062102c6a483440bfda2a03",
            ],
            commit::Parents::First,
        )
    }

    #[test]
    fn multiple_tips() -> crate::Result {
        check_traversal(
            &[
                "01ec18a3ebf2855708ad3c9d244306bc1fae3e9b",
                "9556057aee5abb06912922e9f26c46386a816822",
            ],
            &[
                "efd9a841189668f1bab5b8ebade9cd0a1b139a37",
                "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353",
                "17d78c64cef6c33a10a604573fd2c429e477fd63",
                "9152eeee2328073cf23dcf8e90c949170b711659",
                "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7",
                "134385f6d781b7e97062102c6a483440bfda2a03",
            ],
        )
    }

    #[test]
    fn filtered_commit_does_not_block_ancestors_reachable_from_another_commit() -> crate::Result {
        // I don't see a use case for the predicate returning false for a commit but return true for
        // at least one of its ancestors, so this test is kind of dubious. But we do want
        // `Ancestors` to not eagerly blacklist all of a commit's ancestors when blacklisting that
        // one commit, and this test happens to check that.
        check_filtered_traversal_with_shared_reference(
            &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"],
            &[
                "efd9a841189668f1bab5b8ebade9cd0a1b139a37",
                "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353",
                "9556057aee5abb06912922e9f26c46386a816822",
                "17d78c64cef6c33a10a604573fd2c429e477fd63",
                "9902e3c3e8f0c569b4ab295ddf473e6de763e1e7",
                "134385f6d781b7e97062102c6a483440bfda2a03",
            ],
            |id| id != hex_to_id("9152eeee2328073cf23dcf8e90c949170b711659"),
        )
    }

    #[test]
    fn predicate_only_called_once_even_if_fork_point() -> crate::Result {
        // The `self.seen` check should come before the `self.predicate` check, as we don't know how
        // expensive calling `self.predicate` may be.
        let mut seen = false;
        check_filtered_traversal_with_shared_reference(
            &["01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"],
            &[
                "efd9a841189668f1bab5b8ebade9cd0a1b139a37",
                "ce2e8ffaa9608a26f7b21afc1db89cadb54fd353",
                "9152eeee2328073cf23dcf8e90c949170b711659",
            ],
            move |id| {
                if id == hex_to_id("9556057aee5abb06912922e9f26c46386a816822") {
                    assert!(!seen);
                    seen = true;
                    false
                } else {
                    true
                }
            },
        )
    }
}
