/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::ops::Deref;
use std::str::FromStr;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Error;
use fbinit::FacebookInit;
use fixtures::ManyFilesDirs;
use fixtures::TestRepoFixture;
use maplit::btreeset;
use pretty_assertions::assert_eq;
use tests_utils::CreateCommitContext;

use crate::repo::Repo;
use crate::ChangesetDiffItem;
use crate::ChangesetFileOrdering;
use crate::ChangesetPathDiffContext;
use crate::CoreContext;
use crate::HgChangesetId;
use crate::Mononoke;
use crate::MononokePath;

#[fbinit::test]
async fn test_diff_with_moves(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let repo: Repo = test_repo_factory::build_empty(fb).await?;
    let root = CreateCommitContext::new_root(&ctx, &repo)
        .add_file("file_to_move", "context1")
        .commit()
        .await?;

    let commit_with_move = CreateCommitContext::new(&ctx, &repo, vec![root])
        .add_file_with_copy_info("file_moved", "context", (root, "file_to_move"))
        .delete_file("file_to_move")
        .commit()
        .await?;

    let mononoke = Mononoke::new_test(vec![("test".to_string(), repo)]).await?;

    let repo = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let commit_with_move_ctx = repo
        .changeset(commit_with_move)
        .await?
        .ok_or_else(|| anyhow!("commit not found"))?;
    let diff = commit_with_move_ctx
        .diff_unordered(
            &repo.changeset(root).await?.context("commit not found")?,
            true, /* include_copies_renames */
            None, /* path_restrictions */
            btreeset! {ChangesetDiffItem::FILES},
        )
        .await?;

    assert_eq!(diff.len(), 1);
    match diff.get(0) {
        Some(ChangesetPathDiffContext::Moved(to, from)) => {
            assert_eq!(to.path(), &MononokePath::try_from("file_moved")?);
            assert_eq!(from.path(), &MononokePath::try_from("file_to_move")?);
        }
        _ => {
            panic!("unexpected diff");
        }
    }
    Ok(())
}

#[fbinit::test]
async fn test_diff_with_multiple_copies(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let repo: Repo = test_repo_factory::build_empty(fb).await?;
    let root = CreateCommitContext::new_root(&ctx, &repo)
        .add_file("file_to_copy", "context1")
        .commit()
        .await?;

    let commit_with_copies = CreateCommitContext::new(&ctx, &repo, vec![root])
        .add_file_with_copy_info("copy_one", "context", (root, "file_to_copy"))
        .add_file_with_copy_info("copy_two", "context", (root, "file_to_copy"))
        .commit()
        .await?;

    let mononoke = Mononoke::new_test(vec![("test".to_string(), repo)]).await?;

    let repo = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let commit_with_copies_ctx = repo
        .changeset(commit_with_copies)
        .await?
        .ok_or_else(|| anyhow!("commit not found"))?;
    let diff = commit_with_copies_ctx
        .diff_unordered(
            &repo.changeset(root).await?.context("commit not found")?,
            true, /* include_copies_renames */
            None, /* path_restrictions */
            btreeset! {ChangesetDiffItem::FILES},
        )
        .await?;

    assert_eq!(diff.len(), 2);
    match diff.get(0) {
        Some(ChangesetPathDiffContext::Copied(to, from)) => {
            assert_eq!(to.path(), &MononokePath::try_from("copy_one")?);
            assert_eq!(from.path(), &MononokePath::try_from("file_to_copy")?);
        }
        other => panic!("unexpected diff: {:?}", other),
    }
    match diff.get(1) {
        Some(ChangesetPathDiffContext::Copied(to, from)) => {
            assert_eq!(to.path(), &MononokePath::try_from("copy_two")?);
            assert_eq!(from.path(), &MononokePath::try_from("file_to_copy")?);
        }
        other => panic!("unexpected diff: {:?}", other),
    }
    Ok(())
}

#[fbinit::test]
async fn test_diff_with_multiple_moves(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let repo: Repo = test_repo_factory::build_empty(fb).await?;
    let root = CreateCommitContext::new_root(&ctx, &repo)
        .add_file("file_to_move", "context1")
        .commit()
        .await?;

    let commit_with_moves = CreateCommitContext::new(&ctx, &repo, vec![root])
        .add_file_with_copy_info("copy_one", "context", (root, "file_to_move"))
        .add_file_with_copy_info("copy_two", "context", (root, "file_to_move"))
        .add_file_with_copy_info("copy_zzz", "context", (root, "file_to_move"))
        .delete_file("file_to_move")
        .commit()
        .await?;

    let mononoke = Mononoke::new_test(vec![("test".to_string(), repo)]).await?;

    let repo = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let commit_with_moves_ctx = repo
        .changeset(commit_with_moves)
        .await?
        .ok_or_else(|| anyhow!("commit not found"))?;
    let diff = commit_with_moves_ctx
        .diff_unordered(
            &repo.changeset(root).await?.context("commit not found")?,
            true, /* include_copies_renames */
            None, /* path_restrictions */
            btreeset! {ChangesetDiffItem::FILES},
        )
        .await?;

    assert_eq!(diff.len(), 3);
    // The first copy of the file becomes a move.
    match diff.get(0) {
        Some(ChangesetPathDiffContext::Moved(to, from)) => {
            assert_eq!(to.path(), &MononokePath::try_from("copy_one")?);
            assert_eq!(from.path(), &MononokePath::try_from("file_to_move")?);
        }
        other => panic!("unexpected diff: {:?}", other),
    }
    match diff.get(1) {
        Some(ChangesetPathDiffContext::Copied(to, from)) => {
            assert_eq!(to.path(), &MononokePath::try_from("copy_two")?);
            assert_eq!(from.path(), &MononokePath::try_from("file_to_move")?);
        }
        other => panic!("unexpected diff: {:?}", other),
    }
    match diff.get(2) {
        Some(ChangesetPathDiffContext::Copied(to, from)) => {
            assert_eq!(to.path(), &MononokePath::try_from("copy_zzz")?);
            assert_eq!(from.path(), &MononokePath::try_from("file_to_move")?);
        }
        other => panic!("unexpected diff: {:?}", other),
    }
    Ok(())
}

fn check_root_dir_diff(diff: Option<&ChangesetPathDiffContext>) -> Result<(), Error> {
    match diff {
        Some(ChangesetPathDiffContext::Changed(path1, path2)) if path1.path() == path2.path() => {
            assert_eq!(path1.path(), &MononokePath::try_from("")?);
        }
        _ => {
            panic!("unexpected root dir diff")
        }
    }
    Ok(())
}
#[fbinit::test]
async fn test_diff_with_dirs(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        ManyFilesDirs::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx, "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;

    // Case one: dirs added
    let cs_id = HgChangesetId::from_str("d261bc7900818dea7c86935b3fb17a33b2e3a6b4")?;
    let cs = repo.changeset(cs_id).await?.expect("changeset exists");
    let other_cs_id = HgChangesetId::from_str("5a28e25f924a5d209b82ce0713d8d83e68982bc8")?;
    let other_cs = repo
        .changeset(other_cs_id)
        .await?
        .expect("other changeset exists");

    let diff: Vec<_> = cs
        .diff_unordered(&other_cs, false, None, btreeset! {ChangesetDiffItem::TREES})
        .await?;
    assert_eq!(diff.len(), 6);
    check_root_dir_diff(diff.get(0))?;
    match diff.get(1) {
        Some(ChangesetPathDiffContext::Added(path)) => {
            assert_eq!(path.path(), &MononokePath::try_from("dir2")?);
        }
        _ => {
            panic!("unexpected diff");
        }
    }

    // Case two: dir (with subdirs) replaced with file
    let cs_id = HgChangesetId::from_str("051946ed218061e925fb120dac02634f9ad40ae2")?;
    let cs = repo.changeset(cs_id).await?.expect("changeset exists");
    let other_cs_id = HgChangesetId::from_str("d261bc7900818dea7c86935b3fb17a33b2e3a6b4")?;
    let other_cs = repo
        .changeset(other_cs_id)
        .await?
        .expect("other changeset exists");

    // Added
    let diff: Vec<_> = cs
        .diff_unordered(&other_cs, false, None, btreeset! {ChangesetDiffItem::TREES})
        .await?;
    assert_eq!(diff.len(), 5);
    check_root_dir_diff(diff.get(0))?;
    match diff.get(1) {
        Some(ChangesetPathDiffContext::Removed(path)) => {
            assert_eq!(path.path(), &MononokePath::try_from("dir1")?);
        }
        _ => {
            panic!("unexpected diff");
        }
    }

    Ok(())
}

fn check_diff_paths(diff_ctxs: &[ChangesetPathDiffContext], paths: &[&str]) {
    let diff_paths = diff_ctxs
        .iter()
        .map(|diff_ctx| match diff_ctx {
            ChangesetPathDiffContext::Added(file) | ChangesetPathDiffContext::Removed(file) => {
                file.path().to_string()
            }
            ChangesetPathDiffContext::Changed(to, from) => {
                assert_eq!(
                    from.path(),
                    to.path(),
                    "paths for changed file do not match"
                );
                to.path().to_string()
            }
            ChangesetPathDiffContext::Copied(to, from)
            | ChangesetPathDiffContext::Moved(to, from) => {
                assert_ne!(
                    from.path(),
                    to.path(),
                    "paths for copied or moved file should not match"
                );
                // Use the destination path, as this is where it should appear
                // in the ordering.
                to.path().to_string()
            }
        })
        .collect::<Vec<_>>();
    assert_eq!(diff_paths, paths,);
}

#[fbinit::test]
async fn test_ordered_diff(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let repo: Repo = test_repo_factory::build_empty(fb).await?;
    let root = CreateCommitContext::new_root(&ctx, &repo)
        .add_file("root", "root")
        .commit()
        .await?;

    // List of file names to test in repo order.  Note in particular that
    // "j.txt" is after "j/k" even though "." is before "/" in lexicographic
    // order, as we sort based on the directory name ("j").
    let file_list = [
        "!", "0", "1", "10", "2", "a/a/a/a", "a/a/a/b", "d/e", "d/g", "i", "j/k", "j.txt", "p",
        "r/s/t/u", "r/v", "r/w/x", "r/y", "z", "Ã©",
    ];

    let mut commit = CreateCommitContext::new(&ctx, &repo, vec![root]);
    for file in file_list.iter() {
        commit = commit.add_file(*file, *file);
    }
    let commit = commit.commit().await?;

    let mononoke = Mononoke::new_test(vec![("test".to_string(), repo.clone())]).await?;

    let repo_ctx = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let commit_ctx = repo_ctx
        .changeset(commit)
        .await?
        .ok_or_else(|| anyhow!("commit not found"))?;
    let root_ctx = &repo_ctx
        .changeset(root)
        .await?
        .context("commit not found")?;
    let diff = commit_ctx
        .diff(
            root_ctx,
            false, /* include_copies_renames */
            None,  /* path_restrictions */
            btreeset! {ChangesetDiffItem::FILES},
            ChangesetFileOrdering::Ordered { after: None },
            None,
        )
        .await?;

    check_diff_paths(&diff, &file_list);

    // Test limits and continuation.
    let diff = commit_ctx
        .diff(
            root_ctx,
            false, /* include_copies_renames */
            None,  /* path_restrictions */
            btreeset! {ChangesetDiffItem::FILES},
            ChangesetFileOrdering::Ordered { after: None },
            Some(8),
        )
        .await?;
    check_diff_paths(&diff, &file_list[..8]);
    let diff = commit_ctx
        .diff(
            root_ctx,
            false, /* include_copies_renames */
            None,  /* path_restrictions */
            btreeset! {ChangesetDiffItem::FILES},
            ChangesetFileOrdering::Ordered {
                after: Some(file_list[7].try_into()?),
            },
            Some(8),
        )
        .await?;
    check_diff_paths(&diff, &file_list[8..16]);
    let diff = commit_ctx
        .diff(
            root_ctx,
            false, /* include_copies_renames */
            None,  /* path_restrictions */
            btreeset! {ChangesetDiffItem::FILES},
            ChangesetFileOrdering::Ordered {
                after: Some(file_list[15].try_into()?),
            },
            Some(8),
        )
        .await?;
    check_diff_paths(&diff, &file_list[16..]);

    let mod_file_list = [
        "1", "d/e", "d/g", "i/ii", "j/k", "j.txt", "p", "r/v", "r/w/y", "z",
    ];
    let del_file_list = ["10", "i", "r/w/x", "r/y"];

    let mut all_file_list = mod_file_list
        .iter()
        .chain(del_file_list.iter())
        .map(Deref::deref)
        .collect::<Vec<_>>();
    all_file_list.sort_unstable();

    let mut commit2 = CreateCommitContext::new(&ctx, &repo, vec![commit]);
    for file in mod_file_list.iter() {
        commit2 = commit2.add_file(*file, "modified");
    }
    commit2 = commit2
        .add_file_with_copy_info("d/f", "copied", (commit, "d/g"))
        .add_file_with_copy_info("q/y", "moved", (commit, "r/y"));
    for file in del_file_list.iter() {
        commit2 = commit2.delete_file(*file);
    }
    let commit2 = commit2.commit().await?;

    let commit2_ctx = repo_ctx
        .changeset(commit2)
        .await?
        .ok_or_else(|| anyhow!("commit not found"))?;
    let diff = commit2_ctx
        .diff(
            &commit_ctx,
            true, /* include_copies_renames */
            None, /* path_restrictions */
            btreeset! {ChangesetDiffItem::FILES},
            ChangesetFileOrdering::Ordered { after: None },
            None,
        )
        .await?;

    let all_file_list = [
        "1", "10", "d/e", "d/f", "d/g", "i", "i/ii", "j/k", "j.txt", "p", "q/y", "r/v", "r/w/x",
        "r/w/y", "z",
    ];
    check_diff_paths(&diff, &all_file_list);

    // Diff including trees.
    let diff = commit2_ctx
        .diff(
            &commit_ctx,
            true, /* include_copies_renames */
            None, /* path_restrictions */
            btreeset! {ChangesetDiffItem::FILES, ChangesetDiffItem::TREES},
            ChangesetFileOrdering::Ordered { after: None },
            None,
        )
        .await?;

    // "i" is listed twice as a file that is deleted and a tree that is added
    let all_file_and_dir_list = [
        "", "1", "10", "d", "d/e", "d/f", "d/g", "i", "i", "i/ii", "j", "j/k", "j.txt", "p", "q",
        "q/y", "r", "r/v", "r/w", "r/w/x", "r/w/y", "z",
    ];
    check_diff_paths(&diff, &all_file_and_dir_list);

    // Diff over two commits of trees.
    let diff = commit2_ctx
        .diff(
            root_ctx,
            false, /* include_copies_renames */
            None,  /* path_restrictions */
            btreeset! {ChangesetDiffItem::TREES},
            ChangesetFileOrdering::Ordered { after: None },
            None,
        )
        .await?;

    let all_changed_dirs_list = [
        "", "a", "a/a", "a/a/a", "d", "i", "j", "q", "r", "r/s", "r/s/t", "r/w",
    ];
    check_diff_paths(&diff, &all_changed_dirs_list);

    // Diff over two commits, filtered by prefix and with a limit.
    let path_restrictions = Some(vec![
        "1".try_into()?,
        "a/a".try_into()?,
        "q".try_into()?,
        "r/s".try_into()?,
    ]);
    let diff = commit2_ctx
        .diff(
            root_ctx,
            false, /* include_copies_renames */
            path_restrictions.clone(),
            btreeset! {ChangesetDiffItem::FILES},
            ChangesetFileOrdering::Ordered { after: None },
            Some(3),
        )
        .await?;

    let filtered_changed_files_list = ["1", "a/a/a/a", "a/a/a/b", "q/y", "r/s/t/u"];
    check_diff_paths(&diff, &filtered_changed_files_list[..3]);

    let diff = commit2_ctx
        .diff(
            root_ctx,
            false, /* include_copies_renames */
            path_restrictions,
            btreeset! {ChangesetDiffItem::FILES},
            ChangesetFileOrdering::Ordered {
                after: Some(filtered_changed_files_list[2].try_into()?),
            },
            Some(3),
        )
        .await?;

    check_diff_paths(&diff, &filtered_changed_files_list[3..]);
    Ok(())
}

#[fbinit::test]
async fn test_ordered_root_diff(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let repo: Repo = test_repo_factory::build_empty(fb).await?;

    // List of file names to test in repo order.  Note in particular that
    // "j.txt" is after "j/k" even though "." is before "/" in lexicographic
    // order, as we sort based on the directory name ("j").
    let file_list = [
        "!", "0", "1", "10", "2", "a/a/a/a", "a/a/a/b", "d/e", "d/g", "i", "j/k", "j.txt", "p",
        "r/s/t/u", "r/v", "r/w/x", "r/y", "z", "Ã©",
    ];

    let mut root = CreateCommitContext::new_root(&ctx, &repo);

    for file in file_list.iter() {
        root = root.add_file(*file, *file);
    }
    let commit = root.commit().await?;

    let mononoke = Mononoke::new_test(vec![("test".to_string(), repo.clone())]).await?;

    let repo_ctx = mononoke
        .repo(ctx.clone(), "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let commit_ctx = repo_ctx
        .changeset(commit)
        .await?
        .ok_or_else(|| anyhow!("commit not found"))?;

    let diff = commit_ctx
        .diff_root(
            None, /* path_restrictions */
            btreeset! {ChangesetDiffItem::FILES},
            ChangesetFileOrdering::Ordered { after: None },
            None, /* limit */
        )
        .await?;
    check_diff_paths(&diff, &file_list);

    // Test limits and continuation.
    let diff = commit_ctx
        .diff_root(
            None, /* path_restrictions */
            btreeset! {ChangesetDiffItem::FILES},
            ChangesetFileOrdering::Ordered { after: None },
            Some(8),
        )
        .await?;
    check_diff_paths(&diff, &file_list[..8]);

    let diff = commit_ctx
        .diff_root(
            None, /* path_restrictions */
            btreeset! {ChangesetDiffItem::FILES},
            ChangesetFileOrdering::Ordered {
                after: Some(file_list[7].try_into()?),
            },
            Some(8),
        )
        .await?;
    check_diff_paths(&diff, &file_list[8..16]);

    let diff = commit_ctx
        .diff_root(
            None, /* path_restrictions */
            btreeset! {ChangesetDiffItem::FILES},
            ChangesetFileOrdering::Ordered {
                after: Some(file_list[15].try_into()?),
            },
            Some(8),
        )
        .await?;
    check_diff_paths(&diff, &file_list[16..]);

    let path_restrictions = Some(vec![
        "1".try_into()?,
        "a/a".try_into()?,
        "q".try_into()?,
        "r/s".try_into()?,
    ]);
    let diff = commit_ctx
        .diff_root(
            path_restrictions.clone(),
            btreeset! {ChangesetDiffItem::FILES},
            ChangesetFileOrdering::Ordered { after: None },
            Some(3),
        )
        .await?;

    let filtered_changed_files_list = ["1", "a/a/a/a", "a/a/a/b", "q/y", "r/s/t/u"];
    check_diff_paths(&diff, &filtered_changed_files_list[..3]);

    let diff = commit_ctx
        .diff_root(
            None, /* path_restrictions */
            btreeset! {ChangesetDiffItem::FILES, ChangesetDiffItem::TREES},
            ChangesetFileOrdering::Ordered { after: None },
            None, /* limit */
        )
        .await?;

    let files_dirs_list = [
        "!", "0", "1", "10", "2", "a", "a/a", "a/a/a", "a/a/a/a", "a/a/a/b", "d", "d/e", "d/g",
        "i", "j", "j/k", "j.txt", "p", "r", "r/s", "r/s/t", "r/s/t/u", "r/v", "r/w", "r/w/x",
        "r/y", "z", "Ã©",
    ];
    check_diff_paths(&diff, &files_dirs_list);

    let diff = commit_ctx
        .diff_root(
            None, /* path_restrictions */
            btreeset! {ChangesetDiffItem::TREES},
            ChangesetFileOrdering::Ordered { after: None },
            None, /* limit */
        )
        .await?;

    let dirs_list = ["a", "a/a", "a/a/a", "d", "j", "r", "r/s", "r/s/t", "r/w"];
    check_diff_paths(&diff, &dirs_list);

    // a non-root commit2
    let commit2 = CreateCommitContext::new(&ctx, &repo, vec![commit])
        .add_file("second_file", "second_file")
        .delete_file("!")
        .delete_file("0")
        .delete_file("j/k")
        .commit()
        .await?;

    // commit2
    let commit2_ctx = repo_ctx
        .changeset(commit2)
        .await?
        .ok_or_else(|| anyhow!("commit not found"))?;

    let diff = commit2_ctx
        .diff_root(
            None, /* path_restrictions */
            btreeset! {ChangesetDiffItem::FILES},
            ChangesetFileOrdering::Ordered { after: None },
            None,
        )
        .await?;

    let second_commit_files_list = [
        "1",
        "10",
        "2",
        "a/a/a/a",
        "a/a/a/b",
        "d/e",
        "d/g",
        "i",
        "j.txt",
        "p",
        "r/s/t/u",
        "r/v",
        "r/w/x",
        "r/y",
        "second_file",
        "z",
        "Ã©",
    ];
    check_diff_paths(&diff, &second_commit_files_list);

    Ok(())
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::BTreeMap;
use std::sync::Arc;

use anyhow::Error;
use anyhow::Result;
use fbinit::FacebookInit;
use tests_utils::drawdag::changes;
use tests_utils::drawdag::create_from_dag_with_changes;

use crate::headerless_unified_diff;
use crate::ChangesetId;
use crate::CoreContext;
use crate::RepoContext;

async fn init_repo(ctx: &CoreContext) -> Result<(RepoContext, BTreeMap<String, ChangesetId>)> {
    let repo = test_repo_factory::build_empty(ctx.fb).await?;
    let changesets = create_from_dag_with_changes(
        ctx,
        &repo,
        r##"
            A-B-C
        "##,
        changes! {
            "B" => |c| c.add_file("file", "test\nbefore\ndata\n").add_file("bin", "bin\0\x01"),
            "C" => |c| c.add_file("file", "test\nafter\ndata\n").add_file("bin", "bin\0\x02"),
        },
    )
    .await?;

    let repo_ctx = RepoContext::new_test(ctx.clone(), Arc::new(repo)).await?;
    Ok((repo_ctx, changesets))
}

#[fbinit::test]
async fn file_diff(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let (repo, changesets) = init_repo(&ctx).await?;

    let b = repo
        .changeset(changesets["B"])
        .await?
        .expect("changeset should exist");
    let c = repo
        .changeset(changesets["C"])
        .await?
        .expect("changeset should exist");

    // Compare two regular files
    let b_file = b
        .path_with_content("file")
        .await?
        .file()
        .await?
        .expect("should be a file");
    let c_file = c
        .path_with_content("file")
        .await?
        .file()
        .await?
        .expect("should be a file");
    let diff = headerless_unified_diff(&b_file, &c_file, 3).await?;
    assert_eq!(
        std::str::from_utf8(&diff.raw_diff)?,
        concat!(
            "@@ -1,3 +1,3 @@\n",
            " test\n",
            "-before\n",
            "+after\n",
            " data\n"
        )
    );
    assert!(!diff.is_binary);

    // Compare two binary files
    let b_bin = b
        .path_with_content("bin")
        .await?
        .file()
        .await?
        .expect("should be a file");
    let c_bin = c
        .path_with_content("bin")
        .await?
        .file()
        .await?
        .expect("should be a file");
    let diff = headerless_unified_diff(&b_bin, &c_bin, 3).await?;
    assert_eq!(std::str::from_utf8(&diff.raw_diff)?, "Binary files differ");
    assert!(diff.is_binary);
    Ok(())
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::io::Write;
use std::sync::Arc;

use anyhow::Result;
use blobstore::Blobstore;
use fbinit::FacebookInit;
use filestore::hash_bytes;
use filestore::Sha1IncrementalHasher;
use git_hash::ObjectId;
use git_object::Tag;
use git_object::WriteTo;

use crate::repo::git::GitError;
use crate::CoreContext;
use crate::RepoContext;

async fn init_repo(ctx: &CoreContext) -> Result<RepoContext> {
    let repo = test_repo_factory::build_empty(ctx.fb).await?;
    let repo_context = RepoContext::new_test(ctx.clone(), Arc::new(repo)).await?;
    Ok(repo_context)
}

/// upload_git_object tests

#[fbinit::test]
/// Validate the basic git upload object functionality works.
async fn basic_upload_git_object(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let repo_ctx = init_repo(&ctx).await?;
    let tag = Tag {
        target: ObjectId::empty_tree(git_hash::Kind::Sha1),
        target_kind: git_object::Kind::Tree,
        name: "TreeTag".into(),
        tagger: None,
        message: "Tag pointing to a tree".into(),
        pgp_signature: None,
    };
    let mut bytes = tag.loose_header().into_vec();
    tag.write_to(bytes.by_ref())?;

    let bytes_to_hash = bytes::Bytes::from(bytes.clone());
    let sha1_hash = hash_bytes(Sha1IncrementalHasher::new(), &bytes_to_hash);
    let output = repo_ctx
        .upload_git_object(git_hash::oid::try_from_bytes(sha1_hash.as_ref())?, bytes)
        .await;
    output.expect("Expected git object to be uploaded successfully");
    Ok(())
}

#[fbinit::test]
/// Validate that we get an error while trying to upload a git blob through this method.
async fn blob_upload_git_object(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let repo_ctx = init_repo(&ctx).await?;
    let blob = git_object::Blob {
        data: "Some file content".as_bytes().to_vec(),
    };
    let mut bytes = blob.loose_header().into_vec();
    blob.write_to(bytes.by_ref())?;
    let bytes_to_hash = bytes::Bytes::from(bytes.clone());
    let sha1_hash = hash_bytes(Sha1IncrementalHasher::new(), &bytes_to_hash);
    let output = repo_ctx
        .upload_git_object(git_hash::oid::try_from_bytes(sha1_hash.as_ref())?, bytes)
        .await;
    assert!(matches!(
        output.expect_err("Expected error during git object upload"),
        GitError::DisallowedBlobObject(_)
    ));
    Ok(())
}

#[fbinit::test]
/// Validate that we get an error while trying to upload invalid git bytes with this method.
async fn invalid_bytes_upload_git_object(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let repo_ctx = init_repo(&ctx).await?;
    let tag = Tag {
        target: ObjectId::empty_tree(git_hash::Kind::Sha1),
        target_kind: git_object::Kind::Tree,
        name: "TreeTag".into(),
        tagger: None,
        message: "Tag pointing to a tree".into(),
        pgp_signature: None,
    };
    let mut bytes = Vec::new();
    tag.write_to(bytes.by_ref())?;

    let bytes_to_hash = bytes::Bytes::from(bytes.clone());
    let sha1_hash = hash_bytes(Sha1IncrementalHasher::new(), &bytes_to_hash);
    let output = repo_ctx
        .upload_git_object(git_hash::oid::try_from_bytes(sha1_hash.as_ref())?, bytes)
        .await;
    assert!(matches!(
        output.expect_err("Expected error during git object upload"),
        GitError::InvalidContent(..)
    ));
    Ok(())
}

#[fbinit::test]
/// Validate that we get an error while trying to upload a git object with incorrect hash.
async fn invalid_hash_upload_git_object(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let repo_ctx = init_repo(&ctx).await?;
    let tag = Tag {
        target: ObjectId::empty_tree(git_hash::Kind::Sha1),
        target_kind: git_object::Kind::Tree,
        name: "TreeTag".into(),
        tagger: None,
        message: "Tag pointing to a tree".into(),
        pgp_signature: None,
    };
    let mut bytes = tag.loose_header().into_vec();
    tag.write_to(bytes.by_ref())?;

    let output = repo_ctx
        .upload_git_object(&ObjectId::empty_tree(git_hash::Kind::Sha1), bytes)
        .await;
    assert!(matches!(
        output.expect_err("Expected error during git object upload"),
        GitError::HashMismatch(..)
    ));
    Ok(())
}

#[fbinit::test]
/// Validate that the git object stored in the blobstore is stored under the right key.
async fn blobstore_check_upload_git_object(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let repo_ctx = init_repo(&ctx).await?;
    let tag = Tag {
        target: ObjectId::empty_tree(git_hash::Kind::Sha1),
        target_kind: git_object::Kind::Tree,
        name: "TreeTag".into(),
        tagger: None,
        message: "Tag pointing to a tree".into(),
        pgp_signature: None,
    };
    let mut bytes = tag.loose_header().into_vec();
    tag.write_to(bytes.by_ref())?;

    let bytes_to_hash = bytes::Bytes::from(bytes.clone());
    let sha1_hash = hash_bytes(Sha1IncrementalHasher::new(), &bytes_to_hash);
    let blobstore_key = format!("git_object.{}", sha1_hash.to_hex());
    repo_ctx
        .upload_git_object(git_hash::oid::try_from_bytes(sha1_hash.as_ref())?, bytes)
        .await?;
    let output = repo_ctx.repo_blobstore().get(&ctx, &blobstore_key).await?;
    output.expect("Expected git object to be uploaded successfully");
    Ok(())
}

/// create_git_tree tests

#[fbinit::test]
/// Validate the basic create git tree functionality works.
async fn basic_create_git_tree(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let repo_ctx = init_repo(&ctx).await?;

    let tree = git_object::Tree { entries: vec![] };
    let mut bytes = tree.loose_header().into_vec();
    tree.write_to(bytes.by_ref())?;

    let bytes_to_hash = bytes::Bytes::from(bytes.clone());
    let sha1_hash = hash_bytes(Sha1IncrementalHasher::new(), &bytes_to_hash);
    let git_tree_hash = git_hash::oid::try_from_bytes(sha1_hash.as_ref())?;
    repo_ctx.upload_git_object(git_tree_hash, bytes).await?;

    let output = repo_ctx.create_git_tree(git_tree_hash).await;
    output.expect("Expected git tree to be created successfully");
    Ok(())
}

#[fbinit::test]
/// Validate the create git tree method fails when the tree doesn't exist in Mononoke store.
async fn invalid_create_git_tree(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let repo_ctx = init_repo(&ctx).await?;
    let git_tree_hash = git_hash::ObjectId::empty_tree(git_hash::Kind::Sha1);

    let output = repo_ctx.create_git_tree(&git_tree_hash).await;
    assert!(matches!(
        output.expect_err("Expected error during create git tree"),
        GitError::NonExistentObject(..)
    ));
    Ok(())
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;

use anyhow::Result;
use commit_graph::CommitGraph;
use commit_graph::CommitGraphRef;
use commit_graph_testlib::utils::assert_topological_order;
use context::CoreContext;
use fbinit::FacebookInit;
use futures::stream::TryStreamExt;
use maplit::hashset;
use mononoke_types::DateTime;
use tests_utils::CreateCommitContext;

use crate::ChangesetHistoryOptions;
use crate::ChangesetId;
use crate::ChangesetPathHistoryOptions;
use crate::RepoContext;

// Generates this commit graph:
//
// @ "c2"
// |
// o   "m2"
// |\
// | o "e3"
// | |
// | o "b3"
// | |
// o | "e2"
// | |
// o | "a4"
// |/
// o "c1"
// |
// o "e1"
// |
// o   "m1"
// |\
// o | "b2"
// | |
// o | "b1"
//   |
//   o "a3"
//   |
//   o "a2"
//   |
//   o "a1"
//
// Commits e1, e2 and e3 are empty (contain no file changes).
async fn init_repo(ctx: &CoreContext) -> Result<(RepoContext, HashMap<&'static str, ChangesetId>)> {
    let repo = test_repo_factory::build_empty(ctx.fb).await?;
    let mut changesets = HashMap::new();

    changesets.insert(
        "a1",
        CreateCommitContext::new_root(ctx, &repo)
            .add_file("a", "1")
            .set_author_date(DateTime::from_timestamp(1000, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "a2",
        CreateCommitContext::new(ctx, &repo, vec![changesets["a1"]])
            .add_file("a", "2")
            .add_file("dir1/a", "2")
            .set_author_date(DateTime::from_timestamp(2000, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "a3",
        CreateCommitContext::new(ctx, &repo, vec![changesets["a2"]])
            .add_file("a", "3")
            .add_file("dir1/a", "3")
            .add_file("dir3/a", "3")
            .set_author_date(DateTime::from_timestamp(3000, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "b1",
        CreateCommitContext::new_root(ctx, &repo)
            .add_file("b", "1")
            .add_file("dir2/b", "1")
            .set_author_date(DateTime::from_timestamp(1500, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "b2",
        CreateCommitContext::new(ctx, &repo, vec![changesets["b1"]])
            .add_file("b", "2")
            .add_file("dir2/b", "2")
            .add_file("dir3/b", "2")
            .set_author_date(DateTime::from_timestamp(2500, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "m1",
        CreateCommitContext::new(ctx, &repo, vec![changesets["b2"], changesets["a3"]])
            .add_file("a", "3")
            .add_file("dir1/a", "3")
            .add_file("dir3/a", "3")
            .add_file("b", "2")
            .add_file("dir2/b", "2")
            .add_file("dir3/b", "2")
            .set_author_date(DateTime::from_timestamp(4000, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "e1",
        CreateCommitContext::new(ctx, &repo, vec![changesets["m1"]])
            .set_author_date(DateTime::from_timestamp(5000, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "c1",
        CreateCommitContext::new(ctx, &repo, vec![changesets["e1"]])
            .add_file("c", "1")
            .add_file("dir3/c", "1")
            .set_author_date(DateTime::from_timestamp(6000, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "a4",
        CreateCommitContext::new(ctx, &repo, vec![changesets["c1"]])
            .add_file("a", "4")
            .add_file("dir1/a", "4")
            .add_file("dir3/a", "4")
            .set_author_date(DateTime::from_timestamp(7000, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "e2",
        CreateCommitContext::new(ctx, &repo, vec![changesets["a4"]])
            .set_author_date(DateTime::from_timestamp(8000, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "b3",
        CreateCommitContext::new(ctx, &repo, vec![changesets["c1"]])
            .add_file("b", "3")
            .add_file("dir2/b", "3")
            .add_file("dir3/b", "3")
            .set_author_date(DateTime::from_timestamp(7500, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "e3",
        CreateCommitContext::new(ctx, &repo, vec![changesets["b3"]])
            .set_author_date(DateTime::from_timestamp(8500, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "m2",
        CreateCommitContext::new(ctx, &repo, vec![changesets["e2"], changesets["e3"]])
            .add_file("a", "4")
            .add_file("dir1/a", "4")
            .add_file("dir3/a", "4")
            .add_file("b", "3")
            .add_file("dir2/b", "3")
            .add_file("dir3/b", "3")
            .set_author_date(DateTime::from_timestamp(9000, 0)?)
            .commit()
            .await?,
    );
    changesets.insert(
        "c2",
        CreateCommitContext::new(ctx, &repo, vec![changesets["m2"]])
            .add_file("c", "2")
            .add_file("dir3/c", "2")
            .set_author_date(DateTime::from_timestamp(10000, 0)?)
            .commit()
            .await?,
    );

    let repo_ctx = RepoContext::new_test(ctx.clone(), Arc::new(repo)).await?;
    Ok((repo_ctx, changesets))
}

#[fbinit::test]
async fn commit_path_history(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let (repo, changesets) = init_repo(&ctx).await?;

    let cs = repo
        .changeset(changesets["c2"])
        .await?
        .expect("changeset exists");

    // History of file "a" includes commits that modified "a".
    let a_path = cs.path_with_history("a").await?;
    let a_history: Vec<_> = a_path
        .history(ChangesetPathHistoryOptions {
            follow_history_across_deletions: true,
            ..Default::default()
        })
        .await?
        .and_then(|cs| async move { Ok(cs.id()) })
        .try_collect()
        .await?;
    assert_eq!(
        a_history,
        vec![
            changesets["a4"],
            changesets["m1"],
            changesets["a3"],
            changesets["a2"],
            changesets["a1"],
        ]
    );

    // History of directory "dir2" includes commits that modified "dir2/b".
    let dir2_path = cs.path_with_history("dir2").await?;
    let dir2_history: Vec<_> = dir2_path
        .history(ChangesetPathHistoryOptions {
            follow_history_across_deletions: true,
            ..Default::default()
        })
        .await?
        .and_then(|cs| async move { Ok(cs.id()) })
        .try_collect()
        .await?;
    assert_eq!(
        dir2_history,
        vec![
            changesets["b3"],
            changesets["m1"],
            changesets["b2"],
            changesets["b1"],
        ]
    );

    // History of directory "dir3" includes some commits on all branches.
    let dir3_path = cs.path_with_history("dir3").await?;
    let dir3_history: Vec<_> = dir3_path
        .history(ChangesetPathHistoryOptions {
            follow_history_across_deletions: true,
            ..Default::default()
        })
        .await?
        .and_then(|cs| async move { Ok(cs.id()) })
        .try_collect()
        .await?;
    assert_eq!(
        dir3_history,
        vec![
            changesets["c2"],
            changesets["m2"],
            changesets["a4"],
            changesets["b3"],
            changesets["c1"],
            changesets["m1"],
            changesets["a3"],
            changesets["b2"],
        ]
    );

    // Root path history includes all commits except the empty ones.
    let root_path = cs.path_with_history("").await?;
    let root_history: Vec<_> = root_path
        .history(ChangesetPathHistoryOptions {
            follow_history_across_deletions: true,
            ..Default::default()
        })
        .await?
        .and_then(|cs| async move { Ok(cs.id()) })
        .try_collect()
        .await?;
    assert_eq!(
        root_history,
        vec![
            changesets["c2"],
            changesets["m2"],
            changesets["a4"],
            changesets["b3"],
            changesets["c1"],
            changesets["m1"],
            changesets["a3"],
            changesets["a2"],
            changesets["b2"],
            changesets["a1"],
            changesets["b1"],
        ]
    );

    // Setting until_timestamp omits some commits.
    let a_history_with_time_filter: Vec<_> = a_path
        .history(ChangesetPathHistoryOptions {
            until_timestamp: Some(2500),
            follow_history_across_deletions: true,
            ..Default::default()
        })
        .await?
        .and_then(|cs| async move { Ok(cs.id()) })
        .try_collect()
        .await?;
    assert_eq!(
        a_history_with_time_filter,
        vec![changesets["a4"], changesets["m1"], changesets["a3"],]
    );

    // Setting descendants_of omits more commits.
    let a_history_with_descendants_of: Vec<_> = a_path
        .history(ChangesetPathHistoryOptions {
            descendants_of: Some(changesets["b1"]),
            follow_history_across_deletions: true,
            ..Default::default()
        })
        .await?
        .and_then(|cs| async move { Ok(cs.id()) })
        .try_collect()
        .await?;
    assert_eq!(
        a_history_with_descendants_of,
        vec![changesets["a4"], changesets["m1"]]
    );

    let a_history_with_exclusion: Vec<_> = a_path
        .history(ChangesetPathHistoryOptions {
            exclude_changeset_and_ancestors: Some(changesets["a3"]),
            follow_history_across_deletions: true,
            ..Default::default()
        })
        .await?
        .and_then(|cs| async move { Ok(cs.id()) })
        .try_collect()
        .await?;
    assert_eq!(
        a_history_with_exclusion,
        vec![changesets["a4"], changesets["m1"]]
    );

    let a_history_with_exclusion: Vec<_> = a_path
        .history(ChangesetPathHistoryOptions {
            exclude_changeset_and_ancestors: Some(changesets["b1"]),
            until_timestamp: Some(2500),
            follow_history_across_deletions: true,
            ..Default::default()
        })
        .await?
        .and_then(|cs| async move { Ok(cs.id()) })
        .try_collect()
        .await?;
    assert_eq!(
        a_history_with_exclusion,
        vec![changesets["a4"], changesets["m1"], changesets["a3"]]
    );

    Ok(())
}

async fn assert_history(
    ctx: &CoreContext,
    commit_graph: &CommitGraph,
    history: Vec<ChangesetId>,
    expected_cs_ids: HashSet<ChangesetId>,
) -> Result<()> {
    let history = history.into_iter().rev().collect();
    assert_topological_order(commit_graph, ctx, &history).await?;
    assert_eq!(history.into_iter().collect::<HashSet<_>>(), expected_cs_ids);

    Ok(())
}

#[fbinit::test]
async fn commit_history(fb: FacebookInit) -> Result<()> {
    let ctx = CoreContext::test_mock(fb);
    let (repo, changesets) = init_repo(&ctx).await?;

    let cs = repo
        .changeset(changesets["c2"])
        .await?
        .expect("changeset exists");

    // The commit history includes all commits, including empty ones.
    let history: Vec<_> = cs
        .history(Default::default())
        .await?
        .and_then(|cs| async move { Ok(cs.id()) })
        .try_collect()
        .await?;
    assert_history(
        &ctx,
        repo.repo().commit_graph(),
        history,
        hashset! {
            changesets["c2"],
            changesets["m2"],
            changesets["e2"],
            changesets["e3"],
            changesets["a4"],
            changesets["b3"],
            changesets["c1"],
            changesets["e1"],
            changesets["m1"],
            changesets["b2"],
            changesets["a3"],
            changesets["b1"],
            changesets["a2"],
            changesets["a1"],
        },
    )
    .await?;

    // The commit history of an empty commit starts with itself.
    let cs = repo
        .changeset(changesets["e1"])
        .await?
        .expect("changeset exists");
    let history: Vec<_> = cs
        .history(Default::default())
        .await?
        .and_then(|cs| async move { Ok(cs.id()) })
        .try_collect()
        .await?;
    assert_history(
        &ctx,
        repo.repo().commit_graph(),
        history,
        hashset! {
            changesets["e1"],
            changesets["m1"],
            changesets["b2"],
            changesets["a3"],
            changesets["b1"],
            changesets["a2"],
            changesets["a1"],
        },
    )
    .await?;

    // Setting until_timestamp omits some commits.
    let history: Vec<_> = cs
        .history(ChangesetHistoryOptions {
            until_timestamp: Some(2500),
            ..Default::default()
        })
        .await?
        .and_then(|cs| async move { Ok(cs.id()) })
        .try_collect()
        .await?;
    assert_history(
        &ctx,
        repo.repo().commit_graph(),
        history,
        hashset! {
            changesets["e1"],
            changesets["m1"],
            changesets["b2"],
            changesets["a3"],
        },
    )
    .await?;

    // Setting descendendants_of omits some commits.
    let cs = repo
        .changeset(changesets["c2"])
        .await?
        .expect("changeset exists");
    let history: Vec<_> = cs
        .history(ChangesetHistoryOptions {
            descendants_of: Some(changesets["b2"]),
            ..Default::default()
        })
        .await?
        .and_then(|cs| async move { Ok(cs.id()) })
        .try_collect()
        .await?;
    assert_history(
        &ctx,
        repo.repo().commit_graph(),
        history,
        hashset! {
            changesets["c2"],
            changesets["m2"],
            changesets["e2"],
            changesets["e3"],
            changesets["a4"],
            changesets["b3"],
            changesets["c1"],
            changesets["e1"],
            changesets["m1"],
            changesets["b2"],
        },
    )
    .await?;

    // Setting exclude_changeset omits some commits.
    let cs = repo
        .changeset(changesets["c2"])
        .await?
        .expect("changeset exists");
    let history: Vec<_> = cs
        .history(ChangesetHistoryOptions {
            until_timestamp: Some(2500),
            exclude_changeset_and_ancestors: Some(changesets["b2"]),
            ..Default::default()
        })
        .await?
        .and_then(|cs| async move { Ok(cs.id()) })
        .try_collect()
        .await?;
    assert_history(
        &ctx,
        repo.repo().commit_graph(),
        history,
        hashset! {
            changesets["c2"],
            changesets["m2"],
            changesets["e2"],
            changesets["e3"],
            changesets["a4"],
            changesets["b3"],
            changesets["c1"],
            changesets["e1"],
            changesets["m1"],
            changesets["a3"],
        },
    )
    .await?;

    let cs = repo
        .changeset(changesets["m2"])
        .await?
        .expect("changeset exists");
    let history: Vec<_> = cs
        .history(ChangesetHistoryOptions {
            exclude_changeset_and_ancestors: Some(changesets["c2"]),
            ..Default::default()
        })
        .await?
        .and_then(|cs| async move { Ok(cs.id()) })
        .try_collect()
        .await?;
    assert_history(&ctx, repo.repo().commit_graph(), history, hashset! {}).await?;

    // Setting both descendendants_of and exclude_changeset_and_ancestors
    // lets us filter out the descendant.
    let cs = repo
        .changeset(changesets["c2"])
        .await?
        .expect("changeset exists");
    let history: Vec<_> = cs
        .history(ChangesetHistoryOptions {
            descendants_of: Some(changesets["b2"]),
            exclude_changeset_and_ancestors: Some(changesets["b2"]),
            ..Default::default()
        })
        .await?
        .and_then(|cs| async move { Ok(cs.id()) })
        .try_collect()
        .await?;
    assert_history(
        &ctx,
        repo.repo().commit_graph(),
        history,
        hashset! {
            changesets["c2"],
            changesets["m2"],
            changesets["e2"],
            changesets["e3"],
            changesets["a4"],
            changesets["b3"],
            changesets["c1"],
            changesets["e1"],
            changesets["m1"],
        },
    )
    .await?;

    Ok(())
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;

use anyhow::anyhow;
use anyhow::Error;
use anyhow::Result;
use blobrepo::AsBlobRepo;
use blobstore::Loadable;
use bookmarks::BookmarkKey;
use bytes::Bytes;
use cacheblob::InProcessLease;
use chrono::FixedOffset;
use chrono::TimeZone;
use cross_repo_sync::update_mapping_with_version;
use cross_repo_sync::CommitSyncRepos;
use cross_repo_sync::CommitSyncer;
use cross_repo_sync_test_utils::init_small_large_repo;
use fbinit::FacebookInit;
use fixtures::BranchUneven;
use fixtures::Linear;
use fixtures::ManyFilesDirs;
use fixtures::TestRepoFixture;
use futures::stream::TryStreamExt;
use futures::FutureExt;
use live_commit_sync_config::TestLiveCommitSyncConfigSource;
use maplit::hashmap;
use metaconfig_types::CommitSyncConfigVersion;
use metaconfig_types::DefaultSmallToLargeCommitSyncPathAction;
use mononoke_types::hash::Blake3;
use mononoke_types::hash::GitSha1;
use mononoke_types::hash::RichGitSha1;
use mononoke_types::hash::Sha1;
use mononoke_types::hash::Sha256;
use mononoke_types::MPath;
use repo_blobstore::RepoBlobstoreRef;
use repo_identity::RepoIdentityRef;
use slog::info;
use synced_commit_mapping::ArcSyncedCommitMapping;
use tests_utils::bookmark;
use tests_utils::resolve_cs_id;
use tests_utils::CreateCommitContext;
use tunables::tunables;
use tunables::with_tunables_async;
use tunables::MononokeTunables;

use crate::BookmarkFreshness;
use crate::ChangesetFileOrdering;
use crate::ChangesetId;
use crate::ChangesetIdPrefix;
use crate::ChangesetPrefixSpecifier;
use crate::ChangesetSpecifier;
use crate::ChangesetSpecifierPrefixResolution;
use crate::CoreContext;
use crate::FileId;
use crate::FileMetadata;
use crate::FileType;
use crate::HgChangesetId;
use crate::HgChangesetIdPrefix;
use crate::Mononoke;
use crate::MononokePath;
use crate::TreeEntry;
use crate::TreeId;

#[fbinit::test]
async fn commit_info_by_hash(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        Linear::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx, "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let hash = "7785606eb1f26ff5722c831de402350cf97052dc44bc175da6ac0d715a3dbbf6";
    let cs_id = ChangesetId::from_str(hash)?;
    let cs = repo.changeset(cs_id).await?.expect("changeset exists");

    assert_eq!(cs.message().await?, "modified 10");
    assert_eq!(cs.author().await?, "Jeremy Fitzhardinge <jsgf@fb.com>");
    assert_eq!(
        cs.author_date().await?,
        FixedOffset::west_opt(7 * 3600)
            .unwrap()
            .timestamp_opt(1504041761, 0)
            .unwrap()
    );
    assert_eq!(cs.generation().await?.value(), 11);

    Ok(())
}

#[fbinit::test]
async fn commit_info_by_hg_hash(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        Linear::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx, "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let hg_hash = "607314ef579bd2407752361ba1b0c1729d08b281";
    let hg_cs_id = HgChangesetId::from_str(hg_hash)?;
    let cs = repo.changeset(hg_cs_id).await?.expect("changeset exists");

    let hash = "2cb6d2d3052bfbdd6a95a61f2816d81130033b5f5a99e8d8fc24d9238d85bb48";
    assert_eq!(cs.id(), ChangesetId::from_str(hash)?);
    assert_eq!(cs.hg_id().await?, Some(HgChangesetId::from_str(hg_hash)?));
    assert_eq!(cs.message().await?, "added 3");
    assert_eq!(cs.author().await?, "Jeremy Fitzhardinge <jsgf@fb.com>");
    assert_eq!(
        cs.author_date().await?,
        FixedOffset::west_opt(7 * 3600)
            .unwrap()
            .timestamp_opt(1504041758, 0)
            .unwrap()
    );

    Ok(())
}

#[fbinit::test]
async fn commit_info_by_bookmark(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        Linear::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx, "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let cs = repo
        .resolve_bookmark(&BookmarkKey::new("master")?, BookmarkFreshness::MostRecent)
        .await?
        .expect("bookmark exists");

    let hash = "7785606eb1f26ff5722c831de402350cf97052dc44bc175da6ac0d715a3dbbf6";
    assert_eq!(cs.id(), ChangesetId::from_str(hash)?);
    let hg_hash = "79a13814c5ce7330173ec04d279bf95ab3f652fb";
    assert_eq!(cs.hg_id().await?, Some(HgChangesetId::from_str(hg_hash)?));
    assert_eq!(cs.message().await?, "modified 10");
    assert_eq!(cs.author().await?, "Jeremy Fitzhardinge <jsgf@fb.com>");
    assert_eq!(
        cs.author_date().await?,
        FixedOffset::west_opt(7 * 3600)
            .unwrap()
            .timestamp_opt(1504041761, 0)
            .unwrap()
    );

    Ok(())
}

#[fbinit::test]
async fn commit_hg_changeset_ids(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        Linear::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx, "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let hash1 = "2cb6d2d3052bfbdd6a95a61f2816d81130033b5f5a99e8d8fc24d9238d85bb48";
    let hash2 = "7785606eb1f26ff5722c831de402350cf97052dc44bc175da6ac0d715a3dbbf6";
    let hg_hash1 = "607314ef579bd2407752361ba1b0c1729d08b281";
    let hg_hash2 = "79a13814c5ce7330173ec04d279bf95ab3f652fb";
    let ids: HashMap<_, _> = repo
        .many_changeset_hg_ids(vec![
            ChangesetId::from_str(hash1)?,
            ChangesetId::from_str(hash2)?,
        ])
        .await?
        .into_iter()
        .collect();
    assert_eq!(
        ids.get(&ChangesetId::from_str(hash1)?),
        Some(&HgChangesetId::from_str(hg_hash1)?)
    );
    assert_eq!(
        ids.get(&ChangesetId::from_str(hash2)?),
        Some(&HgChangesetId::from_str(hg_hash2)?)
    );

    Ok(())
}

#[fbinit::test]
async fn commit_is_ancestor_of(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        BranchUneven::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx, "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let mut changesets = Vec::new();
    for hg_hash in [
        "5d43888a3c972fe68c224f93d41b30e9f888df7c", // 0: branch 1 near top
        "d7542c9db7f4c77dab4b315edd328edf1514952f", // 1: branch 1 near bottom
        "1d8a907f7b4bf50c6a09c16361e2205047ecc5e5", // 2: branch 2
        "15c40d0abc36d47fb51c8eaec51ac7aad31f669c", // 3: base
    ] {
        let changeset = repo
            .changeset(HgChangesetId::from_str(hg_hash)?)
            .await
            .expect("changeset exists");
        changesets.push(changeset);
    }
    for (index, base_index, is_ancestor_of) in [
        (0usize, 0usize, true),
        (0, 1, false),
        (0, 2, false),
        (0, 3, false),
        (1, 0, true),
        (1, 1, true),
        (1, 2, false),
        (1, 3, false),
        (2, 0, false),
        (2, 1, false),
        (2, 2, true),
        (2, 3, false),
        (3, 0, true),
        (3, 1, true),
        (3, 2, true),
        (3, 3, true),
    ] {
        assert_eq!(
            changesets[index]
                .as_ref()
                .unwrap()
                .is_ancestor_of(changesets[base_index].as_ref().unwrap().id())
                .await?,
            is_ancestor_of,
            "changesets[{}].is_ancestor_of(changesets[{}].id()) == {}",
            index,
            base_index,
            is_ancestor_of
        );
    }
    Ok(())
}

async fn commit_find_files_impl(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        ManyFilesDirs::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx, "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let hash = "b0d1bf77898839595ee0f0cba673dd6e3be9dadaaa78bc6dd2dea97ca6bee77e";
    let cs_id = ChangesetId::from_str(hash)?;
    let cs = repo.changeset(cs_id).await?.expect("changeset exists");

    // Find everything
    let mut files: Vec<_> = cs
        .find_files_unordered(None, None)
        .await?
        .try_collect()
        .await?;
    files.sort();
    let expected_files = vec![
        MononokePath::try_from("1")?,
        MononokePath::try_from("2")?,
        MononokePath::try_from("dir1/file_1_in_dir1")?,
        MononokePath::try_from("dir1/file_2_in_dir1")?,
        MononokePath::try_from("dir1/subdir1/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir1/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_2")?,
        MononokePath::try_from("dir2/file_1_in_dir2")?,
    ];
    assert_eq!(files, expected_files);

    // Find everything ordered
    let files: Vec<_> = cs
        .find_files(
            None,
            None,
            None,
            ChangesetFileOrdering::Ordered { after: None },
        )
        .await?
        .try_collect()
        .await?;
    assert_eq!(files, expected_files);

    // Find everything after a particular file
    let files: Vec<_> = cs
        .find_files(
            None,
            None,
            None,
            ChangesetFileOrdering::Ordered {
                after: Some(MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?),
            },
        )
        .await?
        .try_collect()
        .await?;
    let expected_files = vec![
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_2")?,
        MononokePath::try_from("dir2/file_1_in_dir2")?,
    ];
    assert_eq!(files, expected_files);

    // Prefixes
    let mut files: Vec<_> = cs
        .find_files_unordered(
            Some(vec![
                MononokePath::try_from("dir1/subdir1/subsubdir1")?,
                MononokePath::try_from("dir2")?,
            ]),
            None,
        )
        .await?
        .try_collect()
        .await?;
    files.sort();
    let expected_files = vec![
        MononokePath::try_from("dir1/subdir1/subsubdir1/file_1")?,
        MononokePath::try_from("dir2/file_1_in_dir2")?,
    ];
    assert_eq!(files, expected_files);

    // Prefixes ordered, starting at the root.  (This has no real effect on
    // the output vs `after: None`, but exercise the code path anyway).
    let files: Vec<_> = cs
        .find_files(
            Some(vec![
                MononokePath::try_from("dir1/subdir1/subsubdir1")?,
                MononokePath::try_from("dir2")?,
            ]),
            None,
            None,
            ChangesetFileOrdering::Ordered {
                after: Some(MononokePath::try_from("")?),
            },
        )
        .await?
        .try_collect()
        .await?;
    assert_eq!(files, expected_files);

    // Prefixes ordered after
    let files: Vec<_> = cs
        .find_files(
            Some(vec![
                MononokePath::try_from("dir1/subdir1/subsubdir1")?,
                MononokePath::try_from("dir2")?,
            ]),
            None,
            None,
            ChangesetFileOrdering::Ordered {
                after: Some(MononokePath::try_from("dir1/subdir1/subsubdir1/file_1")?),
            },
        )
        .await?
        .try_collect()
        .await?;
    let expected_files = vec![MononokePath::try_from("dir2/file_1_in_dir2")?];
    assert_eq!(files, expected_files);

    // Basenames
    let mut files: Vec<_> = cs
        .find_files_unordered(None, Some(vec![String::from("file_1")]))
        .await?
        .try_collect()
        .await?;
    files.sort();
    let expected_files = vec![
        MononokePath::try_from("dir1/subdir1/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir1/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?,
    ];
    assert_eq!(files, expected_files);

    // Basenames ordered
    let files: Vec<_> = cs
        .find_files(
            None,
            Some(vec![String::from("file_1")]),
            None,
            ChangesetFileOrdering::Ordered {
                after: Some(MononokePath::try_from("")?),
            },
        )
        .await?
        .try_collect()
        .await?;
    assert_eq!(files, expected_files);

    // Basenames ordered after
    let files: Vec<_> = cs
        .find_files(
            None,
            Some(vec![String::from("file_1")]),
            None,
            ChangesetFileOrdering::Ordered {
                after: Some(MononokePath::try_from("dir1/subdir1/subsubdir1/file_1")?),
            },
        )
        .await?
        .try_collect()
        .await?;
    let expected_files = vec![MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?];
    assert_eq!(files, expected_files);

    // Basenames and Prefixes
    let mut files: Vec<_> = cs
        .find_files_unordered(
            Some(vec![
                MononokePath::try_from("dir1/subdir1/subsubdir2")?,
                MononokePath::try_from("dir2")?,
            ]),
            Some(vec![String::from("file_2"), String::from("file_1_in_dir2")]),
        )
        .await?
        .try_collect()
        .await?;
    files.sort();
    let expected_files = vec![
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_2")?,
        MononokePath::try_from("dir2/file_1_in_dir2")?,
    ];
    assert_eq!(files, expected_files);

    // Basenames and Prefixes ordered
    let mut files: Vec<_> = cs
        .find_files(
            Some(vec![
                MononokePath::try_from("dir1/subdir1/subsubdir2")?,
                MononokePath::try_from("dir2")?,
            ]),
            Some(vec![String::from("file_2"), String::from("file_1_in_dir2")]),
            None,
            ChangesetFileOrdering::Ordered {
                after: Some(MononokePath::try_from("")?),
            },
        )
        .await?
        .try_collect()
        .await?;
    let expected_files = vec![
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_2")?,
        MononokePath::try_from("dir2/file_1_in_dir2")?,
    ];
    files.sort();
    assert_eq!(files, expected_files);

    // Basenames and Prefixes ordered after
    let files: Vec<_> = cs
        .find_files(
            Some(vec![
                MononokePath::try_from("dir1/subdir1/subsubdir2")?,
                MononokePath::try_from("dir2")?,
            ]),
            Some(vec![String::from("file_2"), String::from("file_1_in_dir2")]),
            None,
            ChangesetFileOrdering::Ordered {
                after: Some(MononokePath::try_from("dir1a/file_1_in_dir2")?),
            },
        )
        .await?
        .try_collect()
        .await?;
    let expected_files = vec![MononokePath::try_from("dir2/file_1_in_dir2")?];
    assert_eq!(files, expected_files);

    // Suffixes
    let mut files: Vec<_> = cs
        .find_files(
            None,
            None,
            Some(vec![String::from("_1"), String::from("_2")]),
            ChangesetFileOrdering::Unordered,
        )
        .await?
        .try_collect()
        .await?;
    files.sort();
    let expected_files = vec![
        MononokePath::try_from("dir1/subdir1/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir1/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_2")?,
    ];
    assert_eq!(files, expected_files);

    // Suffixes, ordered
    let files: Vec<_> = cs
        .find_files(
            None,
            None,
            Some(vec![String::from("_1"), String::from("_2")]),
            ChangesetFileOrdering::Ordered {
                after: Some(MononokePath::try_from("")?),
            },
        )
        .await?
        .try_collect()
        .await?;
    let expected_files = vec![
        MononokePath::try_from("dir1/subdir1/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir1/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_2")?,
    ];
    assert_eq!(files, expected_files);

    // Suffixes, ordered after
    let files: Vec<_> = cs
        .find_files(
            None,
            None,
            Some(vec![String::from("_1"), String::from("_2")]),
            ChangesetFileOrdering::Ordered {
                after: Some(MononokePath::try_from("dir1/subdir1/subsubdir1/file_1")?),
            },
        )
        .await?
        .try_collect()
        .await?;
    let expected_files = vec![
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_2")?,
    ];
    assert_eq!(files, expected_files);

    // Suffixes, prefixes
    let mut files: Vec<_> = cs
        .find_files(
            Some(vec![
                MononokePath::try_from("dir1/subdir1/subsubdir1")?,
                MononokePath::try_from("dir1/subdir1/subsubdir2")?,
            ]),
            None,
            Some(vec![String::from("1"), String::from("2")]),
            ChangesetFileOrdering::Unordered,
        )
        .await?
        .try_collect()
        .await?;
    files.sort();
    let expected_files = vec![
        MononokePath::try_from("dir1/subdir1/subsubdir1/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_2")?,
    ];
    assert_eq!(files, expected_files);

    // Suffixes, prefixes, ordered
    let files: Vec<_> = cs
        .find_files(
            Some(vec![
                MononokePath::try_from("dir1/subdir1/subsubdir1")?,
                MononokePath::try_from("dir1/subdir1/subsubdir2")?,
            ]),
            None,
            Some(vec![String::from("1"), String::from("2")]),
            ChangesetFileOrdering::Ordered {
                after: Some(MononokePath::try_from("")?),
            },
        )
        .await?
        .try_collect()
        .await?;
    let expected_files = vec![
        MononokePath::try_from("dir1/subdir1/subsubdir1/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_2")?,
    ];
    assert_eq!(files, expected_files);

    // Suffixes, prefixes, ordered after
    let files: Vec<_> = cs
        .find_files(
            Some(vec![
                MononokePath::try_from("dir1/subdir1/subsubdir1")?,
                MononokePath::try_from("dir1/subdir1/subsubdir2")?,
            ]),
            None,
            Some(vec![String::from("1"), String::from("2")]),
            ChangesetFileOrdering::Ordered {
                after: Some(MononokePath::try_from("dir1/subdir1/subsubdir1/file_1")?),
            },
        )
        .await?
        .try_collect()
        .await?;
    let expected_files = vec![
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_2")?,
    ];
    assert_eq!(files, expected_files);

    // Suffixes, basenames
    let mut files: Vec<_> = cs
        .find_files(
            None,
            Some(vec![String::from("file_1_in_dir2")]),
            Some(vec![String::from("1")]),
            ChangesetFileOrdering::Unordered,
        )
        .await?
        .try_collect()
        .await?;
    files.sort();
    let expected_files = vec![
        MononokePath::try_from("1")?,
        MononokePath::try_from("dir1/file_1_in_dir1")?,
        MononokePath::try_from("dir1/file_2_in_dir1")?,
        MononokePath::try_from("dir1/subdir1/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir1/file_1")?,
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?,
        MononokePath::try_from("dir2/file_1_in_dir2")?,
    ];
    assert_eq!(files, expected_files);

    // Suffixes, basenames, ordered
    let files: Vec<_> = cs
        .find_files(
            None,
            Some(vec![String::from("file_1_in_dir2")]),
            Some(vec![String::from("1")]),
            ChangesetFileOrdering::Ordered {
                after: Some(MononokePath::try_from("")?),
            },
        )
        .await?
        .try_collect()
        .await?;
    // BSSM have different but consistent orders
    let expected_files = if tunables()
        .disable_basename_suffix_skeleton_manifest()
        .unwrap_or_default()
    {
        vec![
            MononokePath::try_from("1")?,
            MononokePath::try_from("dir1/file_1_in_dir1")?,
            MononokePath::try_from("dir1/file_2_in_dir1")?,
            MononokePath::try_from("dir1/subdir1/file_1")?,
            MononokePath::try_from("dir1/subdir1/subsubdir1/file_1")?,
            MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?,
            MononokePath::try_from("dir2/file_1_in_dir2")?,
        ]
    } else {
        vec![
            MononokePath::try_from("1")?,
            MononokePath::try_from("dir1/subdir1/file_1")?,
            MononokePath::try_from("dir1/subdir1/subsubdir1/file_1")?,
            MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?,
            MononokePath::try_from("dir1/file_1_in_dir1")?,
            MononokePath::try_from("dir1/file_2_in_dir1")?,
            MononokePath::try_from("dir2/file_1_in_dir2")?,
        ]
    };
    assert_eq!(files, expected_files);

    // Suffixes, basenames, ordered after
    let files: Vec<_> = cs
        .find_files(
            None,
            Some(vec![String::from("file_1_in_dir2")]),
            Some(vec![String::from("1")]),
            ChangesetFileOrdering::Ordered {
                after: Some(MononokePath::try_from("dir1/subdir1/subsubdir1/file_1")?),
            },
        )
        .await?
        .try_collect()
        .await?;
    let expected_files = if tunables()
        .disable_basename_suffix_skeleton_manifest()
        .unwrap_or_default()
    {
        vec![
            MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?,
            MononokePath::try_from("dir2/file_1_in_dir2")?,
        ]
    } else {
        vec![
            MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?,
            MononokePath::try_from("dir1/file_1_in_dir1")?,
            MononokePath::try_from("dir1/file_2_in_dir1")?,
            MononokePath::try_from("dir2/file_1_in_dir2")?,
        ]
    };
    assert_eq!(files, expected_files);

    // Suffixes, basenames, prefixes
    let mut files: Vec<_> = cs
        .find_files(
            Some(vec![
                MononokePath::try_from("dir1/subdir1/subsubdir2")?,
                MononokePath::try_from("dir2")?,
            ]),
            Some(vec![String::from("file_1_in_dir2")]),
            Some(vec![String::from("1")]),
            ChangesetFileOrdering::Unordered,
        )
        .await?
        .try_collect()
        .await?;
    files.sort();
    let expected_files = vec![
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?,
        MononokePath::try_from("dir2/file_1_in_dir2")?,
    ];
    assert_eq!(files, expected_files);

    // Suffixes, basenames, prefixes, ordered
    let files: Vec<_> = cs
        .find_files(
            Some(vec![
                MononokePath::try_from("dir1/subdir1/subsubdir2")?,
                MononokePath::try_from("dir2")?,
            ]),
            Some(vec![String::from("file_1_in_dir2")]),
            Some(vec![String::from("1")]),
            ChangesetFileOrdering::Ordered {
                after: Some(MononokePath::try_from("")?),
            },
        )
        .await?
        .try_collect()
        .await?;
    let expected_files = vec![
        MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?,
        MononokePath::try_from("dir2/file_1_in_dir2")?,
    ];
    assert_eq!(files, expected_files);

    // Suffixes, basenames, prefixes, ordered after
    let files: Vec<_> = cs
        .find_files(
            Some(vec![
                MononokePath::try_from("dir1/subdir1/subsubdir2")?,
                MononokePath::try_from("dir2")?,
            ]),
            Some(vec![String::from("file_1_in_dir2")]),
            Some(vec![String::from("1")]),
            ChangesetFileOrdering::Ordered {
                after: Some(MononokePath::try_from("dir1/subdir1/subsubdir2/file_1")?),
            },
        )
        .await?
        .try_collect()
        .await?;
    let expected_files = vec![MononokePath::try_from("dir2/file_1_in_dir2")?];
    assert_eq!(files, expected_files);

    Ok(())
}

#[fbinit::test]
async fn commit_find_files_with_bssm(fb: FacebookInit) {
    let tunables = MononokeTunables::default();
    tunables.update_bools(&hashmap! {
        "enable_bssm_suffix_query".to_string() => true
    });
    with_tunables_async(tunables, commit_find_files_impl(fb).boxed())
        .await
        .unwrap();
}

#[fbinit::test]
async fn commit_find_files_without_bssm(fb: FacebookInit) {
    let tunables = MononokeTunables::default();
    tunables.update_bools(&hashmap! {
        "disable_basename_suffix_skeleton_manifest".to_string() => true
    });
    with_tunables_async(tunables, commit_find_files_impl(fb).boxed())
        .await
        .unwrap();
}

#[fbinit::test]
async fn commit_path_exists_and_type(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        ManyFilesDirs::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx, "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let hash = "b0d1bf77898839595ee0f0cba673dd6e3be9dadaaa78bc6dd2dea97ca6bee77e";
    let cs_id = ChangesetId::from_str(hash)?;
    let cs = repo.changeset(cs_id).await?.expect("changeset exists");

    let root_path = cs.root().await?;
    assert!(root_path.exists().await?);
    assert!(root_path.is_tree().await?);

    let dir1_path = cs.path_with_content("dir1").await?;
    assert!(dir1_path.exists().await?);
    assert!(dir1_path.is_tree().await?);
    assert_eq!(dir1_path.file_type().await?, None);

    let file1_path = cs.path_with_content("dir1/file_1_in_dir1").await?;
    assert!(file1_path.exists().await?);
    assert!(!file1_path.is_tree().await?);
    assert_eq!(file1_path.file_type().await?, Some(FileType::Regular));

    let nonexistent_path = cs.path_with_content("nonexistent").await?;
    assert!(!nonexistent_path.exists().await?);
    assert!(!nonexistent_path.is_tree().await?);
    assert_eq!(nonexistent_path.file_type().await?, None);

    Ok(())
}

#[fbinit::test]
async fn tree_list(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        ManyFilesDirs::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx, "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let hash = "b0d1bf77898839595ee0f0cba673dd6e3be9dadaaa78bc6dd2dea97ca6bee77e";
    let cs_id = ChangesetId::from_str(hash)?;
    let cs = repo.changeset(cs_id).await?.expect("changeset exists");
    assert_eq!(
        {
            let path = cs.root().await?;
            let tree = path.tree().await?.unwrap();
            tree.list()
                .await?
                .map(|(name, _entry)| name)
                .collect::<Vec<_>>()
        },
        vec![
            String::from("1"),
            String::from("2"),
            String::from("dir1"),
            String::from("dir2")
        ]
    );
    assert_eq!(
        {
            let path = cs.path_with_content("dir1").await?;
            let tree = path.tree().await?.unwrap();
            tree.list()
                .await?
                .map(|(name, _entry)| name)
                .collect::<Vec<_>>()
        },
        vec![
            String::from("file_1_in_dir1"),
            String::from("file_2_in_dir1"),
            String::from("subdir1"),
        ]
    );
    let subsubdir2_id = {
        // List `dir1/subdir1`, but also capture a subtree id.
        let path = cs.path_with_content("dir1/subdir1").await?;
        let tree = path.tree().await?.unwrap();
        assert_eq!(
            {
                tree.list()
                    .await?
                    .map(|(name, _entry)| name)
                    .collect::<Vec<_>>()
            },
            vec![
                String::from("file_1"),
                String::from("subsubdir1"),
                String::from("subsubdir2")
            ]
        );
        match tree
            .list()
            .await?
            .collect::<HashMap<_, _>>()
            .get("subsubdir2")
            .expect("entry should exist for subsubdir2")
        {
            TreeEntry::Directory(dir) => dir.id().clone(),
            entry => panic!("subsubdir2 entry should be a directory, not {:?}", entry),
        }
    };
    assert_eq!(
        {
            let path = cs.path_with_content("dir1/subdir1/subsubdir1").await?;
            let tree = path.tree().await?.unwrap();
            tree.list()
                .await?
                .map(|(name, entry)| match entry {
                    TreeEntry::File(file) => {
                        Some((name, file.size(), file.content_sha1().to_string()))
                    }
                    _ => None,
                })
                .collect::<Vec<_>>()
        },
        vec![Some((
            String::from("file_1"),
            9,
            String::from("aa02177d2c1f3af3fb5b7b25698cb37772b1226b")
        ))]
    );
    // Get tree by id
    assert_eq!(
        {
            let tree = repo.tree(subsubdir2_id).await?.expect("tree exists");
            tree.list()
                .await?
                .map(|(name, _entry)| name)
                .collect::<Vec<_>>()
        },
        vec![String::from("file_1"), String::from("file_2")]
    );
    // Get tree by non-existent id returns None.
    assert!(
        repo.tree(TreeId::from_bytes([1; 32]).unwrap())
            .await?
            .is_none()
    );
    // Get tree by non-existent path returns None.
    {
        let path = cs.path_with_content("nonexistent").await?;
        assert!(path.tree().await?.is_none());
    }

    Ok(())
}

#[fbinit::test]
async fn file_metadata(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        ManyFilesDirs::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx, "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;

    let expected_metadata = FileMetadata {
        content_id: FileId::from_str(
            "9d9cf646b38852094ec48ab401eea6f4481cc89a80589331845dc08f75a652d2",
        )?,
        total_size: 9,
        sha1: Sha1::from_str("b29930dda02406077d96a7b7a08ce282b3de6961")?,
        sha256: Sha256::from_str(
            "47d741b6059c6d7e99be25ce46fb9ba099cfd6515de1ef7681f93479d25996a4",
        )?,
        git_sha1: RichGitSha1::from_sha1(
            GitSha1::from_str("ac3e272b72bbf89def8657766b855d0656630ed4")?,
            "blob",
            9,
        ),
        seeded_blake3: Blake3::from_str(
            "bbd41b606802e603ef6eae081bed1f29eb1ffb897f98e670bbd770c3dba8b81d",
        )?,
        is_binary: false,
        is_ascii: true,
        is_utf8: true,
        ends_in_newline: true,
        newline_count: 1,
        first_line: Some(String::from("content1")),
        is_generated: false,
        is_partially_generated: false,
    };

    // Get file by changeset path.
    let hash = "b0d1bf77898839595ee0f0cba673dd6e3be9dadaaa78bc6dd2dea97ca6bee77e";
    let cs_id = ChangesetId::from_str(hash)?;
    let cs = repo.changeset(cs_id).await?.expect("changeset exists");

    let path = cs.path_with_content("dir1/file_1_in_dir1").await?;
    let file = path.file().await?.unwrap();
    let metadata = file.metadata().await?;
    assert_eq!(metadata, expected_metadata);

    // Get file by content id.
    let file = repo
        .file(FileId::from_str(
            "9d9cf646b38852094ec48ab401eea6f4481cc89a80589331845dc08f75a652d2",
        )?)
        .await?
        .expect("file exists");
    let metadata = file.metadata().await?;
    assert_eq!(metadata, expected_metadata);

    // Get file by content sha1.
    let file = repo
        .file_by_content_sha1(Sha1::from_str("b29930dda02406077d96a7b7a08ce282b3de6961")?)
        .await?
        .expect("file exists");
    let metadata = file.metadata().await?;
    assert_eq!(metadata, expected_metadata);

    // Get file by content sha256.
    let file = repo
        .file_by_content_sha256(Sha256::from_str(
            "47d741b6059c6d7e99be25ce46fb9ba099cfd6515de1ef7681f93479d25996a4",
        )?)
        .await?
        .expect("file exists");
    let metadata = file.metadata().await?;
    assert_eq!(metadata, expected_metadata);

    // Get file by content seeded blake3
    let file = repo
        .file_by_content_seeded_blake3(Blake3::from_str(
            "bbd41b606802e603ef6eae081bed1f29eb1ffb897f98e670bbd770c3dba8b81d",
        )?)
        .await?
        .expect("file exists");
    let metadata = file.metadata().await?;
    assert_eq!(metadata, expected_metadata);

    Ok(())
}

#[fbinit::test]
async fn file_contents(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        ManyFilesDirs::get_custom_test_repo(fb).await,
    )])
    .await?;
    let repo = mononoke
        .repo(ctx, "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;

    let hash = "b0d1bf77898839595ee0f0cba673dd6e3be9dadaaa78bc6dd2dea97ca6bee77e";
    let cs_id = ChangesetId::from_str(hash)?;
    let cs = repo.changeset(cs_id).await?.expect("changeset exists");

    let path = cs.path_with_content("dir1/file_1_in_dir1").await?;
    let file = path.file().await?.unwrap();
    let content = file.content_concat().await?;
    assert_eq!(content, Bytes::from("content1\n"));

    let content_range = file.content_range_concat(3, 4).await?;
    assert_eq!(content_range, Bytes::from("tent"));

    Ok(())
}

#[fbinit::test]
async fn xrepo_commit_lookup_simple(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let (mononoke, _cfg_src) = init_x_repo(&ctx).await?;

    let smallrepo = mononoke
        .repo(ctx.clone(), "smallrepo")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let largerepo = mononoke
        .repo(ctx.clone(), "largerepo")
        .await?
        .expect("repo exists")
        .build()
        .await?;

    let small_master_cs_id = resolve_cs_id(&ctx, smallrepo.blob_repo(), "master").await?;

    info!(
        ctx.logger(),
        "remapping {} from small to large", small_master_cs_id
    );
    // Confirm that a cross-repo lookup for an unsynced commit just fails
    let cs = smallrepo
        .xrepo_commit_lookup(&largerepo, small_master_cs_id, None)
        .await?
        .expect("changeset should exist");
    let large_master_cs_id = resolve_cs_id(&ctx, largerepo.blob_repo(), "master").await?;
    assert_eq!(cs.id(), large_master_cs_id);

    info!(
        ctx.logger(),
        "remapping {} from large to small", large_master_cs_id
    );
    let cs = largerepo
        .xrepo_commit_lookup(&smallrepo, large_master_cs_id, None)
        .await?
        .expect("changeset should exist");
    assert_eq!(cs.id(), small_master_cs_id);
    Ok(())
}

#[fbinit::test]
async fn xrepo_commit_lookup_draft(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let (mononoke, _cfg_src) = init_x_repo(&ctx).await?;

    let smallrepo = mononoke
        .repo(ctx.clone(), "smallrepo")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let small_master_cs_id = resolve_cs_id(&ctx, smallrepo.blob_repo(), "master").await?;
    let largerepo = mononoke
        .repo(ctx.clone(), "largerepo")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let large_master_cs_id = resolve_cs_id(&ctx, largerepo.blob_repo(), "master").await?;

    let new_large_draft =
        CreateCommitContext::new(&ctx, largerepo.blob_repo(), vec![large_master_cs_id])
            .add_file("prefix/remapped", "content1")
            .add_file("not_remapped", "content2")
            .commit()
            .await?;

    let cs = largerepo
        .xrepo_commit_lookup(&smallrepo, new_large_draft, None)
        .await?;
    assert!(cs.is_some());
    let bcs = cs
        .unwrap()
        .id()
        .load(&ctx, smallrepo.blob_repo().repo_blobstore())
        .await?;
    let file_changes: Vec<_> = bcs.file_changes().map(|(path, _)| path).cloned().collect();
    assert_eq!(file_changes, vec![MPath::new("remapped")?]);

    // Now in another direction
    let new_small_draft =
        CreateCommitContext::new(&ctx, smallrepo.blob_repo(), vec![small_master_cs_id])
            .add_file("remapped2", "content2")
            .commit()
            .await?;
    let cs = smallrepo
        .xrepo_commit_lookup(&largerepo, new_small_draft, None)
        .await?;
    assert!(cs.is_some());
    let bcs = cs
        .unwrap()
        .id()
        .load(&ctx, largerepo.blob_repo().repo_blobstore())
        .await?;
    let file_changes: Vec<_> = bcs.file_changes().map(|(path, _)| path).cloned().collect();
    assert_eq!(file_changes, vec![MPath::new("prefix/remapped2")?]);

    Ok(())
}

#[fbinit::test]
async fn xrepo_commit_lookup_public(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let (mononoke, _cfg_src) = init_x_repo(&ctx).await?;

    let smallrepo = mononoke
        .repo(ctx.clone(), "smallrepo")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let small_master_cs_id = resolve_cs_id(&ctx, smallrepo.blob_repo(), "master").await?;
    let largerepo = mononoke
        .repo(ctx.clone(), "largerepo")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let large_master_cs_id = resolve_cs_id(&ctx, largerepo.blob_repo(), "master").await?;

    let new_large_public =
        CreateCommitContext::new(&ctx, largerepo.blob_repo(), vec![large_master_cs_id])
            .add_file("prefix/remapped", "content1")
            .add_file("not_remapped", "content2")
            .commit()
            .await?;

    bookmark(&ctx, largerepo.blob_repo(), "publicbook")
        .set_to(new_large_public)
        .await?;

    let cs = largerepo
        .xrepo_commit_lookup(&smallrepo, new_large_public, None)
        .await?;
    assert!(cs.is_some());
    let bcs = cs
        .unwrap()
        .id()
        .load(&ctx, smallrepo.blob_repo().repo_blobstore())
        .await?;
    let file_changes: Vec<_> = bcs.file_changes().map(|(path, _)| path).cloned().collect();
    assert_eq!(file_changes, vec![MPath::new("remapped")?]);

    // Now in another direction - it should fail
    let new_small_public =
        CreateCommitContext::new(&ctx, smallrepo.blob_repo(), vec![small_master_cs_id])
            .add_file("remapped2", "content2")
            .commit()
            .await?;
    bookmark(&ctx, smallrepo.blob_repo(), "newsmallpublicbook")
        .set_to(new_small_public)
        .await?;
    let res = smallrepo
        .xrepo_commit_lookup(&largerepo, new_small_public, None)
        .await;
    assert!(res.is_err());

    Ok(())
}

#[fbinit::test]
async fn xrepo_commit_lookup_config_changing_live(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let (mononoke, cfg_src) = init_x_repo(&ctx).await?;

    let smallrepo = mononoke
        .repo(ctx.clone(), "smallrepo")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let largerepo = mononoke
        .repo(ctx.clone(), "largerepo")
        .await?
        .expect("repo exists")
        .build()
        .await?;
    let small_master_cs_id = resolve_cs_id(&ctx, smallrepo.blob_repo(), "master").await?;
    let large_master_cs_id = resolve_cs_id(&ctx, largerepo.blob_repo(), "master").await?;

    // Before config change
    let first_large =
        CreateCommitContext::new(&ctx, largerepo.blob_repo(), vec![large_master_cs_id])
            .add_file("prefix/remapped_before", "content1")
            .add_file("not_remapped", "content2")
            .commit()
            .await?;

    let first_small = largerepo
        .xrepo_commit_lookup(&smallrepo, first_large, None)
        .await?;
    let file_changes: Vec<_> = first_small
        .unwrap()
        .id()
        .load(&ctx, smallrepo.blob_repo().repo_blobstore())
        .await?
        .file_changes()
        .map(|(path, _)| path)
        .cloned()
        .collect();

    assert_eq!(file_changes, vec![MPath::new("remapped_before")?]);

    // Config change: new config remaps prefix2 instead of prefix
    let large_repo_id = largerepo.blob_repo().repo_identity().id();
    let small_repo_id = smallrepo.blob_repo().repo_identity().id();
    let mut cfg = cfg_src
        .get_commit_sync_config_by_version_if_exists(
            large_repo_id,
            &CommitSyncConfigVersion("TEST_VERSION_NAME".to_string()),
        )?
        .unwrap();
    let common_config = cfg_src
        .get_common_config_if_exists(large_repo_id)?
        .ok_or_else(|| anyhow!("common config doesn't exist"))?;
    cfg.small_repos
        .get_mut(&small_repo_id)
        .unwrap()
        .default_action =
        DefaultSmallToLargeCommitSyncPathAction::PrependPrefix(MPath::new("prefix2").unwrap());
    let new_version = CommitSyncConfigVersion("TEST_VERSION_NAME_2".to_string());
    cfg.version_name = new_version.clone();
    cfg_src.add_config(cfg.clone());

    let change_mapping_small =
        CreateCommitContext::new(&ctx, smallrepo.blob_repo(), vec![small_master_cs_id])
            .commit()
            .await?;
    let change_mapping_large =
        CreateCommitContext::new(&ctx, largerepo.blob_repo(), vec![large_master_cs_id])
            .commit()
            .await?;

    let commit_sync_repos = CommitSyncRepos::new(
        largerepo.inner_repo().clone(),
        smallrepo.inner_repo().clone(),
        &common_config,
    )?;

    let commit_syncer = CommitSyncer::new(
        &ctx,
        largerepo.synced_commit_mapping().clone(),
        commit_sync_repos,
        largerepo.live_commit_sync_config(),
        Arc::new(InProcessLease::new()),
    );

    update_mapping_with_version(
        &ctx,
        hashmap! {change_mapping_large => change_mapping_small},
        &commit_syncer,
        &new_version,
    )
    .await?;

    // After config change
    let second_large =
        CreateCommitContext::new(&ctx, largerepo.blob_repo(), vec![change_mapping_large])
            .add_file("prefix2/remapped_after", "content1")
            .add_file("not_remapped", "content2")
            .commit()
            .await?;

    let second_small = largerepo
        .xrepo_commit_lookup(&smallrepo, second_large, None)
        .await?;
    let file_changes: Vec<_> = second_small
        .unwrap()
        .id()
        .load(&ctx, smallrepo.blob_repo().repo_blobstore())
        .await?
        .file_changes()
        .map(|(path, _)| path)
        .cloned()
        .collect();

    assert_eq!(file_changes, vec![MPath::new("remapped_after")?]);
    Ok(())
}

async fn init_x_repo(
    ctx: &CoreContext,
) -> Result<(Mononoke, TestLiveCommitSyncConfigSource), Error> {
    let (syncers, commit_sync_config, lv_cfg, lv_cfg_src) = init_small_large_repo(ctx).await?;

    let small_to_large = syncers.small_to_large;
    let mapping: ArcSyncedCommitMapping = Arc::new(small_to_large.get_mapping().clone());
    let mononoke = Mononoke::new_test_xrepo(
        ctx.clone(),
        (
            "smallrepo".to_string(),
            small_to_large.get_small_repo().as_blob_repo().clone(),
        ),
        (
            "largerepo".to_string(),
            small_to_large.get_large_repo().as_blob_repo().clone(),
        ),
        commit_sync_config.clone(),
        mapping.clone(),
        Arc::new(lv_cfg),
    )
    .await?;
    lv_cfg_src.add_config(commit_sync_config);
    Ok((mononoke, lv_cfg_src))
}

#[fbinit::test]
async fn resolve_changeset_id_prefix(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mononoke = Mononoke::new_test(vec![(
        "test".to_string(),
        Linear::get_custom_test_repo(fb).await,
    )])
    .await?;

    let repo = mononoke
        .repo(ctx, "test")
        .await?
        .expect("repo exists")
        .build()
        .await?;

    let hg_cs_id = ChangesetSpecifier::Hg(HgChangesetId::from_str(
        "607314ef579bd2407752361ba1b0c1729d08b281",
    )?);

    let bonsai_cs_id = ChangesetSpecifier::Bonsai(ChangesetId::from_str(
        "7785606eb1f26ff5722c831de402350cf97052dc44bc175da6ac0d715a3dbbf6",
    )?);

    // test different lengths
    let test_cases: Vec<(_, Vec<ChangesetPrefixSpecifier>)> = vec![
        (
            &hg_cs_id,
            vec![
                HgChangesetIdPrefix::from_str("6073")?.into(),
                HgChangesetIdPrefix::from_str("607314e")?.into(),
                HgChangesetIdPrefix::from_str("607314ef57")?.into(),
                HgChangesetIdPrefix::from_str("607314ef579bd2407752361ba")?.into(),
                HgChangesetIdPrefix::from_str("607314ef579bd2407752361ba1b0c1729d08b281")?.into(),
            ],
        ),
        (
            &bonsai_cs_id,
            vec![
                ChangesetIdPrefix::from_str("7785")?.into(),
                ChangesetIdPrefix::from_str("7785606")?.into(),
                ChangesetIdPrefix::from_str("7785606eb1f26f")?.into(),
                ChangesetIdPrefix::from_str("7785606eb1f26ff5722c831")?.into(),
                ChangesetIdPrefix::from_str(
                    "7785606eb1f26ff5722c831de402350cf97052dc44bc175da6ac0d715a3dbbf6",
                )?
                .into(),
            ],
        ),
    ];

    for (expected, prefixes) in test_cases {
        for prefix in prefixes {
            assert_eq!(
                repo.resolve_changeset_id_prefix(prefix).await?,
                ChangesetSpecifierPrefixResolution::Single(*expected)
            );
        }
    }

    // nonexistent changeset
    assert_eq!(
        ChangesetSpecifierPrefixResolution::NoMatch,
        repo.resolve_changeset_id_prefix(HgChangesetIdPrefix::from_str("607314efffff")?.into())
            .await?
    );

    // invalid hex string
    assert!(HgChangesetIdPrefix::from_str("607314euuuuu").is_err());

    Ok(())
}