/*
 * 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::collections::HashMap;
use std::str::FromStr;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;

use anyhow::anyhow;
use anyhow::Error;
use ascii::AsciiString;
use assert_matches::assert_matches;
use blobrepo::save_bonsai_changesets;
use blobrepo_hg::BlobRepoHg;
use blobstore::Loadable;
use bonsai_hg_mapping::BonsaiHgMappingRef;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateLogArc;
use bookmarks::BookmarkUpdateLogRef;
use bookmarks::BookmarkUpdateReason;
use bookmarks::BookmarksArc;
use bookmarks::BookmarksMaybeStaleExt;
use bookmarks::BookmarksRef;
use bookmarks::Freshness;
use cloned::cloned;
use commit_graph::CommitGraphRef;
use commit_transformation::upload_commits;
use context::CoreContext;
use cross_repo_sync::rewrite_commit;
use cross_repo_sync::CandidateSelectionHint;
use cross_repo_sync::CommitSyncContext;
use cross_repo_sync::CommitSyncDataProvider;
use cross_repo_sync::CommitSyncOutcome;
use cross_repo_sync::CommitSyncRepos;
use cross_repo_sync::CommitSyncer;
use cross_repo_sync::CHANGE_XREPO_MAPPING_EXTRA;
use cross_repo_sync_test_utils::TestRepo;
use fbinit::FacebookInit;
use fixtures::Linear;
use fixtures::TestRepoFixture;
use futures::FutureExt;
use futures::TryFutureExt;
use futures::TryStreamExt;
use futures_ext::FbTryFutureExt;
use live_commit_sync_config::TestLiveCommitSyncConfig;
use manifest::Entry;
use manifest::ManifestOps;
use maplit::btreemap;
use maplit::hashmap;
use mercurial_derivation::DeriveHgChangeset;
use mercurial_types::HgChangesetId;
use metaconfig_types::CommitSyncConfig;
use metaconfig_types::CommitSyncConfigVersion;
use metaconfig_types::CommonCommitSyncConfig;
use metaconfig_types::DefaultSmallToLargeCommitSyncPathAction;
use metaconfig_types::SmallRepoCommitSyncConfig;
use metaconfig_types::SmallRepoPermanentConfig;
use mononoke_types::ChangesetId;
use mononoke_types::MPath;
use mononoke_types::RepositoryId;
use movers::Mover;
use mutable_counters::MutableCountersArc;
use pretty_assertions::assert_eq;
use repo_blobstore::RepoBlobstoreRef;
use repo_identity::RepoIdentityRef;
use sql_construct::SqlConstruct;
use synced_commit_mapping::EquivalentWorkingCopyEntry;
use synced_commit_mapping::SqlSyncedCommitMapping;
use synced_commit_mapping::SyncedCommitMapping;
use synced_commit_mapping::SyncedCommitMappingEntry;
use synced_commit_mapping::SyncedCommitSourceRepo;
use test_repo_factory::TestRepoFactory;
use tests_utils::bookmark;
use tests_utils::create_commit;
use tests_utils::list_working_copy_utf8;
use tests_utils::resolve_cs_id;
use tests_utils::store_files;
use tests_utils::store_rename;
use tests_utils::CreateCommitContext;
use tokio::runtime::Runtime;
use tunables::with_tunables_async;
use wireproto_handler::TargetRepoDbs;

use crate::backsync_latest;
use crate::format_counter;
use crate::sync_entries;
use crate::BacksyncLimit;

const REPOMERGE_FOLDER: &str = "repomerge";
const REPOMERGE_FILE: &str = "repomergefile";
const BRANCHMERGE_FILE: &str = "branchmerge";

#[fbinit::test]
async fn backsync_linear_simple(fb: FacebookInit) -> Result<(), Error> {
    let (commit_syncer, target_repo_dbs) =
        init_repos(fb, MoverType::Noop, BookmarkRenamerType::Noop).await?;
    backsync_and_verify_master_wc(fb, commit_syncer.clone(), target_repo_dbs).await?;

    let ctx = CoreContext::test_mock(fb);
    let target_cs_id = resolve_cs_id(&ctx, commit_syncer.get_target_repo(), "master").await?;

    let map = list_working_copy_utf8(&ctx, commit_syncer.get_target_repo(), target_cs_id).await?;
    assert_eq!(
        map.into_iter().collect::<BTreeMap<_, _>>(),
        btreemap! {
                MPath::new("1")? => "1\n".to_string(),
                MPath::new("2")? => "2\n".to_string(),
                MPath::new("3")? => "3\n".to_string(),
                MPath::new("4")? => "4\n".to_string(),
                MPath::new("5")? => "5\n".to_string(),
                MPath::new("6")? => "6\n".to_string(),
                MPath::new("7")? => "7\n".to_string(),
                MPath::new("8")? => "8\n".to_string(),
                MPath::new("9")? => "9\n".to_string(),
                MPath::new("10")? => "modified10\n".to_string(),
                MPath::new("randomfile")? => "some other content".to_string(),
                MPath::new("files")? => "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n".to_string(),
        }
    );

    let target_cs_id =
        resolve_cs_id(&ctx, commit_syncer.get_target_repo(), "anotherbookmark").await?;
    let map = list_working_copy_utf8(&ctx, commit_syncer.get_target_repo(), target_cs_id).await?;
    assert_eq!(
        map.into_iter().collect::<BTreeMap<_, _>>(),
        btreemap! {
                MPath::new("1")? => "1\n".to_string(),
                MPath::new("2")? => "2\n".to_string(),
                MPath::new("3")? => "merged 3".to_string(),
                MPath::new("4")? => "4\n".to_string(),
                MPath::new("5")? => "5\n".to_string(),
                MPath::new("6")? => "6\n".to_string(),
                MPath::new("7")? => "7\n".to_string(),
                MPath::new("8")? => "8\n".to_string(),
                MPath::new("9")? => "9\n".to_string(),
                MPath::new("10")? => "modified10\n".to_string(),
                MPath::new("files")? => "branchmerge files content".to_string(),
                MPath::new("branchmerge")? => "new branch merge content".to_string(),
                MPath::new("repomergefile")? => "some content".to_string(),
                MPath::new("randomfile")? => "some other content".to_string(),
                MPath::new("repomerge/first")? => "new repo content".to_string(),
                MPath::new("repomerge/movedest")? => "moved content".to_string(),
                MPath::new("repomerge/second")? => "new repo second content".to_string(),
                MPath::new("repomerge/toremove")? => "new repo content".to_string(),

        }
    );

    Ok(())
}

#[fbinit::test]
fn test_sync_entries(fb: FacebookInit) -> Result<(), Error> {
    // Test makes sure sync_entries() actually sync ALL entries even if transaction
    // for updating bookmark and/or counter failed. This transaction failure is benign and
    // expected, it means that two backsyncers doing the same job in parallel

    let runtime = Runtime::new()?;
    runtime.block_on(async move {
        let (commit_syncer, target_repo_dbs) =
            init_repos(fb, MoverType::Noop, BookmarkRenamerType::Noop).await?;

        let target_repo_dbs = Arc::new(target_repo_dbs);
        // Backsync a few entries
        let ctx = CoreContext::test_mock(fb);
        backsync_latest(
            ctx.clone(),
            commit_syncer.clone(),
            target_repo_dbs.clone(),
            BacksyncLimit::Limit(2),
            Arc::new(AtomicBool::new(false)),
            CommitSyncContext::Backsyncer,
            false,
        )
        .map_err(Error::from)
        .await?;

        let source_repo = commit_syncer.get_source_repo();

        let next_log_entries: Vec<_> = source_repo
            .bookmark_update_log()
            .read_next_bookmark_log_entries(ctx.clone(), 0, 1000, Freshness::MostRecent)
            .try_collect()
            .await?;

        // Sync entries starting from counter 0. sync_entries() function should skip
        // 2 first entries, and sync all entries after that
        sync_entries(
            ctx.clone(),
            &commit_syncer,
            target_repo_dbs.clone(),
            next_log_entries.clone(),
            0,
            Arc::new(AtomicBool::new(false)),
            CommitSyncContext::Backsyncer,
            false,
        )
        .await?;

        let latest_log_id = next_log_entries.len() as i64;

        // Make sure all of the entries were synced
        let fetched_value = target_repo_dbs
            .counters
            .get_counter(&ctx, &format_counter(&source_repo.repo_identity().id()))
            .await?;

        assert_eq!(fetched_value, Some(latest_log_id));

        Ok(())
    })
}

#[fbinit::test]
async fn backsync_linear_with_mover_that_removes_some_files(fb: FacebookInit) -> Result<(), Error> {
    let (commit_syncer, target_repo_dbs) = init_repos(
        fb,
        MoverType::Only("files".to_string()),
        BookmarkRenamerType::Noop,
    )
    .await?;

    backsync_and_verify_master_wc(fb, commit_syncer.clone(), target_repo_dbs).await?;
    let ctx = CoreContext::test_mock(fb);
    let target_cs_id = resolve_cs_id(&ctx, commit_syncer.get_target_repo(), "master").await?;

    let map = list_working_copy_utf8(&ctx, commit_syncer.get_target_repo(), target_cs_id).await?;
    assert_eq!(
        map,
        hashmap! {MPath::new("files")? => "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n".to_string()}
    );

    let target_cs_id =
        resolve_cs_id(&ctx, commit_syncer.get_target_repo(), "anotherbookmark").await?;
    let map = list_working_copy_utf8(&ctx, commit_syncer.get_target_repo(), target_cs_id).await?;
    assert_eq!(
        map,
        hashmap! {MPath::new("files")? => "branchmerge files content".to_string()}
    );
    Ok(())
}

#[fbinit::test]
async fn backsync_linear_with_mover_that_removes_single_file(
    fb: FacebookInit,
) -> Result<(), Error> {
    let (commit_syncer, target_repo_dbs) = init_repos(
        fb,
        MoverType::Except(vec!["10".to_string()]),
        BookmarkRenamerType::Noop,
    )
    .await?;

    backsync_and_verify_master_wc(fb, commit_syncer.clone(), target_repo_dbs).await?;

    let ctx = CoreContext::test_mock(fb);
    let target_cs_id = resolve_cs_id(&ctx, commit_syncer.get_target_repo(), "master").await?;

    let map = list_working_copy_utf8(&ctx, commit_syncer.get_target_repo(), target_cs_id).await?;
    assert_eq!(
        map.into_iter().collect::<BTreeMap<_, _>>(),
        btreemap! {
                MPath::new("1")? => "1\n".to_string(),
                MPath::new("2")? => "2\n".to_string(),
                MPath::new("3")? => "3\n".to_string(),
                MPath::new("4")? => "4\n".to_string(),
                MPath::new("5")? => "5\n".to_string(),
                MPath::new("6")? => "6\n".to_string(),
                MPath::new("7")? => "7\n".to_string(),
                MPath::new("8")? => "8\n".to_string(),
                MPath::new("9")? => "9\n".to_string(),
                MPath::new("randomfile")? => "some other content".to_string(),
                MPath::new("files")? => "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n".to_string(),
        }
    );

    let target_cs_id =
        resolve_cs_id(&ctx, commit_syncer.get_target_repo(), "anotherbookmark").await?;
    let map = list_working_copy_utf8(&ctx, commit_syncer.get_target_repo(), target_cs_id).await?;
    assert_eq!(
        map.into_iter().collect::<BTreeMap<_, _>>(),
        btreemap! {
                MPath::new("1")? => "1\n".to_string(),
                MPath::new("2")? => "2\n".to_string(),
                MPath::new("3")? => "merged 3".to_string(),
                MPath::new("4")? => "4\n".to_string(),
                MPath::new("5")? => "5\n".to_string(),
                MPath::new("6")? => "6\n".to_string(),
                MPath::new("7")? => "7\n".to_string(),
                MPath::new("8")? => "8\n".to_string(),
                MPath::new("9")? => "9\n".to_string(),
                MPath::new("files")? => "branchmerge files content".to_string(),
                MPath::new("branchmerge")? => "new branch merge content".to_string(),
                MPath::new("repomergefile")? => "some content".to_string(),
                MPath::new("randomfile")? => "some other content".to_string(),
                MPath::new("repomerge/first")? => "new repo content".to_string(),
                MPath::new("repomerge/movedest")? => "moved content".to_string(),
                MPath::new("repomerge/second")? => "new repo second content".to_string(),
                MPath::new("repomerge/toremove")? => "new repo content".to_string(),

        }
    );

    Ok(())
}

#[fbinit::test]
async fn backsync_linear_bookmark_renamer_only_master(fb: FacebookInit) -> Result<(), Error> {
    let master = BookmarkKey::new("master")?;
    let (commit_syncer, target_repo_dbs) =
        init_repos(fb, MoverType::Noop, BookmarkRenamerType::Only(master)).await?;

    backsync_and_verify_master_wc(fb, commit_syncer.clone(), target_repo_dbs).await?;

    let ctx = CoreContext::test_mock(fb);
    let target_cs_id = resolve_cs_id(&ctx, commit_syncer.get_target_repo(), "master").await?;

    let map = list_working_copy_utf8(&ctx, commit_syncer.get_target_repo(), target_cs_id).await?;
    assert_eq!(
        map.into_iter().collect::<BTreeMap<_, _>>(),
        btreemap! {
                MPath::new("1")? => "1\n".to_string(),
                MPath::new("2")? => "2\n".to_string(),
                MPath::new("3")? => "3\n".to_string(),
                MPath::new("4")? => "4\n".to_string(),
                MPath::new("5")? => "5\n".to_string(),
                MPath::new("6")? => "6\n".to_string(),
                MPath::new("7")? => "7\n".to_string(),
                MPath::new("8")? => "8\n".to_string(),
                MPath::new("9")? => "9\n".to_string(),
                MPath::new("10")? => "modified10\n".to_string(),
                MPath::new("randomfile")? => "some other content".to_string(),
                MPath::new("files")? => "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n".to_string(),
        }
    );

    // Bookmark should be deleted
    assert_eq!(
        commit_syncer
            .get_target_repo()
            .get_bookmark_hg(ctx, &BookmarkKey::new("anotherbookmark")?)
            .await?,
        None
    );

    Ok(())
}

#[fbinit::test]
async fn backsync_linear_bookmark_renamer_remove_all(fb: FacebookInit) -> Result<(), Error> {
    let (commit_syncer, target_repo_dbs) =
        init_repos(fb, MoverType::Noop, BookmarkRenamerType::RemoveAll).await?;

    backsync_and_verify_master_wc(fb, commit_syncer.clone(), target_repo_dbs).await?;

    let ctx = CoreContext::test_mock(fb);
    // Bookmarks should be deleted
    assert_eq!(
        commit_syncer
            .get_target_repo()
            .get_bookmark_hg(ctx.clone(), &BookmarkKey::new("master")?)
            .await?,
        None
    );

    assert_eq!(
        commit_syncer
            .get_target_repo()
            .get_bookmark_hg(ctx, &BookmarkKey::new("anotherbookmark")?)
            .await?,
        None
    );

    Ok(())
}

#[fbinit::test]
async fn backsync_two_small_repos(fb: FacebookInit) -> Result<(), Error> {
    let (small_repos, _large_repo, _latest_log_id, dont_verify_commits) =
        init_merged_repos(fb, 2).await?;

    let ctx = CoreContext::test_mock(fb);

    for (commit_syncer, target_repo_dbs) in small_repos {
        let small_repo_id = commit_syncer.get_target_repo().repo_identity().id();
        println!("backsyncing small repo#{}", small_repo_id.id());
        let target_repo_dbs = Arc::new(target_repo_dbs);
        let small_repo_id = commit_syncer.get_target_repo().repo_identity().id();
        backsync_latest(
            ctx.clone(),
            commit_syncer.clone(),
            target_repo_dbs.clone(),
            BacksyncLimit::NoLimit,
            Arc::new(AtomicBool::new(false)),
            CommitSyncContext::Backsyncer,
            false,
        )
        .map_err(Error::from)
        .await?;

        println!("verifying small repo#{}", small_repo_id.id());
        verify_mapping_and_all_wc(
            ctx.clone(),
            commit_syncer.clone(),
            dont_verify_commits.clone(),
        )
        .await?;
    }

    Ok(())
}

#[fbinit::test]
async fn backsync_merge_new_repo_all_files_removed(fb: FacebookInit) -> Result<(), Error> {
    // Remove all files from new repo except for the file in the merge commit itself
    let (commit_syncer, target_repo_dbs) = init_repos(
        fb,
        MoverType::Except(vec![
            REPOMERGE_FOLDER.to_string(),
            REPOMERGE_FILE.to_string(),
        ]),
        BookmarkRenamerType::Noop,
    )
    .await?;

    backsync_and_verify_master_wc(fb, commit_syncer.clone(), target_repo_dbs).await?;

    let ctx = CoreContext::test_mock(fb);
    let target_cs_id = resolve_cs_id(&ctx, commit_syncer.get_target_repo(), "master").await?;

    let map = list_working_copy_utf8(&ctx, commit_syncer.get_target_repo(), target_cs_id).await?;
    assert_eq!(
        map.into_iter().collect::<BTreeMap<_, _>>(),
        btreemap! {
                MPath::new("1")? => "1\n".to_string(),
                MPath::new("2")? => "2\n".to_string(),
                MPath::new("3")? => "3\n".to_string(),
                MPath::new("4")? => "4\n".to_string(),
                MPath::new("5")? => "5\n".to_string(),
                MPath::new("6")? => "6\n".to_string(),
                MPath::new("7")? => "7\n".to_string(),
                MPath::new("8")? => "8\n".to_string(),
                MPath::new("9")? => "9\n".to_string(),
                MPath::new("10")? => "modified10\n".to_string(),
                MPath::new("randomfile")? => "some other content".to_string(),
                MPath::new("files")? => "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n".to_string(),
        }
    );

    let target_cs_id =
        resolve_cs_id(&ctx, commit_syncer.get_target_repo(), "anotherbookmark").await?;
    let map = list_working_copy_utf8(&ctx, commit_syncer.get_target_repo(), target_cs_id).await?;
    assert_eq!(
        map.into_iter().collect::<BTreeMap<_, _>>(),
        btreemap! {
                MPath::new("1")? => "1\n".to_string(),
                MPath::new("2")? => "2\n".to_string(),
                MPath::new("3")? => "merged 3".to_string(),
                MPath::new("4")? => "4\n".to_string(),
                MPath::new("5")? => "5\n".to_string(),
                MPath::new("6")? => "6\n".to_string(),
                MPath::new("7")? => "7\n".to_string(),
                MPath::new("8")? => "8\n".to_string(),
                MPath::new("9")? => "9\n".to_string(),
                MPath::new("10")? => "modified10\n".to_string(),
                MPath::new("files")? => "branchmerge files content".to_string(),
                MPath::new("branchmerge")? => "new branch merge content".to_string(),
                MPath::new("randomfile")? => "some other content".to_string(),
        }
    );

    Ok(())
}

#[fbinit::test]
async fn backsync_merge_new_repo_branch_removed(fb: FacebookInit) -> Result<(), Error> {
    // Remove all files from new repo except for the file in the merge commit itself
    let (commit_syncer, target_repo_dbs) = init_repos(
        fb,
        MoverType::Except(vec![REPOMERGE_FOLDER.to_string()]),
        BookmarkRenamerType::Noop,
    )
    .await?;

    backsync_and_verify_master_wc(fb, commit_syncer.clone(), target_repo_dbs).await?;

    let ctx = CoreContext::test_mock(fb);
    let target_cs_id = resolve_cs_id(&ctx, commit_syncer.get_target_repo(), "master").await?;

    let map = list_working_copy_utf8(&ctx, commit_syncer.get_target_repo(), target_cs_id).await?;
    assert_eq!(
        map.into_iter().collect::<BTreeMap<_, _>>(),
        btreemap! {
                MPath::new("1")? => "1\n".to_string(),
                MPath::new("2")? => "2\n".to_string(),
                MPath::new("3")? => "3\n".to_string(),
                MPath::new("4")? => "4\n".to_string(),
                MPath::new("5")? => "5\n".to_string(),
                MPath::new("6")? => "6\n".to_string(),
                MPath::new("7")? => "7\n".to_string(),
                MPath::new("8")? => "8\n".to_string(),
                MPath::new("9")? => "9\n".to_string(),
                MPath::new("10")? => "modified10\n".to_string(),
                MPath::new("randomfile")? => "some other content".to_string(),
                MPath::new("files")? => "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n".to_string(),
        }
    );

    let target_cs_id =
        resolve_cs_id(&ctx, commit_syncer.get_target_repo(), "anotherbookmark").await?;
    let map = list_working_copy_utf8(&ctx, commit_syncer.get_target_repo(), target_cs_id).await?;
    assert_eq!(
        map.into_iter().collect::<BTreeMap<_, _>>(),
        btreemap! {
                MPath::new("1")? => "1\n".to_string(),
                MPath::new("2")? => "2\n".to_string(),
                MPath::new("3")? => "merged 3".to_string(),
                MPath::new("4")? => "4\n".to_string(),
                MPath::new("5")? => "5\n".to_string(),
                MPath::new("6")? => "6\n".to_string(),
                MPath::new("7")? => "7\n".to_string(),
                MPath::new("8")? => "8\n".to_string(),
                MPath::new("9")? => "9\n".to_string(),
                MPath::new("10")? => "modified10\n".to_string(),
                MPath::new("files")? => "branchmerge files content".to_string(),
                MPath::new("branchmerge")? => "new branch merge content".to_string(),
                MPath::new("repomergefile")? => "some content".to_string(),
                MPath::new("randomfile")? => "some other content".to_string(),
        }
    );

    Ok(())
}

#[fbinit::test]
async fn backsync_branch_merge_remove_branch_merge_file(fb: FacebookInit) -> Result<(), Error> {
    let (commit_syncer, target_repo_dbs) = init_repos(
        fb,
        MoverType::Except(vec![BRANCHMERGE_FILE.to_string()]),
        BookmarkRenamerType::Noop,
    )
    .await?;

    backsync_and_verify_master_wc(fb, commit_syncer.clone(), target_repo_dbs).await?;

    let ctx = CoreContext::test_mock(fb);
    let target_cs_id = resolve_cs_id(&ctx, commit_syncer.get_target_repo(), "master").await?;

    let map = list_working_copy_utf8(&ctx, commit_syncer.get_target_repo(), target_cs_id).await?;
    assert_eq!(
        map.into_iter().collect::<BTreeMap<_, _>>(),
        btreemap! {
                MPath::new("1")? => "1\n".to_string(),
                MPath::new("2")? => "2\n".to_string(),
                MPath::new("3")? => "3\n".to_string(),
                MPath::new("4")? => "4\n".to_string(),
                MPath::new("5")? => "5\n".to_string(),
                MPath::new("6")? => "6\n".to_string(),
                MPath::new("7")? => "7\n".to_string(),
                MPath::new("8")? => "8\n".to_string(),
                MPath::new("9")? => "9\n".to_string(),
                MPath::new("10")? => "modified10\n".to_string(),
                MPath::new("randomfile")? => "some other content".to_string(),
                MPath::new("files")? => "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n".to_string(),
        }
    );

    let target_cs_id =
        resolve_cs_id(&ctx, commit_syncer.get_target_repo(), "anotherbookmark").await?;
    let map = list_working_copy_utf8(&ctx, commit_syncer.get_target_repo(), target_cs_id).await?;
    assert_eq!(
        map.into_iter().collect::<BTreeMap<_, _>>(),
        btreemap! {
                MPath::new("1")? => "1\n".to_string(),
                MPath::new("2")? => "2\n".to_string(),
                MPath::new("3")? => "merged 3".to_string(),
                MPath::new("4")? => "4\n".to_string(),
                MPath::new("5")? => "5\n".to_string(),
                MPath::new("6")? => "6\n".to_string(),
                MPath::new("7")? => "7\n".to_string(),
                MPath::new("8")? => "8\n".to_string(),
                MPath::new("9")? => "9\n".to_string(),
                MPath::new("10")? => "modified10\n".to_string(),
                MPath::new("files")? => "branchmerge files content".to_string(),
                MPath::new("repomergefile")? => "some content".to_string(),
                MPath::new("randomfile")? => "some other content".to_string(),
                MPath::new("repomerge/first")? => "new repo content".to_string(),
                MPath::new("repomerge/movedest")? => "moved content".to_string(),
                MPath::new("repomerge/second")? => "new repo second content".to_string(),
                MPath::new("repomerge/toremove")? => "new repo content".to_string(),

        }
    );

    Ok(())
}

#[fbinit::test]
async fn backsync_unrelated_branch(fb: FacebookInit) -> Result<(), Error> {
    let master = BookmarkKey::new("master")?;
    let (commit_syncer, target_repo_dbs) = init_repos(
        fb,
        MoverType::Except(vec!["unrelated_branch".to_string()]),
        BookmarkRenamerType::Only(master),
    )
    .await?;
    let target_repo_dbs = Arc::new(target_repo_dbs);
    let source_repo = commit_syncer.get_source_repo();

    let ctx = CoreContext::test_mock(fb);
    let merge = build_unrelated_branch(ctx.clone(), source_repo).await;

    move_bookmark(
        ctx.clone(),
        source_repo.clone(),
        &BookmarkKey::new("otherrepo/somebook")?,
        merge,
    )
    .await?;

    backsync_latest(
        ctx.clone(),
        commit_syncer.clone(),
        target_repo_dbs.clone(),
        BacksyncLimit::NoLimit,
        Arc::new(AtomicBool::new(false)),
        CommitSyncContext::Backsyncer,
        false,
    )
    .await?;

    // Unrelated branch should be ignored until it's merged into already backsynced
    // branch
    let maybe_outcome = commit_syncer.get_commit_sync_outcome(&ctx, merge).await?;
    assert!(maybe_outcome.is_none());

    println!("merging into master");
    let new_master =
        CreateCommitContext::new(&ctx, &source_repo, vec!["master", "otherrepo/somebook"])
            .commit()
            .await?;

    move_bookmark(
        ctx.clone(),
        source_repo.clone(),
        &BookmarkKey::new("master")?,
        new_master,
    )
    .await?;

    backsync_latest(
        ctx.clone(),
        commit_syncer.clone(),
        target_repo_dbs.clone(),
        BacksyncLimit::NoLimit,
        Arc::new(AtomicBool::new(false)),
        CommitSyncContext::Backsyncer,
        false,
    )
    .await?;
    let maybe_outcome = commit_syncer
        .get_commit_sync_outcome(&ctx, new_master)
        .await?;
    assert!(maybe_outcome.is_some());
    let maybe_outcome = commit_syncer.get_commit_sync_outcome(&ctx, merge).await?;
    assert!(maybe_outcome.is_some());

    Ok(())
}

#[fbinit::test]
async fn backsync_change_mapping(fb: FacebookInit) -> Result<(), Error> {
    // Initialize source and target repos
    let ctx = CoreContext::test_mock(fb);
    let mut factory = TestRepoFactory::new(fb)?;
    let source_repo_id = RepositoryId::new(1);
    let source_repo: TestRepo = factory.with_id(source_repo_id).build().await?;
    let target_repo_id = RepositoryId::new(2);
    let target_repo: TestRepo = factory.with_id(target_repo_id).build().await?;

    // Create commit syncer with two version - current and new
    let target_repo_dbs = TargetRepoDbs {
        bookmarks: target_repo.bookmarks_arc(),
        bookmark_update_log: target_repo.bookmark_update_log_arc(),
        counters: target_repo.mutable_counters_arc(),
    };
    init_target_repo(&ctx, &target_repo_dbs, source_repo_id).await?;
    let target_repo_dbs = Arc::new(target_repo_dbs);
    let mapping = SqlSyncedCommitMapping::with_sqlite_in_memory()?;

    let repos = CommitSyncRepos::LargeToSmall {
        large_repo: source_repo.clone(),
        small_repo: target_repo.clone(),
    };

    let current_version = CommitSyncConfigVersion("current_version".to_string());
    let new_version = CommitSyncConfigVersion("new_version".to_string());

    let bookmark_renamer_type = BookmarkRenamerType::Noop;

    let (lv_cfg, lv_cfg_src) = TestLiveCommitSyncConfig::new_with_source();

    let current_version_config = CommitSyncConfig {
        large_repo_id: source_repo.repo_identity().id(),
        common_pushrebase_bookmarks: vec![BookmarkKey::new("master")?],
        small_repos: hashmap! {
            target_repo.repo_identity().id() => SmallRepoCommitSyncConfig {
                default_action: DefaultSmallToLargeCommitSyncPathAction::PrependPrefix(
                    MPath::new("current_prefix").unwrap(),
                ),
                map: hashmap! { },

            },
        },
        version_name: current_version.clone(),
    };

    lv_cfg_src.add_config(current_version_config);

    let new_version_config = CommitSyncConfig {
        large_repo_id: source_repo.repo_identity().id(),
        common_pushrebase_bookmarks: vec![BookmarkKey::new("master")?],
        small_repos: hashmap! {
            target_repo.repo_identity().id() => SmallRepoCommitSyncConfig {
                default_action: DefaultSmallToLargeCommitSyncPathAction::PrependPrefix(
                    MPath::new("new_prefix").unwrap(),
                ),
                map: hashmap! { },

            },
        },
        version_name: new_version.clone(),
    };
    lv_cfg_src.add_config(new_version_config);

    let common = bookmark_renamer_type.get_common_repo_config(
        target_repo.repo_identity().id(),
        source_repo.repo_identity().id(),
    );
    lv_cfg_src.add_common_config(common);

    let commit_sync_data_provider = CommitSyncDataProvider::Live(Arc::new(lv_cfg));

    let commit_syncer =
        CommitSyncer::new_with_provider(&ctx, mapping.clone(), repos, commit_sync_data_provider);

    // Rewrite root commit with current version
    let root_cs_id = CreateCommitContext::new_root(&ctx, &source_repo)
        .commit()
        .await?;

    commit_syncer
        .unsafe_always_rewrite_sync_commit(
            &ctx,
            root_cs_id,
            None,
            &current_version,
            CommitSyncContext::Tests,
        )
        .await?;

    // Add one more empty commit with old mapping
    let before_mapping_change = CreateCommitContext::new(&ctx, &source_repo, vec![root_cs_id])
        .commit()
        .await?;

    // Now create a commit with a special extra that changes the mapping
    // to new version while backsyncing
    let change_mapping_commit =
        CreateCommitContext::new(&ctx, &source_repo, vec![before_mapping_change])
            .add_extra(
                CHANGE_XREPO_MAPPING_EXTRA.to_string(),
                new_version.clone().0.into_bytes(),
            )
            .commit()
            .await?;

    let after_mapping_change_commit =
        CreateCommitContext::new(&ctx, &source_repo, vec![change_mapping_commit])
            .add_file("new_prefix/file", "content")
            .commit()
            .await?;

    bookmark(&ctx, &source_repo, "head")
        .set_to(after_mapping_change_commit)
        .await?;

    // Do the backsync, and check the version
    let tunables = tunables::MononokeTunables::default();
    tunables.update_bools(&hashmap! {
        "allow_change_xrepo_mapping_extra".to_string() => true,
    });

    let f = backsync_latest(
        ctx.clone(),
        commit_syncer.clone(),
        target_repo_dbs.clone(),
        BacksyncLimit::NoLimit,
        Arc::new(AtomicBool::new(false)),
        CommitSyncContext::Backsyncer,
        false,
    );
    with_tunables_async(tunables, f.boxed()).await?;

    let commit_sync_outcome = commit_syncer
        .get_commit_sync_outcome(&ctx, before_mapping_change)
        .await?
        .ok_or_else(|| anyhow!("unexpected missing commit sync outcome"))?;

    assert_matches!(commit_sync_outcome, CommitSyncOutcome::RewrittenAs(_, version) => {
        assert_eq!(current_version, version);
    });

    let commit_sync_outcome = commit_syncer
        .get_commit_sync_outcome(&ctx, change_mapping_commit)
        .await?
        .ok_or_else(|| anyhow!("unexpected missing commit sync outcome"))?;

    assert_matches!(commit_sync_outcome, CommitSyncOutcome::RewrittenAs(_, version) => {
        assert_eq!(new_version, version);
    });

    let commit_sync_outcome = commit_syncer
        .get_commit_sync_outcome(&ctx, after_mapping_change_commit)
        .await?
        .ok_or_else(|| anyhow!("unexpected missing commit sync outcome"))?;

    let target_cs_id = assert_matches!(commit_sync_outcome, CommitSyncOutcome::RewrittenAs(target_cs_id, version) => {
        assert_eq!(new_version, version);
        target_cs_id
    });

    let map = list_working_copy_utf8(&ctx, commit_syncer.get_target_repo(), target_cs_id).await?;
    assert_eq!(map, hashmap! {MPath::new("file")? => "content".to_string()});

    Ok(())
}

async fn build_unrelated_branch(ctx: CoreContext, source_repo: &TestRepo) -> ChangesetId {
    let p1 = new_commit(
        ctx.clone(),
        source_repo,
        vec![],
        btreemap! {"unrelated_branch" => Some("first content")},
    )
    .await;
    println!("p1: {:?}", p1);

    let p2 = new_commit(
        ctx.clone(),
        source_repo,
        vec![],
        btreemap! {"unrelated_branch" => Some("second content")},
    )
    .await;
    println!("p2: {:?}", p2);

    let merge = new_commit(
        ctx.clone(),
        source_repo,
        vec![p1, p2],
        btreemap! {"unrelated_branch" => Some("merge content")},
    )
    .await;
    println!("merge: {:?}", merge);

    merge
}

async fn new_commit<T: AsRef<str>>(
    ctx: CoreContext,
    repo: &TestRepo,
    parents: Vec<ChangesetId>,
    contents: BTreeMap<&str, Option<T>>,
) -> ChangesetId {
    create_commit(
        ctx.clone(),
        repo.clone(),
        parents,
        store_files(&ctx, contents, &repo).await,
    )
    .await
}

async fn backsync_and_verify_master_wc(
    fb: FacebookInit,
    commit_syncer: CommitSyncer<SqlSyncedCommitMapping, TestRepo>,
    target_repo_dbs: TargetRepoDbs,
) -> Result<(), Error> {
    let source_repo = commit_syncer.get_source_repo();

    let ctx = CoreContext::test_mock(fb);
    let next_log_entries: Vec<_> = commit_syncer
        .get_source_repo()
        .bookmark_update_log()
        .read_next_bookmark_log_entries(ctx.clone(), 0, 1000, Freshness::MaybeStale)
        .try_collect()
        .await?;
    let target_repo_dbs = Arc::new(target_repo_dbs);
    let latest_log_id = next_log_entries.len() as i64;

    let mut futs = vec![];
    // Run syncs in parallel
    for _ in 1..5 {
        let f = tokio::task::spawn(backsync_latest(
            ctx.clone(),
            commit_syncer.clone(),
            target_repo_dbs.clone(),
            BacksyncLimit::NoLimit,
            Arc::new(AtomicBool::new(false)),
            CommitSyncContext::Backsyncer,
            false,
        ))
        .flatten_err();
        futs.push(f);
    }

    futures::future::try_join_all(futs).await?;

    // Check that counter was moved
    let fetched_value = target_repo_dbs
        .counters
        .get_counter(&ctx, &format_counter(&source_repo.repo_identity().id()))
        .await?;
    assert_eq!(fetched_value, Some(latest_log_id));

    verify_mapping_and_all_wc(ctx.clone(), commit_syncer, vec![]).await?;
    Ok(())
}

async fn verify_mapping_and_all_wc(
    ctx: CoreContext,
    commit_syncer: CommitSyncer<SqlSyncedCommitMapping, TestRepo>,
    dont_verify_commits: Vec<ChangesetId>,
) -> Result<(), Error> {
    let source_repo = commit_syncer.get_source_repo();
    let target_repo = commit_syncer.get_target_repo();

    verify_bookmarks(ctx.clone(), commit_syncer.clone()).await?;

    let heads: Vec<_> = source_repo
        .bookmarks()
        .get_heads_maybe_stale(ctx.clone())
        .try_collect()
        .await?;

    println!("checking all source commits");
    let all_source_commits = source_repo
        .commit_graph()
        .ancestors_difference(&ctx, heads, vec![])
        .await?;

    // Check that all commits were synced correctly
    for source_cs_id in all_source_commits {
        if dont_verify_commits.contains(&source_cs_id) {
            continue;
        }
        let csc = commit_syncer.clone();
        let outcome = csc.get_commit_sync_outcome(&ctx, source_cs_id).await?;
        let source_bcs = source_cs_id
            .load(&ctx, source_repo.repo_blobstore())
            .await?;
        let outcome = outcome.unwrap_or_else(|| {
            panic!(
                "commit has not been synced {} {:?}",
                source_cs_id, source_bcs
            )
        });
        use CommitSyncOutcome::*;

        let (target_cs_id, mover_to_use) = match outcome {
            NotSyncCandidate(_) => {
                continue;
            }
            EquivalentWorkingCopyAncestor(target_cs_id, ref version)
            | RewrittenAs(target_cs_id, ref version) => {
                println!("using mover for {:?}", version);
                (
                    target_cs_id,
                    commit_syncer.get_mover_by_version(version).await?,
                )
            }
        };

        // Empty commits should always be synced, except for merges
        let bcs = source_cs_id
            .load(&ctx, csc.get_source_repo().repo_blobstore())
            .await?;
        if bcs.file_changes().collect::<Vec<_>>().is_empty() && !bcs.is_merge() {
            match outcome {
                RewrittenAs(..) => {}
                _ => {
                    panic!("empty commit should always be remapped {:?}", outcome);
                }
            };
        }

        let source_hg_cs_id = source_repo.derive_hg_changeset(&ctx, source_cs_id).await?;
        let target_hg_cs_id = target_repo.derive_hg_changeset(&ctx, target_cs_id).await?;

        compare_contents(
            &ctx,
            source_hg_cs_id,
            target_hg_cs_id,
            commit_syncer.clone(),
            mover_to_use.clone(),
        )
        .await?;
    }
    Ok(())
}

async fn verify_bookmarks(
    ctx: CoreContext,
    commit_syncer: CommitSyncer<SqlSyncedCommitMapping, TestRepo>,
) -> Result<(), Error> {
    let source_repo = commit_syncer.get_source_repo();
    let target_repo = commit_syncer.get_target_repo();
    let bookmark_renamer = commit_syncer.get_bookmark_renamer().await?;

    let bookmarks: Vec<_> = source_repo
        .get_publishing_bookmarks_maybe_stale_hg(ctx.clone())
        .try_collect()
        .await?;

    // Check that bookmark point to corresponding working copies
    for (bookmark, source_hg_cs_id) in bookmarks {
        println!("checking bookmark: {}", bookmark.key());
        match bookmark_renamer(bookmark.key()) {
            Some(renamed_book) => {
                if &renamed_book != bookmark.key() {
                    assert!(
                        target_repo
                            .get_bookmark_hg(ctx.clone(), bookmark.key())
                            .await?
                            .is_none()
                    );
                }
                let target_hg_cs_id = target_repo
                    .get_bookmark_hg(ctx.clone(), &renamed_book)
                    .await?
                    .unwrap_or_else(|| {
                        panic!("{} bookmark doesn't exist in target repo!", bookmark.key())
                    });

                let source_bcs_id = source_repo
                    .bonsai_hg_mapping()
                    .get_bonsai_from_hg(&ctx, source_hg_cs_id)
                    .await?
                    .unwrap();

                let commit_sync_outcome = commit_syncer
                    .get_commit_sync_outcome(&ctx, source_bcs_id)
                    .await?;
                let commit_sync_outcome = commit_sync_outcome.expect("unsynced commit");

                println!(
                    "verify_bookmarks. calling compare_contents: source_bcs_id: {}, outcome: {:?}",
                    source_bcs_id, commit_sync_outcome
                );

                use CommitSyncOutcome::*;
                let mover = match commit_sync_outcome {
                    NotSyncCandidate(_) => {
                        panic!("commit should not point to NotSyncCandidate");
                    }
                    EquivalentWorkingCopyAncestor(_, version) | RewrittenAs(_, version) => {
                        println!("using mover for {:?}", version);
                        commit_syncer.get_mover_by_version(&version).await?
                    }
                };

                compare_contents(
                    &ctx,
                    source_hg_cs_id,
                    target_hg_cs_id,
                    commit_syncer.clone(),
                    mover.clone(),
                )
                .await?;
            }
            None => {
                // Make sure we don't have this bookmark in target repo
                assert!(
                    target_repo
                        .get_bookmark_hg(ctx.clone(), bookmark.key())
                        .await?
                        .is_none()
                );
            }
        }
    }

    Ok(())
}

async fn compare_contents(
    ctx: &CoreContext,
    source_hg_cs_id: HgChangesetId,
    target_hg_cs_id: HgChangesetId,
    commit_syncer: CommitSyncer<SqlSyncedCommitMapping, TestRepo>,
    mover: Mover,
) -> Result<(), Error> {
    let source_content =
        list_content(ctx, source_hg_cs_id, commit_syncer.get_source_repo()).await?;
    let target_content =
        list_content(ctx, target_hg_cs_id, commit_syncer.get_target_repo()).await?;

    println!(
        "source content: {:?}, target content {:?}",
        source_content, target_content
    );
    let filtered_source_content = source_content
        .into_iter()
        .filter_map(|(key, value)| {
            mover(&MPath::new(key).unwrap())
                .unwrap()
                .map(|key| (key, value))
        })
        .map(|(path, value)| (format!("{}", path), value))
        .collect();

    assert_eq!(target_content, filtered_source_content);

    Ok(())
}

async fn list_content(
    ctx: &CoreContext,
    hg_cs_id: HgChangesetId,
    repo: &TestRepo,
) -> Result<HashMap<String, String>, Error> {
    let cs = hg_cs_id.load(ctx, repo.repo_blobstore()).await?;

    let entries = cs
        .manifestid()
        .list_all_entries(ctx.clone(), repo.repo_blobstore().clone())
        .try_collect::<Vec<_>>()
        .await?;

    let mut actual = HashMap::new();
    for (path, entry) in entries {
        match entry {
            Entry::Leaf((_, filenode_id)) => {
                let blobstore = repo.repo_blobstore();
                let envelope = filenode_id.load(ctx, blobstore).await?;
                let content =
                    filestore::fetch_concat(blobstore, ctx, envelope.content_id()).await?;
                let s = String::from_utf8_lossy(content.as_ref()).into_owned();
                actual.insert(format!("{}", path.unwrap()), s);
            }
            Entry::Tree(_) => {}
        }
    }

    Ok(actual)
}

enum BookmarkRenamerType {
    CommonAndPrefix(BookmarkKey, String),
    Only(BookmarkKey),
    RemoveAll,
    Noop,
}

impl BookmarkRenamerType {
    fn get_common_repo_config(
        &self,
        small_repo_id: RepositoryId,
        large_repo_id: RepositoryId,
    ) -> CommonCommitSyncConfig {
        use BookmarkRenamerType::*;

        match self {
            CommonAndPrefix(common, bookmark_prefix) => CommonCommitSyncConfig {
                common_pushrebase_bookmarks: vec![common.clone()],
                small_repos: hashmap! {
                    small_repo_id => SmallRepoPermanentConfig {
                        bookmark_prefix: AsciiString::from_str(bookmark_prefix).unwrap(),
                    }
                },
                large_repo_id,
            },
            Only(name) => CommonCommitSyncConfig {
                common_pushrebase_bookmarks: vec![name.clone()],
                small_repos: hashmap! {
                    small_repo_id => SmallRepoPermanentConfig {
                        bookmark_prefix: AsciiString::from_str("nonexistentprefix").unwrap(),
                    }
                },
                large_repo_id,
            },
            RemoveAll => CommonCommitSyncConfig {
                common_pushrebase_bookmarks: vec![],
                small_repos: hashmap! {
                    small_repo_id => SmallRepoPermanentConfig {
                        bookmark_prefix: AsciiString::from_str("nonexistentprefix").unwrap(),
                    }
                },
                large_repo_id,
            },
            Noop => CommonCommitSyncConfig {
                common_pushrebase_bookmarks: vec![],
                small_repos: hashmap! {
                    small_repo_id => SmallRepoPermanentConfig {
                        bookmark_prefix: AsciiString::new(),
                    }
                },
                large_repo_id,
            },
        }
    }
}

enum MoverType {
    Noop,
    Except(Vec<String>),
    Only(String),
}

impl MoverType {
    fn get_small_repo_config(&self) -> SmallRepoCommitSyncConfig {
        use MoverType::*;

        match self {
            Noop => SmallRepoCommitSyncConfig {
                default_action: DefaultSmallToLargeCommitSyncPathAction::Preserve,
                map: hashmap! {},
            },
            Except(files) => {
                let mut map = hashmap! {};
                for file in files {
                    map.insert(
                        MPath::new(file).unwrap(),
                        MPath::new(format!("nonexistentpath{}", file)).unwrap(),
                    );
                }
                SmallRepoCommitSyncConfig {
                    default_action: DefaultSmallToLargeCommitSyncPathAction::Preserve,
                    map,
                }
            }
            Only(path) => SmallRepoCommitSyncConfig {
                default_action: DefaultSmallToLargeCommitSyncPathAction::PrependPrefix(
                    MPath::new("nonexistentpath").unwrap(),
                ),
                map: hashmap! {
                    MPath::new(path).unwrap() => MPath::new(path).unwrap(),
                },
            },
        }
    }
}

async fn init_repos(
    fb: FacebookInit,
    mover_type: MoverType,
    bookmark_renamer_type: BookmarkRenamerType,
) -> Result<
    (
        CommitSyncer<SqlSyncedCommitMapping, TestRepo>,
        TargetRepoDbs,
    ),
    Error,
> {
    let ctx = CoreContext::test_mock(fb);
    let mut factory = TestRepoFactory::new(fb)?;
    let source_repo_id = RepositoryId::new(1);
    let source_repo: TestRepo = factory.with_id(source_repo_id).build().await?;
    Linear::initrepo(fb, &source_repo).await;

    let target_repo_id = RepositoryId::new(2);
    let target_repo: TestRepo = factory.with_id(target_repo_id).build().await?;

    let target_repo_dbs = TargetRepoDbs {
        bookmarks: target_repo.bookmarks_arc(),
        bookmark_update_log: target_repo.bookmark_update_log_arc(),
        counters: target_repo.mutable_counters_arc(),
    };
    init_target_repo(&ctx, &target_repo_dbs, source_repo_id).await?;

    let mapping = SqlSyncedCommitMapping::with_sqlite_in_memory()?;

    let repos = CommitSyncRepos::LargeToSmall {
        large_repo: source_repo.clone(),
        small_repo: target_repo.clone(),
    };

    let empty: BTreeMap<_, Option<&str>> = BTreeMap::new();
    // Create fake empty commit in the target repo
    let initial_commit_in_target = create_commit(
        ctx.clone(),
        target_repo.clone(),
        vec![],
        store_files(&ctx, empty.clone(), &source_repo).await,
    )
    .await;

    let (lv_cfg, lv_cfg_src) = TestLiveCommitSyncConfig::new_with_source();

    let version = CommitSyncConfigVersion("TEST_VERSION_NAME".to_string());
    let version_config = CommitSyncConfig {
        large_repo_id: source_repo.repo_identity().id(),
        common_pushrebase_bookmarks: vec![BookmarkKey::new("master")?],
        small_repos: hashmap! {
            target_repo.repo_identity().id() => mover_type.get_small_repo_config(),
        },
        version_name: version.clone(),
    };

    lv_cfg_src.add_config(version_config);
    let common = bookmark_renamer_type.get_common_repo_config(
        target_repo.repo_identity().id(),
        source_repo.repo_identity().id(),
    );
    lv_cfg_src.add_common_config(common);

    let commit_sync_data_provider = CommitSyncDataProvider::Live(Arc::new(lv_cfg));
    let commit_syncer =
        CommitSyncer::new_with_provider(&ctx, mapping.clone(), repos, commit_sync_data_provider);

    // Sync first commit manually
    let initial_bcs_id = source_repo
        .bonsai_hg_mapping()
        .get_bonsai_from_hg(
            &ctx,
            HgChangesetId::from_str("2d7d4ba9ce0a6ffd222de7785b249ead9c51c536").unwrap(),
        )
        .await?
        .unwrap();
    let first_bcs = initial_bcs_id
        .load(&ctx, source_repo.repo_blobstore())
        .await?;
    upload_commits(&ctx, vec![first_bcs.clone()], &source_repo, &target_repo).await?;
    let first_bcs_mut = first_bcs.into_mut();
    let maybe_rewritten = {
        let empty_map = HashMap::new();
        cloned!(ctx, source_repo);
        rewrite_commit(
            &ctx,
            first_bcs_mut,
            &empty_map,
            commit_syncer.get_mover_by_version(&version).await?,
            &source_repo,
            Default::default(),
        )
        .await
    }?;
    let rewritten_first_bcs_id = match maybe_rewritten {
        Some(mut rewritten) => {
            rewritten.parents.push(initial_commit_in_target);

            let rewritten = rewritten.freeze()?;
            save_bonsai_changesets(vec![rewritten.clone()], ctx.clone(), &target_repo).await?;
            rewritten.get_changeset_id()
        }
        None => initial_commit_in_target,
    };

    let first_entry = SyncedCommitMappingEntry::new(
        source_repo.repo_identity().id(),
        initial_bcs_id,
        target_repo.repo_identity().id(),
        rewritten_first_bcs_id,
        CommitSyncConfigVersion("TEST_VERSION_NAME".to_string()),
        commit_syncer.get_source_repo_type(),
    );
    mapping.add(&ctx, first_entry).await?;

    // Create a few new commits on top of master

    let master = BookmarkKey::new("master")?;
    let master_val = source_repo
        .bookmarks()
        .get(ctx.clone(), &master)
        .await?
        .unwrap();

    let empty_bcs_id = create_commit(
        ctx.clone(),
        source_repo.clone(),
        vec![master_val],
        store_files(&ctx, empty, &source_repo).await,
    )
    .await;

    let first_bcs_id = create_commit(
        ctx.clone(),
        source_repo.clone(),
        vec![empty_bcs_id],
        store_files(
            &ctx,
            btreemap! {"randomfile" => Some("some content")},
            &source_repo,
        )
        .await,
    )
    .await;

    let second_bcs_id = create_commit(
        ctx.clone(),
        source_repo.clone(),
        vec![first_bcs_id],
        store_files(
            &ctx,
            btreemap! {"randomfile" => Some("some other content")},
            &source_repo,
        )
        .await,
    )
    .await;

    move_bookmark(ctx.clone(), source_repo.clone(), &master, second_bcs_id).await?;

    // Create new bookmark
    let master = BookmarkKey::new("anotherbookmark")?;
    move_bookmark(ctx.clone(), source_repo.clone(), &master, first_bcs_id).await?;

    // Merge new repo into master
    let first_new_repo_file = format!("{}/first", REPOMERGE_FOLDER);
    let to_remove_new_repo_file = format!("{}/toremove", REPOMERGE_FOLDER);
    let move_dest_new_repo_file = format!("{}/movedest", REPOMERGE_FOLDER);
    let second_new_repo_file = format!("{}/second", REPOMERGE_FOLDER);

    let first_new_repo_commit = new_commit(
        ctx.clone(),
        &source_repo,
        vec![],
        btreemap! {
            first_new_repo_file.as_ref() => Some("new repo content"),
            to_remove_new_repo_file.as_ref() => Some("new repo content"),
        },
    )
    .await;

    let p2 = {
        let (path_rename, rename_file_change) = store_rename(
            &ctx,
            (
                MPath::new(to_remove_new_repo_file.clone())?,
                first_new_repo_commit,
            ),
            &move_dest_new_repo_file,
            "moved content",
            &source_repo,
        )
        .await;

        let mut stored_files = store_files(
            &ctx,
            btreemap! {
                second_new_repo_file.as_ref() => Some("new repo second content"),
            },
            &source_repo,
        )
        .await;
        stored_files.insert(path_rename, rename_file_change);

        create_commit(
            ctx.clone(),
            source_repo.clone(),
            vec![first_new_repo_commit],
            stored_files,
        )
        .await
    };

    let merge = new_commit(
        ctx.clone(),
        &source_repo,
        vec![second_bcs_id, p2],
        btreemap! {
             REPOMERGE_FILE => Some("some content"),
        },
    )
    .await;
    move_bookmark(ctx.clone(), source_repo.clone(), &master, merge).await?;

    // Create a branch merge - merge initial commit in the repo with the last
    let branch_merge_p1 = new_commit(
        ctx.clone(),
        &source_repo,
        vec![initial_bcs_id],
        btreemap! {
            "3" => Some("branchmerge 3 content"),
        },
    )
    .await;

    let branch_merge = new_commit(
        ctx.clone(),
        &source_repo,
        vec![branch_merge_p1, merge],
        btreemap! {
            BRANCHMERGE_FILE => Some("branch merge content"),
            // Both parents have different content in "files" and "3" - need to resolve it
            "files" => Some("branchmerge files content"),
            "3" => Some("merged 3"),
        },
    )
    .await;
    move_bookmark(ctx.clone(), source_repo.clone(), &master, branch_merge).await?;

    // Do a branch merge again, but this time only change content in BRANCHMERGE_FILE
    let branch_merge_second = new_commit(
        ctx.clone(),
        &source_repo,
        vec![branch_merge_p1, branch_merge],
        btreemap! {
            BRANCHMERGE_FILE => Some("new branch merge content"),
            // Both parents have different content in "files" and "3" - need to resolve it
            "files" => Some("branchmerge files content"),
            "3" => Some("merged 3"),
        },
    )
    .await;
    move_bookmark(
        ctx.clone(),
        source_repo.clone(),
        &master,
        branch_merge_second,
    )
    .await?;

    Ok((commit_syncer, target_repo_dbs))
}

async fn init_target_repo(
    ctx: &CoreContext,
    target_repo_dbs: &TargetRepoDbs,
    source_repo_id: RepositoryId,
) -> Result<(), Error> {
    // Init counters
    target_repo_dbs
        .counters
        .set_counter(ctx, &format_counter(&source_repo_id), 0, None)
        .await?;

    Ok(())
}

async fn init_merged_repos(
    fb: FacebookInit,
    num_repos: usize,
) -> Result<
    (
        Vec<(
            CommitSyncer<SqlSyncedCommitMapping, TestRepo>,
            TargetRepoDbs,
        )>,
        TestRepo,
        i64,
        Vec<ChangesetId>,
    ),
    Error,
> {
    let ctx = CoreContext::test_mock(fb);

    let mut factory = TestRepoFactory::new(fb)?;
    let large_repo_id = RepositoryId::new(num_repos as i32);
    let large_repo: TestRepo = factory.with_id(large_repo_id).build().await?;

    let mapping = SqlSyncedCommitMapping::with_sqlite_in_memory()?;

    let mut output = vec![];
    let mut small_repos = vec![];
    let mut moved_cs_ids = vec![];
    // Create small repos and one large repo
    for idx in 0..num_repos {
        let repoid = RepositoryId::new(idx as i32);
        let small_repo: TestRepo = factory.with_id(repoid).build().await?;
        let small_repo_dbs = TargetRepoDbs {
            bookmarks: small_repo.bookmarks_arc(),
            bookmark_update_log: small_repo.bookmark_update_log_arc(),
            counters: small_repo.mutable_counters_arc(),
        };

        // Init counters
        small_repo_dbs
            .counters
            .set_counter(&ctx, &format_counter(&large_repo_id), 0, None)
            .await?;

        let after_merge_version = CommitSyncConfigVersion("TEST_VERSION_NAME".to_string());
        let noop_version = CommitSyncConfigVersion("noop".to_string());

        let (lv_cfg, lv_cfg_src) = TestLiveCommitSyncConfig::new_with_source();

        let new_version_config = CommitSyncConfig {
            large_repo_id: large_repo.repo_identity().id(),
            common_pushrebase_bookmarks: vec![BookmarkKey::new("master")?],
            small_repos: hashmap! {
                small_repo.repo_identity().id() => SmallRepoCommitSyncConfig {
                    default_action: DefaultSmallToLargeCommitSyncPathAction::PrependPrefix(
                        MPath::new(format!("smallrepo{}", small_repo.repo_identity().id().id())).unwrap(),
                    ),
                    map: hashmap! { },

                },
            },
            version_name: after_merge_version.clone(),
        };

        lv_cfg_src.add_config(new_version_config);

        let mover_type = MoverType::Noop;
        let noop_version_config = CommitSyncConfig {
            large_repo_id: large_repo.repo_identity().id(),
            common_pushrebase_bookmarks: vec![BookmarkKey::new("master")?],
            small_repos: hashmap! {
                small_repo.repo_identity().id() => mover_type.get_small_repo_config(),
            },
            version_name: noop_version.clone(),
        };
        lv_cfg_src.add_config(noop_version_config);

        let bookmark_renamer_type = BookmarkRenamerType::CommonAndPrefix(
            BookmarkKey::new("master")?,
            format!("smallrepo{}", repoid.id()),
        );

        let common = bookmark_renamer_type.get_common_repo_config(
            small_repo.repo_identity().id(),
            large_repo.repo_identity().id(),
        );
        lv_cfg_src.add_common_config(common);

        let commit_sync_data_provider = CommitSyncDataProvider::Live(Arc::new(lv_cfg));
        let repos = CommitSyncRepos::LargeToSmall {
            large_repo: large_repo.clone(),
            small_repo: small_repo.clone(),
        };

        let commit_syncer = CommitSyncer::new_with_provider(
            &ctx,
            mapping.clone(),
            repos,
            commit_sync_data_provider,
        );
        output.push((commit_syncer, small_repo_dbs));

        let filename = format!("file_in_smallrepo{}", small_repo.repo_identity().id().id());
        let small_repo_cs_id = create_commit(
            ctx.clone(),
            small_repo.clone(),
            vec![],
            store_files(
                &ctx,
                btreemap! { filename.as_str() => Some("some content")},
                &small_repo,
            )
            .await,
        )
        .await;
        println!("small repo cs id w/o parents: {}", small_repo_cs_id);

        small_repos.push((small_repo.clone(), small_repo_cs_id.clone()));

        let mut other_repo_ids = vec![];
        for i in 0..num_repos {
            if i != idx {
                other_repo_ids.push(RepositoryId::new(i as i32));
            }
        }

        preserve_premerge_commit(
            ctx.clone(),
            large_repo.clone(),
            small_repo.clone(),
            other_repo_ids.clone(),
            small_repo_cs_id,
            &mapping,
        )
        .await?;

        let renamed_filename = format!(
            "smallrepo{}/{}",
            small_repo.repo_identity().id().id(),
            filename
        );
        let (renamed_path, rename) = store_rename(
            &ctx,
            (MPath::new(&filename).unwrap(), small_repo_cs_id),
            renamed_filename.as_str(),
            "some content",
            &large_repo,
        )
        .await;

        let moved_cs_id = create_commit(
            ctx.clone(),
            large_repo.clone(),
            vec![small_repo_cs_id],
            btreemap! {
                renamed_path => rename,
            },
        )
        .await;
        println!("large repo moved cs id: {}", moved_cs_id);
        moved_cs_ids.push(moved_cs_id);
    }

    // Create merge commit
    let merge_cs_id = create_commit(
        ctx.clone(),
        large_repo.clone(),
        moved_cs_ids.clone(),
        btreemap! {},
    )
    .await;

    println!("large repo merge cs id: {}", merge_cs_id);
    // Create an empty commit on top of a merge commit and sync it to all small repos
    let empty: BTreeMap<_, Option<&str>> = BTreeMap::new();
    // Create empty commit in the large repo, and sync it to all small repos
    let first_after_merge_commit = create_commit(
        ctx.clone(),
        large_repo.clone(),
        vec![merge_cs_id],
        store_files(&ctx, empty.clone(), &large_repo).await,
    )
    .await;
    println!("large repo empty commit: {}", first_after_merge_commit);

    for (small_repo, latest_small_repo_cs_id) in &small_repos {
        let small_repo_first_after_merge = create_commit(
            ctx.clone(),
            small_repo.clone(),
            vec![*latest_small_repo_cs_id],
            store_files(&ctx, empty.clone(), &small_repo).await,
        )
        .await;

        println!(
            "empty commit in {}: {}",
            small_repo.repo_identity().id(),
            small_repo_first_after_merge
        );
        let entry = SyncedCommitMappingEntry::new(
            large_repo.repo_identity().id(),
            first_after_merge_commit,
            small_repo.repo_identity().id(),
            small_repo_first_after_merge,
            CommitSyncConfigVersion("TEST_VERSION_NAME".to_string()),
            SyncedCommitSourceRepo::Large,
        );
        mapping.add(&ctx, entry).await?;
    }

    // Create new commit in large repo
    let mut latest_log_id = 0;
    {
        let master = BookmarkKey::new("master")?;
        let mut prev_master = None;
        for repo_id in 0..num_repos {
            let filename = format!("smallrepo{}/newfile", repo_id);
            let new_commit = create_commit(
                ctx.clone(),
                large_repo.clone(),
                vec![first_after_merge_commit],
                store_files(
                    &ctx,
                    btreemap! { filename.as_str() => Some("new content")},
                    &large_repo,
                )
                .await,
            )
            .await;

            println!("new commit in large repo: {}", new_commit);
            latest_log_id += 1;
            move_bookmark(ctx.clone(), large_repo.clone(), &master, new_commit).await?;
            prev_master = Some(new_commit);
        }

        // Create bookmark on premerge commit from first repo
        let premerge_book = BookmarkKey::new("smallrepo0/premerge_book")?;
        latest_log_id += 1;
        move_bookmark(
            ctx.clone(),
            large_repo.clone(),
            &premerge_book,
            small_repos[0].1,
        )
        .await?;

        // Now on second repo and move it to rewritten changeset
        let premerge_book = BookmarkKey::new("smallrepo1/premerge_book")?;
        latest_log_id += 1;
        move_bookmark(
            ctx.clone(),
            large_repo.clone(),
            &premerge_book,
            small_repos[1].1,
        )
        .await?;

        latest_log_id += 1;
        move_bookmark(
            ctx.clone(),
            large_repo.clone(),
            &premerge_book,
            prev_master.unwrap(),
        )
        .await?;

        // New commit that touches files from two different small repos
        let filename1 = "smallrepo0/newfile";
        let filename2 = "smallrepo1/newfile";
        let new_commit = create_commit(
            ctx.clone(),
            large_repo.clone(),
            vec![first_after_merge_commit],
            store_files(
                &ctx,
                btreemap! {
                    filename1 => Some("new content1"),
                    filename2 => Some("new content2"),
                },
                &large_repo,
            )
            .await,
        )
        .await;
        println!("large_repo newcommit: {}", new_commit);

        latest_log_id += 1;
        move_bookmark(ctx.clone(), large_repo.clone(), &master, new_commit).await?;

        // Create a Preserved commit
        let premerge_book = BookmarkKey::new("smallrepo0/preserved_commit")?;
        let filename = "smallrepo1/newfile";
        let new_commit = create_commit(
            ctx.clone(),
            large_repo.clone(),
            vec![small_repos[0].1],
            store_files(
                &ctx,
                btreemap! {
                    filename => Some("preserved content"),
                },
                &large_repo,
            )
            .await,
        )
        .await;
        println!("smallrepo1 newcommit: {}", new_commit);

        latest_log_id += 1;
        move_bookmark(ctx.clone(), large_repo.clone(), &premerge_book, new_commit).await?;
    }

    let mut commits_to_skip_verification = vec![];
    commits_to_skip_verification.extend(moved_cs_ids);
    commits_to_skip_verification.push(merge_cs_id);

    Ok((
        output,
        large_repo,
        latest_log_id,
        commits_to_skip_verification,
    ))
}

async fn preserve_premerge_commit(
    ctx: CoreContext,
    large_repo: TestRepo,
    small_repo: TestRepo,
    another_small_repo_ids: Vec<RepositoryId>,
    bcs_id: ChangesetId,
    mapping: &SqlSyncedCommitMapping,
) -> Result<(), Error> {
    println!(
        "preserve_premerge_commit called. large_repo: {}; small_repo: {}, another_small_repo_ids: {:?}, bcs_id: {}",
        large_repo.repo_identity().id(),
        small_repo.repo_identity().id(),
        another_small_repo_ids,
        bcs_id
    );

    let version = CommitSyncConfigVersion("noop".to_string());
    // Doesn't matter what mover to use - we are going to preserve the commit anyway
    let small_to_large_sync_config = {
        let repos = CommitSyncRepos::SmallToLarge {
            large_repo: large_repo.clone(),
            small_repo: small_repo.clone(),
        };

        let (lv_cfg, lv_cfg_src) = TestLiveCommitSyncConfig::new_with_source();

        let bookmark_renamer_type = BookmarkRenamerType::Noop;
        let mover_type = MoverType::Noop;

        let version_config = CommitSyncConfig {
            large_repo_id: large_repo.repo_identity().id(),
            common_pushrebase_bookmarks: vec![BookmarkKey::new("master")?],
            small_repos: hashmap! {
                small_repo.repo_identity().id() => mover_type.get_small_repo_config(),
            },
            version_name: version.clone(),
        };

        lv_cfg_src.add_config(version_config);
        let common = bookmark_renamer_type.get_common_repo_config(
            small_repo.repo_identity().id(),
            large_repo.repo_identity().id(),
        );
        lv_cfg_src.add_common_config(common);

        let commit_sync_data_provider = CommitSyncDataProvider::Live(Arc::new(lv_cfg));
        CommitSyncer::new_with_provider(&ctx, mapping.clone(), repos, commit_sync_data_provider)
    };

    small_to_large_sync_config
        .unsafe_sync_commit_with_expected_version(
            &ctx,
            bcs_id,
            CandidateSelectionHint::Only,
            CommitSyncConfigVersion("noop".to_string()),
            CommitSyncContext::Tests,
        )
        .await?;

    for another_repo_id in another_small_repo_ids {
        mapping
            .insert_equivalent_working_copy(
                &ctx,
                EquivalentWorkingCopyEntry {
                    large_repo_id: large_repo.repo_identity().id(),
                    large_bcs_id: bcs_id,
                    small_repo_id: another_repo_id,
                    small_bcs_id: None,
                    version_name: Some(version.clone()),
                },
            )
            .await?;
    }
    Ok(())
}

async fn move_bookmark(
    ctx: CoreContext,
    repo: TestRepo,
    bookmark: &BookmarkKey,
    bcs_id: ChangesetId,
) -> Result<(), Error> {
    let mut txn = repo.bookmarks().create_transaction(ctx.clone());

    let prev_bcs_id = repo.bookmarks().get(ctx, bookmark).await?;

    match prev_bcs_id {
        Some(prev_bcs_id) => {
            txn.update(
                bookmark,
                bcs_id,
                prev_bcs_id,
                BookmarkUpdateReason::TestMove,
            )?;
        }
        None => {
            txn.create(bookmark, bcs_id, BookmarkUpdateReason::TestMove)?;
        }
    }

    assert!(txn.commit().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::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;

use anyhow::bail;
use anyhow::format_err;
use anyhow::Context;
use anyhow::Error;
use async_trait::async_trait;
use bookmarks::BookmarkKey;
use bookmarks::Freshness;
use cached_config::ConfigStore;
use cmdlib::args;
use cmdlib::args::MononokeMatches;
use cmdlib::helpers;
use cmdlib_x_repo::create_commit_syncers_from_matches;
use context::CoreContext;
use context::SessionContainer;
use cross_repo_sync::validation;
use cross_repo_sync::validation::BookmarkDiff;
use cross_repo_sync::CommitSyncOutcome;
use cross_repo_sync::CommitSyncer;
use cross_repo_sync::Repo as CrossRepo;
use cross_repo_sync::Syncers;
use executor_lib::RepoShardedProcess;
use executor_lib::RepoShardedProcessExecutor;
use executor_lib::ShardedProcessExecutor;
use fbinit::FacebookInit;
use futures::future;
use futures::TryStreamExt;
use live_commit_sync_config::CONFIGERATOR_PUSHREDIRECT_ENABLE;
use mononoke_types::ChangesetId;
use once_cell::sync::OnceCell;
use pushredirect_enable::types::MononokePushRedirectEnable;
use scuba_ext::MononokeScubaSampleBuilder;
use sharding_ext::RepoShard;
use slog::error;
use slog::info;
use slog::Logger;
use stats::prelude::*;
use synced_commit_mapping::SqlSyncedCommitMapping;
use synced_commit_mapping::SyncedCommitMapping;
use tokio::runtime::Runtime;

define_stats! {
    prefix = "mononoke.bookmark_validator";
    result_counter: dynamic_singleton_counter(
        "{}.{}",
        (large_repo_name: String, small_repo_name: String)
    ),
}

const SM_SERVICE_SCOPE: &str = "global";
const SM_CLEANUP_TIMEOUT_SECS: u64 = 120;
const APP_NAME: &str = "megarepo_bookmarks_validator";

type Repo = cross_repo_sync::ConcreteRepo;

/// Struct representing the Bookmark Validate BP.
pub struct BookmarkValidateProcess {
    matches: Arc<MononokeMatches<'static>>,
    fb: FacebookInit,
    _runtime: Runtime,
}

impl BookmarkValidateProcess {
    fn new(fb: FacebookInit) -> anyhow::Result<Self> {
        let app_name = "Tool to validate that small and large repo bookmarks are in sync";
        let app = args::MononokeAppBuilder::new(app_name)
            .with_source_and_target_repos()
            .with_dynamic_repos()
            .with_fb303_args()
            .build();
        let (matches, _runtime) = app.get_matches(fb)?;
        let matches = Arc::new(matches);
        Ok(Self {
            matches,
            fb,
            _runtime,
        })
    }
}

#[async_trait]
impl RepoShardedProcess for BookmarkValidateProcess {
    async fn setup(&self, repo: &RepoShard) -> anyhow::Result<Arc<dyn RepoShardedProcessExecutor>> {
        let logger = self.matches.logger();
        // For bookmark validator, two repos (i.e. source and target) are required as input
        let source_repo_name = repo.repo_name.clone();
        let target_repo_name = match repo.target_repo_name.clone() {
            Some(repo_name) => repo_name,
            None => {
                let details = format!(
                    "Only source repo name {} provided, target repo name missing in {}",
                    source_repo_name, repo
                );
                error!(logger, "{}", details);
                bail!("{}", details)
            }
        };
        info!(
            logger,
            "Setting up bookmark validate command from repo {} to repo {}",
            source_repo_name,
            target_repo_name,
        );
        let ctx =
            create_core_context(self.fb, logger.clone()).clone_with_repo_name(&repo.to_string());
        let config_store = self.matches.config_store().clone();
        let source_repo_id =
            args::resolve_repo_by_name(&config_store, &self.matches, &source_repo_name)?.id;
        let target_repo_id =
            args::resolve_repo_by_name(&config_store, &self.matches, &target_repo_name)?.id;

        let syncers = create_commit_syncers_from_matches::<Repo>(
            &ctx,
            &self.matches,
            Some((source_repo_id, target_repo_id)),
        )
        .await?;
        if syncers.large_to_small.get_large_repo().repo_identity().id() != source_repo_id {
            let details = format!(
                "Source repo must be a large repo!. Source repo: {}, Target repo: {}",
                source_repo_name, target_repo_name
            );
            error!(logger, "{}", details);
            bail!("{}", details);
        }
        let details = format!(
            "Completed bookmark validate command setup from repo {} to repo {}",
            source_repo_name, target_repo_name
        );
        let executor = BookmarkValidateProcessExecutor::new(
            syncers,
            ctx,
            config_store,
            source_repo_name,
            target_repo_name,
        );
        info!(logger, "{}", details,);
        Ok(Arc::new(executor))
    }
}

/// Struct representing the execution of the Bookmark Validate
/// BP over the context of a provided repos.
pub struct BookmarkValidateProcessExecutor {
    syncers: Syncers<SqlSyncedCommitMapping, Repo>,
    ctx: CoreContext,
    config_store: ConfigStore,
    cancellation_requested: Arc<AtomicBool>,
    source_repo_name: String,
    target_repo_name: String,
}

impl BookmarkValidateProcessExecutor {
    fn new(
        syncers: Syncers<SqlSyncedCommitMapping, Repo>,
        ctx: CoreContext,
        config_store: ConfigStore,
        source_repo_name: String,
        target_repo_name: String,
    ) -> Self {
        Self {
            syncers,
            ctx,
            config_store,
            source_repo_name,
            target_repo_name,
            cancellation_requested: Arc::new(AtomicBool::new(false)),
        }
    }
}

#[async_trait]
impl RepoShardedProcessExecutor for BookmarkValidateProcessExecutor {
    async fn execute(&self) -> anyhow::Result<()> {
        info!(
            self.ctx.logger(),
            "Initiating bookmark validate command execution for repo pair {}-{}",
            &self.source_repo_name,
            &self.target_repo_name,
        );
        loop_forever(
            self.ctx.clone(),
            self.syncers.clone(),
            &self.config_store,
            Arc::clone(&self.cancellation_requested),
        )
        .await
        .with_context(|| {
            format!(
                "Error during bookmark validate command execution for repo pair {}-{}",
                &self.source_repo_name, &self.target_repo_name,
            )
        })?;
        info!(
            self.ctx.logger(),
            "Finished bookmark validate command execution for repo pair {}-{}",
            &self.source_repo_name,
            self.target_repo_name
        );
        Ok(())
    }

    async fn stop(&self) -> anyhow::Result<()> {
        info!(
            self.ctx.logger(),
            "Terminating bookmark validate command execution for repo pair {}-{}",
            &self.source_repo_name,
            self.target_repo_name,
        );
        self.cancellation_requested.store(true, Ordering::Relaxed);
        Ok(())
    }
}

#[fbinit::main]
fn main(fb: FacebookInit) -> Result<(), Error> {
    let process = BookmarkValidateProcess::new(fb)?;
    match process.matches.value_of("sharded-service-name") {
        Some(service_name) => {
            // The service name needs to be 'static to satisfy SM contract
            static SM_SERVICE_NAME: OnceCell<String> = OnceCell::new();
            let logger = process.matches.logger().clone();
            let matches = Arc::clone(&process.matches);
            let mut executor = ShardedProcessExecutor::new(
                process.fb,
                process.matches.runtime().clone(),
                &logger,
                SM_SERVICE_NAME.get_or_init(|| service_name.to_string()),
                SM_SERVICE_SCOPE,
                SM_CLEANUP_TIMEOUT_SECS,
                Arc::new(process),
                true, // enable shard (repo) level healing
            )?;
            helpers::block_execute(
                executor.block_and_execute(&logger, Arc::new(AtomicBool::new(false))),
                fb,
                &std::env::var("TW_JOB_NAME").unwrap_or_else(|_| APP_NAME.to_string()),
                matches.logger(),
                &matches,
                cmdlib::monitoring::AliveService,
            )
        }
        None => {
            let logger = process.matches.logger().clone();
            let matches = process.matches.clone();
            let runtime = matches.runtime();
            let ctx = create_core_context(fb, logger.clone());
            let config_store = matches.config_store();
            let source_repo_id =
                args::not_shardmanager_compatible::get_source_repo_id(config_store, &matches)?;
            let syncers = runtime.block_on(create_commit_syncers_from_matches::<Repo>(
                &ctx, &matches, None,
            ))?;
            if syncers.large_to_small.get_large_repo().repo_identity().id() != source_repo_id {
                return Err(format_err!("Source repo must be a large repo!"));
            }
            helpers::block_execute(
                loop_forever(ctx, syncers, config_store, Arc::new(AtomicBool::new(false))),
                fb,
                APP_NAME,
                &logger,
                &matches,
                cmdlib::monitoring::AliveService,
            )
        }
    }
}

fn create_core_context(fb: FacebookInit, logger: Logger) -> CoreContext {
    let session_container = SessionContainer::new_with_defaults(fb);
    let scuba_sample = MononokeScubaSampleBuilder::with_discard();
    session_container.new_context(logger, scuba_sample)
}

async fn loop_forever<M: SyncedCommitMapping + Clone + 'static, R: CrossRepo>(
    ctx: CoreContext,
    syncers: Syncers<M, R>,
    config_store: &ConfigStore,
    cancellation_requested: Arc<AtomicBool>,
) -> Result<(), Error> {
    let large_repo_name = syncers
        .large_to_small
        .get_large_repo()
        .repo_identity()
        .name();
    let small_repo_name = syncers
        .large_to_small
        .get_small_repo()
        .repo_identity()
        .name();

    let small_repo_id = syncers.small_to_large.get_small_repo().repo_identity().id();

    let config_handle =
        config_store.get_config_handle(CONFIGERATOR_PUSHREDIRECT_ENABLE.to_string())?;

    loop {
        // Before initiating every iteration, check if cancellation has been requested.
        if cancellation_requested.load(Ordering::Relaxed) {
            info!(
                ctx.logger(),
                "bookmark validation stopping due to cancellation request"
            );
            return Ok(());
        }
        let config: Arc<MononokePushRedirectEnable> = config_handle.get();

        let enabled = config
            .per_repo
            .get(&(small_repo_id.id() as i64))
            // We only care about public pushes because draft pushes are not in the bookmark
            // update log at all.
            .map_or(false, |enables| enables.public_push);

        if enabled {
            let res = validate(&ctx, &syncers, large_repo_name, small_repo_name).await;
            if let Err(err) = res {
                match err {
                    ValidationError::InfraError(error) => {
                        error!(ctx.logger(), "infra error: {:?}", error);
                    }
                    ValidationError::ValidationError(err_msg) => {
                        STATS::result_counter.set_value(
                            ctx.fb,
                            0,
                            (large_repo_name.to_string(), small_repo_name.to_string()),
                        );
                        error!(ctx.logger(), "validation failed: {:?}", err_msg);
                    }
                }
            } else {
                STATS::result_counter.set_value(
                    ctx.fb,
                    1,
                    (large_repo_name.to_string(), small_repo_name.to_string()),
                );
            }
        } else {
            info!(ctx.logger(), "push redirector is disabled");
            // Log success to prevent alarm from going off
            STATS::result_counter.set_value(
                ctx.fb,
                1,
                (large_repo_name.to_string(), small_repo_name.to_string()),
            );
        }
        tokio::time::sleep(Duration::new(1, 0)).await;
    }
}

enum ValidationError {
    InfraError(Error),
    ValidationError(String),
}

impl From<Error> for ValidationError {
    fn from(error: Error) -> Self {
        Self::InfraError(error)
    }
}

async fn validate<M: SyncedCommitMapping + Clone + 'static, R: CrossRepo>(
    ctx: &CoreContext,
    syncers: &Syncers<M, R>,
    large_repo_name: &str,
    small_repo_name: &str,
) -> Result<(), ValidationError> {
    let commit_syncer = &syncers.small_to_large;
    let diffs = validation::find_bookmark_diff(ctx.clone(), commit_syncer).await?;

    info!(ctx.logger(), "got {} bookmark diffs", diffs.len());
    for diff in diffs {
        info!(ctx.logger(), "processing {:?}", diff);
        use BookmarkDiff::*;

        let (large_bookmark, large_cs_id, small_cs_id) = match diff {
            // Target is large, source is small here
            InconsistentValue {
                target_bookmark,
                target_cs_id,
                source_cs_id,
            } => (target_bookmark, Some(target_cs_id), source_cs_id),
            MissingInTarget {
                target_bookmark,
                source_cs_id,
            } => (target_bookmark, None, Some(source_cs_id)),
            NoSyncOutcome { target_bookmark } => {
                return Err(ValidationError::ValidationError(format!(
                    "unexpected no sync outcome for {}",
                    target_bookmark
                )));
            }
        };

        // Check that large_bookmark actually pointed to a commit equivalent to small_cs_id
        // not so long ago.
        let max_log_records: u32 = 100;
        let max_delay_secs: u32 = 300;
        let in_history = check_large_bookmark_history(
            ctx,
            syncers,
            &large_bookmark,
            &large_cs_id,
            &small_cs_id,
            max_log_records,
            max_delay_secs,
        )
        .await?;
        if in_history {
            info!(ctx.logger(), "all is well");
        } else {
            let err_msg = format!(
                "{} points to {:?} in {}, but points to {:?} in {}",
                large_bookmark, large_cs_id, large_repo_name, small_cs_id, small_repo_name,
            );
            return Err(ValidationError::ValidationError(err_msg));
        }
    }
    Ok(())
}

// Check that commit equivalent to maybe_small_cs_id was in large_bookmark log recently
async fn check_large_bookmark_history<M: SyncedCommitMapping + Clone + 'static, R: CrossRepo>(
    ctx: &CoreContext,
    syncers: &Syncers<M, R>,
    large_bookmark: &BookmarkKey,
    maybe_large_cs_id: &Option<ChangesetId>,
    maybe_small_cs_id: &Option<ChangesetId>,
    max_log_records: u32,
    max_delay_secs: u32,
) -> Result<bool, Error> {
    let small_to_large = &syncers.small_to_large;
    let large_to_small = &syncers.large_to_small;
    info!(ctx.logger(), "checking history of {}", large_bookmark);

    let large_repo = small_to_large.get_large_repo();
    // Log entries are sorted newest to oldest
    let log_entries: Vec<_> = large_repo
        .bookmark_update_log()
        .list_bookmark_log_entries(
            ctx.clone(),
            large_bookmark.clone(),
            max_log_records,
            None,
            Freshness::MostRecent,
        )
        .try_collect()
        .await?;

    // check_large_bookmark_history is called after current value of large bookmark (i.e.
    // maybe_large_cs_id) was fetched. That means that log_entries might contain newer bookmark
    // update log entries, so let's remove all the bookmark update log entries that are newer than
    // maybe_large_cs_id.
    let log_entries = log_entries
        .into_iter()
        .skip_while(|(_, book_val, _, _)| book_val != maybe_large_cs_id)
        .collect::<Vec<_>>();
    if log_entries.is_empty() {
        // We can't find the value of large bookmark in bookmark update log.
        return Ok(false);
    }

    if let Some((_, _, _, latest_timestamp)) = log_entries.get(0) {
        // Remap large repo commits into small repo commits
        // Note that in theory it's possible to map a small repo commit into a large repo and compare
        // only this remapped commit with the log of the large bookmark. However it doesn't work well
        // in practice - if two small repos are tailed into a large repo and one small repo is has
        // much more commits than the other, then latest max_log_records in the large repo might be
        // from the more active source repo. Hence check_large_bookmark_history might return 'false'
        // for the less active repo.
        let remapped_log_entries =
            log_entries
                .iter()
                .map(|(_, book_val, _, timestamp)| async move {
                    let res: Result<_, Error> = match book_val {
                        Some(large_cs_id) => {
                            let maybe_remapped_cs_id =
                                remap(ctx, large_to_small, large_cs_id).await?;
                            Ok(maybe_remapped_cs_id
                                .map(|remapped_cs_id| (Some(remapped_cs_id), timestamp)))
                        }
                        None => Ok(Some((None, timestamp))),
                    };
                    res
                });

        let remapped_log_entries = future::try_join_all(remapped_log_entries).await?;

        let maybe_log_entry = remapped_log_entries
            .into_iter()
            .filter_map(std::convert::identity)
            .find(|(maybe_remapped_cs_id, timestamp)| {
                // Delay is measured from the latest entry in the large repo bookmark update log
                let delay = latest_timestamp.timestamp_seconds() - timestamp.timestamp_seconds();
                (maybe_remapped_cs_id == maybe_small_cs_id) && (delay < max_delay_secs as i64)
            });

        if maybe_log_entry.is_some() {
            return Ok(true);
        }
    }
    // We haven't found an entry with the same id - check that bookmark might have
    // been created recently
    let was_created = log_entries.len() < (max_log_records as usize);
    if was_created && maybe_small_cs_id.is_none() {
        match log_entries.last() {
            Some((_, _, _, timestamp)) => Ok(timestamp.since_seconds() < max_delay_secs as i64),
            None => {
                // Shouldn't happen in practive, so return false in that case
                Ok(false)
            }
        }
    } else {
        Ok(false)
    }
}

async fn remap<M: SyncedCommitMapping + Clone + 'static, R: CrossRepo>(
    ctx: &CoreContext,
    commit_syncer: &CommitSyncer<M, R>,
    source_cs_id: &ChangesetId,
) -> Result<Option<ChangesetId>, Error> {
    let maybe_commit_sync_outcome = commit_syncer
        .get_commit_sync_outcome(ctx, *source_cs_id)
        .await?;

    use CommitSyncOutcome::*;

    match maybe_commit_sync_outcome {
        None | Some(NotSyncCandidate(_)) => Ok(None),
        Some(RewrittenAs(cs_id, _)) | Some(EquivalentWorkingCopyAncestor(cs_id, _)) => {
            Ok(Some(cs_id))
        }
    }
}

#[cfg(test)]
mod tests {
    use cross_repo_sync::CandidateSelectionHint;
    use cross_repo_sync::CommitSyncContext;
    use cross_repo_sync_test_utils::init_small_large_repo;
    use mononoke_types::DateTime;
    use tests_utils::bookmark;
    use tests_utils::resolve_cs_id;
    use tests_utils::CreateCommitContext;

    use super::*;

    #[fbinit::test]
    async fn test_simple_check_large_bookmark_history(fb: FacebookInit) -> Result<(), Error> {
        let ctx = CoreContext::test_mock(fb);
        let (syncers, _, _, _) = init_small_large_repo(&ctx).await?;
        let small_to_large = &syncers.small_to_large;
        let large_repo = small_to_large.get_large_repo();
        let small_repo = small_to_large.get_small_repo();
        let small_master = resolve_cs_id(&ctx, small_repo, "master").await?;
        // Arbitrary large number
        let max_delay_secs = 10000000;
        let large_master = resolve_cs_id(&ctx, large_repo, "master").await?;
        let in_history = check_large_bookmark_history(
            &ctx,
            &syncers,
            &BookmarkKey::new("master")?,
            &Some(large_master),
            &Some(small_master),
            100,
            max_delay_secs,
        )
        .await?;
        assert!(in_history);

        // "master" moved, but it still in history
        let large_repo = small_to_large.get_large_repo();
        bookmark(&ctx, &large_repo, "master")
            .set_to("premove")
            .await?;
        let large_master = resolve_cs_id(&ctx, large_repo, "master").await?;
        let in_history = check_large_bookmark_history(
            &ctx,
            &syncers,
            &BookmarkKey::new("master")?,
            &Some(large_master),
            &Some(small_master),
            100,
            max_delay_secs,
        )
        .await?;
        assert!(in_history);

        // Now check with only one log record allowed - shouldn't be in history
        let in_history = check_large_bookmark_history(
            &ctx,
            &syncers,
            &BookmarkKey::new("master")?,
            &Some(large_master),
            &Some(small_master),
            1,
            max_delay_secs,
        )
        .await?;
        assert!(!in_history);

        // Create a new commit - not in master history
        let cs_id = CreateCommitContext::new(&ctx, small_repo, vec!["master"])
            .add_file("somefile", "content")
            .commit()
            .await?;

        let in_history = check_large_bookmark_history(
            &ctx,
            &syncers,
            &BookmarkKey::new("master")?,
            &Some(large_master),
            &Some(cs_id),
            100,
            max_delay_secs,
        )
        .await?;
        assert!(!in_history);

        Ok(())
    }

    #[fbinit::test]
    async fn test_another_repo_check_large_bookmark_history(fb: FacebookInit) -> Result<(), Error> {
        let ctx = CoreContext::test_mock(fb);
        let (syncers, _, _, _) = init_small_large_repo(&ctx).await?;
        let small_to_large = &syncers.small_to_large;

        let small_repo = small_to_large.get_small_repo();
        let large_repo = small_to_large.get_large_repo();
        let small_master = resolve_cs_id(&ctx, small_repo, "master").await?;

        // Move master a few times in large with commits that do not remap to a small repo
        let mut last = None;
        for i in 1..10 {
            let cs_id = CreateCommitContext::new(&ctx, large_repo, vec!["master"])
                .add_file("somefile", format!("content{}", i))
                .commit()
                .await?;
            bookmark(&ctx, &large_repo, "master").set_to(cs_id).await?;
            last = Some(cs_id);
        }
        syncers
            .large_to_small
            .sync_commit(
                &ctx,
                last.unwrap(),
                CandidateSelectionHint::Only,
                CommitSyncContext::Tests,
                false,
            )
            .await?;

        // Since all commits were from another repo, large repo's master still remaps
        // to small repo master, so it's in history
        let max_delay_secs = 10000000;
        let large_master = resolve_cs_id(&ctx, large_repo, "master").await?;
        let in_history = check_large_bookmark_history(
            &ctx,
            &syncers,
            &BookmarkKey::new("master")?,
            &Some(large_master),
            &Some(small_master),
            1,
            max_delay_secs,
        )
        .await?;
        assert!(in_history);

        // But now move a master a few times with commits that remap to small repo
        // (note "prefix/" in the filename).
        // In that case validation should fail.
        for i in 1..10 {
            let cs_id = CreateCommitContext::new(&ctx, large_repo, vec!["master"])
                .add_file("prefix/somefile", format!("content{}", i))
                .commit()
                .await?;
            bookmark(&ctx, &large_repo, "master").set_to(cs_id).await?;
        }
        let large_master = resolve_cs_id(&ctx, large_repo, "master").await?;
        let in_history = check_large_bookmark_history(
            &ctx,
            &syncers,
            &BookmarkKey::new("master")?,
            &Some(large_master),
            &Some(small_master),
            1,
            max_delay_secs,
        )
        .await?;
        assert!(!in_history);
        Ok(())
    }

    #[fbinit::test]
    async fn test_recently_created_check_large_bookmark_history(
        fb: FacebookInit,
    ) -> Result<(), Error> {
        let ctx = CoreContext::test_mock(fb);
        let (syncers, _, _, _) = init_small_large_repo(&ctx).await?;
        let small_to_large = &syncers.small_to_large;
        let large_repo = small_to_large.get_large_repo();

        // Move master a few times
        for i in 1..10 {
            let cs_id = CreateCommitContext::new(&ctx, large_repo, vec!["master"])
                .add_file("somefile", format!("content{}", i))
                .commit()
                .await?;
            bookmark(&ctx, &large_repo, "master").set_to(cs_id).await?;
        }

        bookmark(&ctx, &large_repo, "newbook")
            .set_to("master")
            .await?;

        // Bookmark was recently created - it's ok if it's not present in small repo
        let max_delay_secs = 10000000;
        let large_master = resolve_cs_id(&ctx, large_repo, "master").await?;
        let in_history = check_large_bookmark_history(
            &ctx,
            &syncers,
            &BookmarkKey::new("newbook")?,
            &Some(large_master),
            &None,
            5,
            max_delay_secs,
        )
        .await?;
        assert!(in_history);

        // However it's not ok for master to not be present in the repo
        let in_history = check_large_bookmark_history(
            &ctx,
            &syncers,
            &BookmarkKey::new("master")?,
            &Some(large_master),
            &None,
            5,
            max_delay_secs,
        )
        .await?;
        assert!(!in_history);

        Ok(())
    }

    #[fbinit::test]
    async fn test_deleted_added_back_created_check_large_bookmark_history(
        fb: FacebookInit,
    ) -> Result<(), Error> {
        let ctx = CoreContext::test_mock(fb);
        let (syncers, _, _, _) = init_small_large_repo(&ctx).await?;
        let small_to_large = &syncers.small_to_large;
        let large_repo = small_to_large.get_large_repo();

        bookmark(&ctx, &large_repo, "master").delete().await?;
        bookmark(&ctx, &large_repo, "master")
            .set_to("premove")
            .await?;

        // Recently deleted - should be in history
        let max_delay_secs = 10000000;
        let large_master = resolve_cs_id(&ctx, large_repo, "master").await?;
        let in_history = check_large_bookmark_history(
            &ctx,
            &syncers,
            &BookmarkKey::new("master")?,
            &Some(large_master),
            &None,
            2,
            max_delay_secs,
        )
        .await?;
        assert!(in_history);
        Ok(())
    }

    #[fbinit::test]
    async fn test_check_large_bookmark_history_after_bookmark_moved(
        fb: FacebookInit,
    ) -> Result<(), Error> {
        let ctx = CoreContext::test_mock(fb);
        let (syncers, _, _, _) = init_small_large_repo(&ctx).await?;
        let small_to_large = &syncers.small_to_large;
        let small_repo = small_to_large.get_small_repo();
        let large_repo = small_to_large.get_large_repo();

        let small_master = resolve_cs_id(&ctx, small_repo, "master").await?;

        // Wait a little bit
        tokio::time::sleep(Duration::from_secs(2)).await;

        // Move a bookmark in the large repo
        let old_large_master = resolve_cs_id(&ctx, large_repo, "master").await?;
        let new_master = CreateCommitContext::new(&ctx, &large_repo, vec![old_large_master])
            .add_file("prefix/somefile", "somecontent")
            .set_author_date(DateTime::now())
            .commit()
            .await?;
        bookmark(&ctx, &large_repo, "master")
            .set_to(new_master)
            .await?;
        syncers
            .large_to_small
            .sync_commit(
                &ctx,
                new_master,
                CandidateSelectionHint::Only,
                CommitSyncContext::Tests,
                false,
            )
            .await?;

        let max_delay_secs = 1;
        let in_history = check_large_bookmark_history(
            &ctx,
            &syncers,
            &BookmarkKey::new("master")?,
            &Some(old_large_master),
            &Some(small_master),
            100,
            max_delay_secs,
        )
        .await?;
        assert!(in_history);

        Ok(())
    }
}