/*
 * 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 anyhow::Result;
use bookmarks::BookmarkName;
use mononoke_types::RepositoryId;
use mononoke_types::Timestamp;
use sql::mysql;
use sql::mysql_async::from_value_opt;
use sql::mysql_async::prelude::ConvIr;
use sql::mysql_async::prelude::FromValue;
use sql::mysql_async::FromValueError;
use sql::mysql_async::Value;

#[derive(Clone, Copy, Debug, Eq, PartialEq, mysql::OptTryFromRowField)]
pub struct RowId(pub u64);

impl From<RowId> for Value {
    fn from(id: RowId) -> Self {
        Value::UInt(id.0)
    }
}

impl std::fmt::Display for RowId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl ConvIr<RowId> for RowId {
    fn new(v: Value) -> Result<Self, FromValueError> {
        Ok(RowId(from_value_opt(v)?))
    }
    fn commit(self) -> Self {
        self
    }
    fn rollback(self) -> Value {
        self.into()
    }
}

impl FromValue for RowId {
    type Intermediate = RowId;
}

macro_rules! mysql_string_newtype {
    ($ty: ident) => {
        #[derive(Clone, Debug, Eq, PartialEq, mysql::OptTryFromRowField)]
        pub struct $ty(pub String);

        impl std::fmt::Display for $ty {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, "{}", self.0)
            }
        }

        impl From<$ty> for Value {
            fn from(val: $ty) -> Self {
                Value::Bytes(val.0.into_bytes())
            }
        }

        impl ConvIr<$ty> for $ty {
            fn new(v: Value) -> Result<Self, FromValueError> {
                match v {
                    Value::Bytes(bytes) => match String::from_utf8(bytes) {
                        Ok(s) => Ok($ty(s)),
                        Err(from_utf8_error) => {
                            Err(FromValueError(Value::Bytes(from_utf8_error.into_bytes())))
                        }
                    },
                    v => Err(FromValueError(v)),
                }
            }

            fn commit(self) -> $ty {
                self
            }

            fn rollback(self) -> Value {
                self.into()
            }
        }

        impl FromValue for $ty {
            type Intermediate = $ty;
        }
    };
}

mysql_string_newtype!(BlobstoreKey);
mysql_string_newtype!(RequestType);
mysql_string_newtype!(ClaimedBy);

#[derive(Clone, Copy, Debug, Eq, PartialEq, mysql::OptTryFromRowField)]
pub enum RequestStatus {
    New,
    InProgress,
    Ready,
    Polled,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LongRunningRequestEntry {
    pub id: RowId,
    pub repo_id: RepositoryId,
    pub bookmark: BookmarkName,
    pub request_type: RequestType,
    pub args_blobstore_key: BlobstoreKey,
    pub result_blobstore_key: Option<BlobstoreKey>,
    pub created_at: Timestamp,
    pub started_processing_at: Option<Timestamp>,
    pub inprogress_last_updated_at: Option<Timestamp>,
    pub ready_at: Option<Timestamp>,
    pub polled_at: Option<Timestamp>,
    pub status: RequestStatus,
    pub claimed_by: Option<ClaimedBy>,
}

impl std::fmt::Display for RequestStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        use RequestStatus::*;

        let s = match self {
            New => "new",
            InProgress => "inprogress",
            Ready => "ready",
            Polled => "polled",
        };
        write!(f, "{}", s)
    }
}

impl ConvIr<RequestStatus> for RequestStatus {
    fn new(v: Value) -> Result<Self, FromValueError> {
        use RequestStatus::*;

        match v {
            Value::Bytes(ref b) if b == b"new" => Ok(New),
            Value::Bytes(ref b) if b == b"inprogress" => Ok(InProgress),
            Value::Bytes(ref b) if b == b"ready" => Ok(Ready),
            Value::Bytes(ref b) if b == b"polled" => Ok(Polled),
            v => Err(FromValueError(v)),
        }
    }

    fn commit(self) -> RequestStatus {
        self
    }

    fn rollback(self) -> Value {
        self.into()
    }
}

impl FromValue for RequestStatus {
    type Intermediate = RequestStatus;
}

impl From<RequestStatus> for Value {
    fn from(status: RequestStatus) -> Self {
        use RequestStatus::*;

        match status {
            New => Value::Bytes(b"new".to_vec()),
            InProgress => Value::Bytes(b"inprogress".to_vec()),
            Ready => Value::Bytes(b"ready".to_vec()),
            Polled => Value::Bytes(b"polled".to_vec()),
        }
    }
}

/// A full identified for a request
/// Note: while RowId is guaranteed to be unique in the table,
///       it is generally illegal to make queries without knowing
///       which request type you are talking about
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RequestId(pub RowId, pub RequestType);


/*
 * 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::Arc;

use context::CoreContext;
use megarepo_config::MononokeMegarepoConfigs;
use megarepo_config::SyncTargetConfig;
use megarepo_config::Target;
use megarepo_error::MegarepoError;
use mononoke_api::Mononoke;
use mononoke_types::ChangesetId;

use crate::common::find_target_sync_config;
use crate::common::MegarepoOp;

// Create a new sync target
pub struct AddBranchingSyncTarget<'a> {
    pub megarepo_configs: &'a Arc<dyn MononokeMegarepoConfigs>,
    pub mononoke: &'a Arc<Mononoke>,
}

impl<'a> MegarepoOp for AddBranchingSyncTarget<'a> {
    fn mononoke(&self) -> &Arc<Mononoke> {
        self.mononoke
    }
}

impl<'a> AddBranchingSyncTarget<'a> {
    pub fn new(
        megarepo_configs: &'a Arc<dyn MononokeMegarepoConfigs>,
        mononoke: &'a Arc<Mononoke>,
    ) -> Self {
        Self {
            megarepo_configs,
            mononoke,
        }
    }

    pub async fn run(
        self,
        ctx: &CoreContext,
        sync_target_config: SyncTargetConfig,
        branching_point: ChangesetId,
    ) -> Result<ChangesetId, MegarepoError> {
        let repo = self
            .find_repo_by_id(ctx, sync_target_config.target.repo_id)
            .await?;
        let bookmark = sync_target_config.target.bookmark.clone();

        self.megarepo_configs
            .add_config_version(ctx.clone(), sync_target_config)
            .await?;
        self.create_bookmark(ctx, repo.inner_repo(), bookmark, branching_point)
            .await?;
        Ok(branching_point)
    }

    pub async fn fork_new_sync_target_config(
        &self,
        ctx: &CoreContext,
        target: Target,
        branching_point: ChangesetId,
        source_target: Target,
    ) -> Result<SyncTargetConfig, MegarepoError> {
        let repo = self.find_repo_by_id(ctx, target.repo_id).await?;

        let (_, sync_target_config) = find_target_sync_config(
            ctx,
            repo.inner_repo(),
            branching_point,
            &source_target,
            self.megarepo_configs,
        )
        .await?;

        let sync_target_config = SyncTargetConfig {
            target,
            ..sync_target_config
        };
        Ok(sync_target_config)
    }
}


/*
 * 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::Arc;

use anyhow::Error;
use context::CoreContext;
use fbinit::FacebookInit;
use maplit::btreemap;
use megarepo_config::MononokeMegarepoConfigs;
use megarepo_config::Target;
use megarepo_mapping::SourceName;
use tests_utils::bookmark;
use tests_utils::resolve_cs_id;
use tests_utils::CreateCommitContext;

use crate::add_branching_sync_target::AddBranchingSyncTarget;
use crate::add_sync_target::AddSyncTarget;
use crate::megarepo_test_utils::MegarepoTest;
use crate::megarepo_test_utils::SyncTargetConfigBuilder;

#[fbinit::test]
async fn test_add_branching_sync_target_success(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let target: Target = test.target("target".to_string());

    let first_source_name = SourceName::new("source_1");
    let version = "version_1".to_string();
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .build_source()?
        .build(&mut test.configs_storage);

    println!("Create initial source commits and bookmarks");
    let first_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("first", "first")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, first_source_name.to_string())
        .set_to(first_source_cs_id)
        .await?;

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());

    let sync_target_config =
        test.configs_storage
            .get_config_by_version(ctx.clone(), target.clone(), version.clone())?;
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    add_sync_target
        .run(
            &ctx,
            sync_target_config,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
            },
            None,
        )
        .await?;

    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;

    println!("Create new release branch");
    let new_target = test.target("release".to_string());
    let add_branching_sync_target = AddBranchingSyncTarget::new(&configs_storage, &test.mononoke);
    let new_config = add_branching_sync_target
        .fork_new_sync_target_config(&ctx, new_target, target_cs_id, target.clone())
        .await?;
    let new_cs_id = add_branching_sync_target
        .run(&ctx, new_config, target_cs_id)
        .await?;
    assert_eq!(
        new_cs_id,
        resolve_cs_id(&ctx, &test.blobrepo, "release").await?
    );

    Ok(())
}

#[fbinit::test]
async fn test_add_branching_sync_target_no_source(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let test = MegarepoTest::new(&ctx).await?;
    let target: Target = test.target("target".to_string());
    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());

    let target_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("first", "first")
        .commit()
        .await?;

    let new_target = test.target("release".to_string());
    let add_branching_sync_target = AddBranchingSyncTarget::new(&configs_storage, &test.mononoke);
    assert!(
        add_branching_sync_target
            .fork_new_sync_target_config(&ctx, new_target, target_cs_id, target.clone())
            .await
            .is_err(),
        "Found a sync target config that doesn't exist!"
    );

    Ok(())
}

#[fbinit::test]
async fn test_add_branching_sync_target_wrong_branch(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let target: Target = test.target("target".to_string());

    let first_source_name = SourceName::new("source_1");
    let version = "version_1".to_string();
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .build_source()?
        .build(&mut test.configs_storage);

    println!("Create initial source commits and bookmarks");
    let first_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("first", "first")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, first_source_name.to_string())
        .set_to(first_source_cs_id)
        .await?;

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());

    let sync_target_config =
        test.configs_storage
            .get_config_by_version(ctx.clone(), target.clone(), version.clone())?;
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    add_sync_target
        .run(
            &ctx,
            sync_target_config,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
            },
            None,
        )
        .await?;

    let second_source_name = SourceName::new("source_2");
    let alt_version = "alt_version_1".to_string();
    let alt_target: Target = test.target("alt_target".to_string());
    SyncTargetConfigBuilder::new(test.repo_id(), alt_target.clone(), alt_version.clone())
        .source_builder(second_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .build_source()?
        .build(&mut test.configs_storage);

    println!("Create alternate source commits and bookmarks");
    let second_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("third", "third")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, second_source_name.to_string())
        .set_to(second_source_cs_id)
        .await?;

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());

    let sync_target_config = test.configs_storage.get_config_by_version(
        ctx.clone(),
        alt_target.clone(),
        alt_version.clone(),
    )?;
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    add_sync_target
        .run(
            &ctx,
            sync_target_config,
            btreemap! {
                second_source_name.clone() => second_source_cs_id,
            },
            None,
        )
        .await?;

    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "alt_target").await?;

    println!("Create new release branch against alt_target's unification commit");
    let new_target = test.target("release".to_string());
    let add_branching_sync_target = AddBranchingSyncTarget::new(&configs_storage, &test.mononoke);
    assert!(
        add_branching_sync_target
            .fork_new_sync_target_config(&ctx, new_target, target_cs_id, target.clone())
            .await
            .is_err(),
        "Found a sync target config for the wrong branch"
    );

    Ok(())
}


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

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

use bookmarks::BookmarkKey;
use bookmarks::BookmarksRef;
use context::CoreContext;
use derived_data_utils::derived_data_utils;
use futures::future;
use futures::stream::FuturesUnordered;
use futures::TryFutureExt;
use futures::TryStreamExt;
use megarepo_config::verify_config;
use megarepo_config::MononokeMegarepoConfigs;
use megarepo_config::SyncTargetConfig;
use megarepo_error::MegarepoError;
use megarepo_mapping::SourceName;
use mononoke_api::Mononoke;
use mononoke_api::RepoContext;
use mononoke_types::ChangesetId;
use mutable_renames::MutableRenames;
use repo_derived_data::RepoDerivedDataArc;
use repo_derived_data::RepoDerivedDataRef;

use crate::common::MegarepoOp;

// Create a new sync target given a config.
// After this command finishes it creates
// move commits on top of source commits
// and also merges them all together.
//
//      Tn
//      | \
//     ...
//      |
//      T1
//     / \
//    M   M
//   /     \
//  S       S
//
// Tx - target merge commits
// M - move commits
// S - source commits that need to be merged
pub struct AddSyncTarget<'a> {
    pub megarepo_configs: &'a Arc<dyn MononokeMegarepoConfigs>,
    pub mononoke: &'a Arc<Mononoke>,
    pub mutable_renames: &'a Arc<MutableRenames>,
}

impl<'a> MegarepoOp for AddSyncTarget<'a> {
    fn mononoke(&self) -> &Arc<Mononoke> {
        self.mononoke
    }
}

impl<'a> AddSyncTarget<'a> {
    pub fn new(
        megarepo_configs: &'a Arc<dyn MononokeMegarepoConfigs>,
        mononoke: &'a Arc<Mononoke>,
        mutable_renames: &'a Arc<MutableRenames>,
    ) -> Self {
        Self {
            megarepo_configs,
            mononoke,
            mutable_renames,
        }
    }

    pub async fn run(
        self,
        ctx: &CoreContext,
        sync_target_config: SyncTargetConfig,
        changesets_to_merge: BTreeMap<SourceName, ChangesetId>,
        message: Option<String>,
    ) -> Result<ChangesetId, MegarepoError> {
        let mut scuba = ctx.scuba().clone();

        verify_config(ctx, &sync_target_config).map_err(MegarepoError::request)?;

        let repo = self
            .find_repo_by_id(ctx, sync_target_config.target.repo_id)
            .await?;

        let maybe_already_done = self
            .check_if_this_method_has_already_succeeded(
                ctx,
                &sync_target_config,
                &changesets_to_merge,
                &repo,
            )
            .await?;
        if let Some(already_done) = maybe_already_done {
            // The same request has already succeeded, nothing to do.
            return Ok(already_done);
        }

        // First let's create commit on top of all source commits that
        // move all files in a correct place
        let moved_commits = self
            .create_move_commits(
                ctx,
                repo.inner_repo(),
                &sync_target_config.sources,
                &changesets_to_merge,
                self.mutable_renames,
            )
            .await?;
        scuba.log_with_msg("Created move commits", None);

        // Now let's merge all the moved commits together
        let top_merge_cs_id = self
            .create_merge_commits(
                ctx,
                repo.inner_repo(),
                moved_commits,
                true, /* write_commit_remapping_state */
                sync_target_config.version.clone(),
                message,
            )
            .await?;

        scuba.log_with_msg(
            "Created add sync target merge commit",
            Some(format!("{}", top_merge_cs_id)),
        );

        // add_sync_target might need to derive a lot of data, and it takes a long time to
        // do it. We don't have any resumability, so if it fails for any reason, then we'd
        // need to start over.

        // For now let's just retry a few times so that we don't have to start over
        // because of flakiness
        let mut i = 0;
        loop {
            i += 1;
            let derived_data_types = repo
                .blob_repo()
                .repo_derived_data()
                .active_config()
                .types
                .iter();
            let derivers = FuturesUnordered::new();
            for ty in derived_data_types {
                let utils = derived_data_utils(ctx.fb, repo.blob_repo(), ty)?;
                derivers.push(utils.derive(
                    ctx.clone(),
                    repo.blob_repo().repo_derived_data_arc(),
                    top_merge_cs_id,
                ));
            }

            let res = derivers.try_for_each(|_| future::ready(Ok(()))).await;
            match res {
                Ok(()) => {
                    break;
                }
                Err(err) => {
                    scuba.log_with_msg("Derived data failed, retrying", Some(format!("{:#}", err)));
                    if i >= 5 {
                        return Err(err.into());
                    }
                }
            }
        }

        scuba.log_with_msg("Derived data", None);

        self.megarepo_configs
            .add_config_version(ctx.clone(), sync_target_config.clone())
            .await?;

        self.create_bookmark(
            ctx,
            repo.inner_repo(),
            sync_target_config.target.bookmark,
            top_merge_cs_id,
        )
        .await?;

        Ok(top_merge_cs_id)
    }

    // If that add_sync_target() call was successful, but failed to send
    // successful result to the client (e.g. network issues) then
    // client will retry a request. We need to detect this situation and
    // send a successful response to the client.
    async fn check_if_this_method_has_already_succeeded(
        &self,
        ctx: &CoreContext,
        sync_target_config: &SyncTargetConfig,
        changesets_to_merge: &BTreeMap<SourceName, ChangesetId>,
        repo: &RepoContext,
    ) -> Result<Option<ChangesetId>, MegarepoError> {
        let bookmark_name = &sync_target_config.target.bookmark;
        let bookmark = BookmarkKey::new(bookmark_name).map_err(MegarepoError::request)?;

        let maybe_cs_id = repo
            .blob_repo()
            .bookmarks()
            .get(ctx.clone(), &bookmark)
            .map_err(MegarepoError::internal)
            .await?;

        let cs_id = match maybe_cs_id {
            Some(cs_id) => cs_id,
            None => {
                // Bookmark just doesn't exist - proceed the method as planned
                return Ok(None);
            }
        };

        // Bookmark exists - let's see if changeset it points to was created
        // by a previous add_sync_target call

        // First let's check if that config from request is the same as what's stored
        // our config storage
        self.check_if_new_sync_target_config_is_equivalent_to_already_existing(
            ctx,
            self.megarepo_configs,
            sync_target_config,
        )
        .await?;

        self.check_if_commit_has_expected_remapping_state(
            ctx,
            cs_id,
            &sync_target_config.version,
            changesets_to_merge,
            repo,
        )
        .await?;

        Ok(Some(cs_id))
    }
}


/*
 * 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::Arc;

use anyhow::Error;
use blobstore::Loadable;
use context::CoreContext;
use fbinit::FacebookInit;
use maplit::btreemap;
use maplit::hashmap;
use megarepo_config::MononokeMegarepoConfigs;
use megarepo_config::Target;
use megarepo_mapping::CommitRemappingState;
use megarepo_mapping::SourceName;
use megarepo_mapping::REMAPPING_STATE_FILE;
use mononoke_types::FileType;
use mononoke_types::MPath;
use repo_blobstore::RepoBlobstoreRef;
use tests_utils::bookmark;
use tests_utils::list_working_copy_utf8;
use tests_utils::list_working_copy_utf8_with_types;
use tests_utils::resolve_cs_id;
use tests_utils::CreateCommitContext;

use crate::add_sync_target::AddSyncTarget;
use crate::megarepo_test_utils::MegarepoTest;
use crate::megarepo_test_utils::SyncTargetConfigBuilder;
use crate::sync_changeset::SyncChangeset;

#[fbinit::test]
async fn test_add_sync_target_simple(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let target: Target = test.target("target".to_string());

    let first_source_name = SourceName::new("source_1");
    let second_source_name = SourceName::new("source_2");
    let version = "version_1".to_string();
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .build_source()?
        .source_builder(second_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .build_source()?
        .build(&mut test.configs_storage);

    println!("Create initial source commits and bookmarks");
    let first_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("first", "first")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, first_source_name.to_string())
        .set_to(first_source_cs_id)
        .await?;

    let second_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("second", "second")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, second_source_name.to_string())
        .set_to(second_source_cs_id)
        .await?;

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());

    let sync_target_config =
        test.configs_storage
            .get_config_by_version(ctx.clone(), target.clone(), version.clone())?;
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    add_sync_target
        .run(
            &ctx,
            sync_target_config,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                second_source_name.clone() => second_source_cs_id,
            },
            None,
        )
        .await?;

    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;
    let mut wc = list_working_copy_utf8(&ctx, &test.blobrepo, target_cs_id).await?;

    let state =
        CommitRemappingState::read_state_from_commit(&ctx, &test.blobrepo, target_cs_id).await?;
    assert_eq!(
        state.get_latest_synced_changeset(&first_source_name),
        Some(&first_source_cs_id),
    );
    assert_eq!(
        state.get_latest_synced_changeset(&second_source_name),
        Some(&second_source_cs_id),
    );
    assert_eq!(state.sync_config_version(), &version);

    // Remove file with commit remapping state because it's never present in source
    assert!(wc.remove(&MPath::new(REMAPPING_STATE_FILE)?).is_some());

    assert_eq!(
        wc,
        hashmap! {
            MPath::new("source_1/first")? => "first".to_string(),
            MPath::new("source_2/second")? => "second".to_string(),
        }
    );

    // Sync a few changesets on top of target
    let cs_id = CreateCommitContext::new(&ctx, &test.blobrepo, vec![first_source_cs_id])
        .add_file("first", "first_updated")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, first_source_name.to_string())
        .set_to(cs_id)
        .await?;

    let sync_changeset = SyncChangeset::new(
        &configs_storage,
        &test.mononoke,
        &test.megarepo_mapping,
        &test.mutable_renames,
    );

    sync_changeset
        .sync(&ctx, cs_id, &first_source_name, &target, target_cs_id)
        .await?;

    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;
    let mut wc = list_working_copy_utf8(&ctx, &test.blobrepo, target_cs_id).await?;
    // Remove file with commit remapping state because it's never present in source
    assert!(wc.remove(&MPath::new(REMAPPING_STATE_FILE)?).is_some());

    assert_eq!(
        wc,
        hashmap! {
            MPath::new("source_1/first")? => "first_updated".to_string(),
            MPath::new("source_2/second")? => "second".to_string(),
        }
    );

    Ok(())
}

#[fbinit::test]
async fn test_add_sync_target_with_linkfiles(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let target: Target = test.target("target".to_string());

    let first_source_name = SourceName::new("source_1");
    let second_source_name = SourceName::new("source_2");
    let version = "version_1".to_string();
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .linkfile("first", "linkfiles/first")
        .build_source()?
        .source_builder(second_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .linkfile("second", "linkfiles/second")
        .build_source()?
        .build(&mut test.configs_storage);

    println!("Create initial source commits and bookmarks");
    let first_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("first", "first")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, first_source_name.to_string())
        .set_to(first_source_cs_id)
        .await?;

    let second_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("second", "second")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, second_source_name.to_string())
        .set_to(second_source_cs_id)
        .await?;

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());

    let sync_target_config =
        test.configs_storage
            .get_config_by_version(ctx.clone(), target, version.clone())?;
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    add_sync_target
        .run(
            &ctx,
            sync_target_config,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                second_source_name.clone() => second_source_cs_id,
            },
            None,
        )
        .await?;

    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;
    let mut wc = list_working_copy_utf8_with_types(&ctx, &test.blobrepo, target_cs_id).await?;

    // Remove file with commit remapping state because it's never present in source
    assert!(wc.remove(&MPath::new(REMAPPING_STATE_FILE)?).is_some());

    assert_eq!(
        wc,
        hashmap! {
            MPath::new("source_1/first")? => ("first".to_string(), FileType::Regular),
            MPath::new("source_2/second")? => ("second".to_string(), FileType::Regular),
            MPath::new("linkfiles/first")? => ("../source_1/first".to_string(), FileType::Symlink),
            MPath::new("linkfiles/second")? => ("../source_2/second".to_string(), FileType::Symlink),
        }
    );

    Ok(())
}

#[fbinit::test]
async fn test_add_sync_target_invalid_same_prefix(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let target: Target = test.target("target".to_string());

    let first_source_name = SourceName::new("source_1");
    let second_source_name = SourceName::new("source_2");
    let version = "version_1".to_string();
    // Use the same prefix so that files from different repos collided
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version.clone())
        .source_builder(first_source_name.clone())
        .default_prefix("prefix")
        .bookmark("source_1")
        .build_source()?
        .source_builder(second_source_name.clone())
        .default_prefix("prefix")
        .bookmark("source_2")
        .build_source()?
        .build(&mut test.configs_storage);

    println!("Create initial source commits and bookmarks");
    let first_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("file", "content")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, first_source_name.to_string())
        .set_to(first_source_cs_id)
        .await?;

    let second_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("file", "content")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, second_source_name.to_string())
        .set_to(second_source_cs_id)
        .await?;

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());

    let sync_target_config =
        test.configs_storage
            .get_config_by_version(ctx.clone(), target, version.clone())?;
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    let res = add_sync_target
        .run(
            &ctx,
            sync_target_config,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                second_source_name.clone() => second_source_cs_id,
            },
            None,
        )
        .await;

    assert!(res.is_err());

    Ok(())
}

#[fbinit::test]
async fn test_add_sync_target_same_file_different_prefix(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let target: Target = test.target("target".to_string());

    let first_source_name = SourceName::new("source_1");
    let second_source_name = SourceName::new("source_2");
    let version = "version_1".to_string();
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .build_source()?
        .source_builder(second_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .build_source()?
        .build(&mut test.configs_storage);

    println!("Create initial source commits and bookmarks");
    let first_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("file", "file")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, first_source_name.to_string())
        .set_to(first_source_cs_id)
        .await?;

    let second_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("file", "file")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, second_source_name.to_string())
        .set_to(second_source_cs_id)
        .await?;

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());

    let sync_target_config =
        test.configs_storage
            .get_config_by_version(ctx.clone(), target.clone(), version.clone())?;
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    add_sync_target
        .run(
            &ctx,
            sync_target_config,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                second_source_name.clone() => second_source_cs_id,
            },
            None,
        )
        .await?;

    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;
    let mut wc = list_working_copy_utf8(&ctx, &test.blobrepo, target_cs_id).await?;

    let state =
        CommitRemappingState::read_state_from_commit(&ctx, &test.blobrepo, target_cs_id).await?;
    assert_eq!(
        state.get_latest_synced_changeset(&first_source_name),
        Some(&first_source_cs_id),
    );
    assert_eq!(
        state.get_latest_synced_changeset(&second_source_name),
        Some(&second_source_cs_id),
    );
    assert_eq!(state.sync_config_version(), &version);

    // Remove file with commit remapping state because it's never present in source
    assert!(wc.remove(&MPath::new(REMAPPING_STATE_FILE)?).is_some());

    assert_eq!(
        wc,
        hashmap! {
            MPath::new("source_1/file")? => "file".to_string(),
            MPath::new("source_2/file")? => "file".to_string(),
        }
    );

    Ok(())
}

#[fbinit::test]
async fn test_add_sync_target_invalid_linkfiles(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let target: Target = test.target("target".to_string());

    let first_source_name = SourceName::new("source_1");
    let second_source_name = SourceName::new("source_2");
    let version = "version_1".to_string();
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .linkfile("first", "linkfiles/linkfile")
        .build_source()?
        .source_builder(second_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .linkfile("second", "linkfiles/linkfile")
        .build_source()?
        .build(&mut test.configs_storage);

    println!("Create initial source commits and bookmarks");
    let first_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("first", "first")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, first_source_name.to_string())
        .set_to(first_source_cs_id)
        .await?;

    let second_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("second", "second")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, second_source_name.to_string())
        .set_to(second_source_cs_id)
        .await?;

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());

    let sync_target_config =
        test.configs_storage
            .get_config_by_version(ctx.clone(), target, version.clone())?;
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    let res = add_sync_target
        .run(
            &ctx,
            sync_target_config,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                second_source_name.clone() => second_source_cs_id,
            },
            None,
        )
        .await;

    assert!(res.is_err());

    Ok(())
}

#[fbinit::test]
async fn test_add_sync_target_invalid_hash_to_merge(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let target: Target = test.target("target".to_string());

    println!("Create initial source commits and bookmarks");
    let first_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("first", "first")
        .commit()
        .await?;

    let second_source_cs_id =
        CreateCommitContext::new(&ctx, &test.blobrepo, vec![first_source_cs_id])
            .add_file("second", "second")
            .commit()
            .await?;

    let first_source_name = SourceName::new("source_1");
    let version = "version_1".to_string();
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version.clone())
        .source_builder(first_source_name.clone())
        .default_prefix("first_source_name")
        // Note that we set first_source_cs_id, but changeset_to_merge is has second_source_cs_id
        // this is invalid and hence add_sync_target should fail
        .source_changeset(first_source_cs_id)
        .build_source()?
        .build(&mut test.configs_storage);

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());

    let sync_target_config =
        test.configs_storage
            .get_config_by_version(ctx.clone(), target, version.clone())?;
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    let res = add_sync_target
        .run(
            &ctx,
            sync_target_config.clone(),
            btreemap! {
                first_source_name.clone() => second_source_cs_id,
            },
            None,
        )
        .await;

    assert!(res.is_err());

    Ok(())
}

#[fbinit::test]
async fn test_add_sync_target_merge_three_sources(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let target: Target = test.target("target".to_string());

    let first_source_name = SourceName::new("source_1");
    let second_source_name = SourceName::new("source_2");
    let third_source_name = SourceName::new("source_3");
    let version = "version_1".to_string();
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .build_source()?
        .source_builder(second_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .build_source()?
        .source_builder(third_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .build_source()?
        .build(&mut test.configs_storage);

    println!("Create initial source commits and bookmarks");
    let first_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("first", "first")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, first_source_name.to_string())
        .set_to(first_source_cs_id)
        .await?;

    let second_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("second", "second")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, second_source_name.to_string())
        .set_to(second_source_cs_id)
        .await?;

    let third_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("third", "third")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, third_source_name.to_string())
        .set_to(third_source_cs_id)
        .await?;

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());

    let sync_target_config =
        test.configs_storage
            .get_config_by_version(ctx.clone(), target, version.clone())?;
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    add_sync_target
        .run(
            &ctx,
            sync_target_config.clone(),
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                second_source_name.clone() => second_source_cs_id,
                third_source_name.clone() => third_source_cs_id,
            },
            None,
        )
        .await?;

    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;
    let mut wc = list_working_copy_utf8(&ctx, &test.blobrepo, target_cs_id).await?;
    // Remove file with commit remapping state because it's never present in source
    assert!(wc.remove(&MPath::new(REMAPPING_STATE_FILE)?).is_some());

    assert_eq!(
        wc,
        hashmap! {
            MPath::new("source_1/first")? => "first".to_string(),
            MPath::new("source_2/second")? => "second".to_string(),
            MPath::new("source_3/third")? => "third".to_string(),
        }
    );

    // Validate the shape of the graph
    // It should look like
    //       o
    //      / \
    //     o   M
    //    / \

    let target_cs = target_cs_id
        .load(&ctx, test.blobrepo.repo_blobstore())
        .await?;
    assert!(target_cs.is_merge());

    let parents = target_cs.parents().collect::<Vec<_>>();
    assert_eq!(parents.len(), 2);

    let first_merge = parents[0]
        .load(&ctx, test.blobrepo.repo_blobstore())
        .await?;
    assert!(first_merge.is_merge());

    let move_commit = parents[1]
        .load(&ctx, test.blobrepo.repo_blobstore())
        .await?;
    assert!(!move_commit.is_merge());

    Ok(())
}

#[fbinit::test]
async fn test_add_sync_target_repeat_same_request(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let target: Target = test.target("target".to_string());

    let first_source_name = SourceName::new("source_1");
    let second_source_name = SourceName::new("source_2");
    let version = "version_1".to_string();
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .build_source()?
        .source_builder(second_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .build_source()?
        .build(&mut test.configs_storage);

    println!("Create initial source commits and bookmarks");
    let first_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("first", "first")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, first_source_name.to_string())
        .set_to(first_source_cs_id)
        .await?;

    let second_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("second", "second")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, second_source_name.to_string())
        .set_to(second_source_cs_id)
        .await?;

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());

    let mut sync_target_config =
        test.configs_storage
            .get_config_by_version(ctx.clone(), target.clone(), version.clone())?;
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    let first_result = add_sync_target
        .run(
            &ctx,
            sync_target_config.clone(),
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                second_source_name.clone() => second_source_cs_id,
            },
            None,
        )
        .await?;

    // Now repeat the same request again (as if client retries a reqeust that has already
    // succeeded). We should get the same result as the first time.
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    let second_result = add_sync_target
        .run(
            &ctx,
            sync_target_config.clone(),
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                second_source_name.clone() => second_source_cs_id,
            },
            None,
        )
        .await?;

    assert_eq!(first_result, second_result);

    // Now modify the request - it should fail
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    assert!(
        add_sync_target
            .run(
                &ctx,
                sync_target_config.clone(),
                btreemap! {
                    first_source_name.clone() => first_source_cs_id,
                },
                None,
            )
            .await
            .is_err()
    );

    // Now send different config with the same name - should fail
    sync_target_config.sources = vec![];
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    let res = add_sync_target
        .run(
            &ctx,
            sync_target_config,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                second_source_name.clone() => second_source_cs_id,
            },
            None,
        )
        .await;
    assert!(res.is_err());
    let err = res.unwrap_err();
    assert!(format!("{}", err).contains("it's different from the one sent in request parameters"));

    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 core::cmp::Ordering;
use std::collections::BTreeMap;
use std::sync::Arc;

use anyhow::anyhow;
use changeset_fetcher::ChangesetFetcherRef;
use context::CoreContext;
use derived_data_utils::derived_data_utils;
use futures::future;
use futures::stream::FuturesUnordered;
use futures::TryStreamExt;
use itertools::EitherOrBoth;
use itertools::Itertools;
use megarepo_config::MononokeMegarepoConfigs;
use megarepo_config::Source;
use megarepo_config::SyncConfigVersion;
use megarepo_config::SyncTargetConfig;
use megarepo_config::Target;
use megarepo_error::MegarepoError;
use megarepo_mapping::CommitRemappingState;
use megarepo_mapping::SourceName;
use mononoke_api::Mononoke;
use mononoke_api::RepoContext;
use mononoke_types::ChangesetId;
use mutable_renames::MutableRenames;
use repo_derived_data::RepoDerivedDataArc;
use repo_derived_data::RepoDerivedDataRef;

use crate::common::find_target_bookmark_and_value;
use crate::common::find_target_sync_config;
use crate::common::MegarepoOp;

/// Structure representing changes needed to be applied onto target to change its
/// config.
/// All the changes will be realized by removing and readding the source. In the
/// future we can make this datastructure richer and include less disruptive
/// methods of introducing small changes (like for example adding a linkfile).
struct SyncTargetConfigChanges {
    added: Vec<(Source, ChangesetId)>,
    removed: Vec<(Source, ChangesetId)>,
}

/// Comparator used for sorting the sources.
fn cmp_by_name(a: &Source, b: &Source) -> Ordering {
    Ord::cmp(&a.source_name, &b.source_name)
}

/// Compares the current state with the desired end state and returns the changes
/// needed to apply to current state.
fn diff_configs(
    old_config: &SyncTargetConfig,
    old_changesets: &BTreeMap<SourceName, ChangesetId>,
    new_config: &SyncTargetConfig,
    new_changesets: &BTreeMap<SourceName, ChangesetId>,
) -> Result<SyncTargetConfigChanges, MegarepoError> {
    let old_sources = old_config
        .sources
        .clone()
        .into_iter()
        .sorted_by(cmp_by_name);
    let new_sources = new_config
        .sources
        .clone()
        .into_iter()
        .sorted_by(cmp_by_name);
    let mut added = Vec::new();
    let mut removed = Vec::new();

    for merged in old_sources.merge_join_by(new_sources, cmp_by_name) {
        match merged {
            EitherOrBoth::Left(old_source) => {
                let cs_id = old_changesets
                    .get(&SourceName::new(&old_source.source_name))
                    .ok_or_else(|| {
                        MegarepoError::request(anyhow!(
                            "remapping state is missing mapping for {}",
                            &old_source.source_name
                        ))
                    })?;
                removed.push((old_source, *cs_id));
            }
            EitherOrBoth::Right(new_source) => {
                let cs_id = new_changesets
                    .get(&SourceName::new(&new_source.source_name))
                    .ok_or_else(|| {
                        MegarepoError::request(anyhow!(
                            "changesets_to_merge is missing mapping for {}",
                            &new_source.source_name
                        ))
                    })?;
                added.push((new_source, *cs_id));
            }
            EitherOrBoth::Both(old_source, new_source) => {
                let old_cs_id = old_changesets
                    .get(&SourceName::new(&old_source.source_name))
                    .ok_or_else(|| {
                        MegarepoError::request(anyhow!(
                            "remapping state is missing mapping for {}",
                            &old_source.source_name
                        ))
                    })?;
                let new_cs_id = new_changesets
                    .get(&SourceName::new(&new_source.source_name))
                    .ok_or_else(|| {
                        MegarepoError::request(anyhow!(
                            "changesets_to_merge is missing mapping for {}",
                            &new_source.source_name
                        ))
                    })?;
                if old_source.source_name != new_source.source_name
                    || old_source.repo_id != new_source.repo_id
                    || old_source.name != new_source.name
                    || old_source.mapping != new_source.mapping
                    || old_cs_id != new_cs_id
                {
                    removed.push((old_source, *old_cs_id));
                    added.push((new_source, *new_cs_id));
                }
            }
        }
    }
    Ok(SyncTargetConfigChanges { added, removed })
}

/// Change target config given a new config. After this command finishes it
/// creates move commits on top of source commits (for the newly added and
/// changed sources), merges them all together then merges them with current head
/// of the target while removing all files belonging to removed sources.
///
/// ```text
///        Tn+1
///       /    \
///      X      Tn
///     / \      \
///    M   M      \
///   /     \      \
///  S       S      \
///
/// Tn - pre-change (old) head of the target branch
/// Tn+1 - post-change (new) head of the target branch (merges in X, and does removals)
/// X - target merge commits
/// M - move commits
/// S - source commits that need to be merged
/// ```
pub struct ChangeTargetConfig<'a> {
    pub megarepo_configs: &'a Arc<dyn MononokeMegarepoConfigs>,
    pub mononoke: &'a Arc<Mononoke>,
    pub mutable_renames: &'a Arc<MutableRenames>,
}

impl<'a> MegarepoOp for ChangeTargetConfig<'a> {
    fn mononoke(&self) -> &Arc<Mononoke> {
        self.mononoke
    }
}

impl<'a> ChangeTargetConfig<'a> {
    pub fn new(
        megarepo_configs: &'a Arc<dyn MononokeMegarepoConfigs>,
        mononoke: &'a Arc<Mononoke>,
        mutable_renames: &'a Arc<MutableRenames>,
    ) -> Self {
        Self {
            megarepo_configs,
            mononoke,
            mutable_renames,
        }
    }

    pub async fn run(
        self,
        ctx: &CoreContext,
        target: &Target,
        new_version: SyncConfigVersion,
        target_location: ChangesetId,
        changesets_to_merge: BTreeMap<SourceName, ChangesetId>,
        message: Option<String>,
    ) -> Result<ChangesetId, MegarepoError> {
        let target_repo = self.find_repo_by_id(ctx, target.repo_id).await?;

        // Find the target config version and remapping state that was used to
        // create the latest target commit. This config version will be used to
        // as a base for comparing with new config.
        let (target_bookmark, actual_target_location) =
            find_target_bookmark_and_value(ctx, &target_repo, target).await?;

        // target doesn't point to the commit we expect - check
        // if this method has already succeded and just immediately return the
        // result if so.
        if actual_target_location != target_location {
            return self
                .check_if_this_method_has_already_succeeded(
                    ctx,
                    &new_version,
                    (target_location, actual_target_location),
                    &changesets_to_merge,
                    &target_repo,
                )
                .await;
        }

        let old_target_cs = &target_repo
            .changeset(target_location)
            .await?
            .ok_or_else(|| {
                MegarepoError::internal(anyhow!("programming error - target changeset not found!"))
            })?;
        let (old_remapping_state, old_config) = find_target_sync_config(
            ctx,
            target_repo.inner_repo(),
            target_location,
            target,
            self.megarepo_configs,
        )
        .await?;

        // Contruct the new config structure and the remapping state
        let new_config = self.megarepo_configs.get_config_by_version(
            ctx.clone(),
            target.clone(),
            new_version.clone(),
        )?;
        let new_remapping_state =
            CommitRemappingState::new(changesets_to_merge.clone(), new_version);

        // Diff the configs to find out action items.
        let diff = diff_configs(
            &old_config,
            &old_remapping_state.latest_synced_changesets,
            &new_config,
            &new_remapping_state.latest_synced_changesets,
        )?;

        // Construct the commit containing all the new content coming due to
        // config change.
        let additions_merge_cs_id = self
            .create_commit_with_new_sources(
                ctx,
                &target_repo,
                &diff,
                &changesets_to_merge,
                new_config.version.clone(),
                message.clone(),
                self.mutable_renames,
            )
            .await?;
        let additions_merge = if let Some(additions_merge_cs_id) = additions_merge_cs_id {
            let mut scuba = ctx.scuba().clone();
            scuba.log_with_msg(
                "Created change target config merge commit for addtions",
                Some(format!("{}", &additions_merge_cs_id)),
            );
            target_repo
                .changeset(additions_merge_cs_id)
                .await
                .map_err(MegarepoError::internal)?
        } else {
            None
        };

        // Construct the commit merging in all the new additions and removing no
        // longer needed content.
        let final_merge = self
            .create_final_merge_commit_with_removals(
                ctx,
                &target_repo,
                &diff.removed,
                message,
                &additions_merge,
                old_target_cs,
                &new_remapping_state,
                Some(new_config.version),
            )
            .await?;
        let mut scuba = ctx.scuba().clone();
        scuba.log_with_msg(
            "Created change target config merge commit connecting addtions to removals",
            Some(format!("{}", &final_merge)),
        );

        // Derrive all the necessary data before moving the bookmark
        let derived_data_types = target_repo
            .inner_repo()
            .repo_derived_data()
            .active_config()
            .types
            .iter();

        let derivers = FuturesUnordered::new();
        for ty in derived_data_types {
            let utils = derived_data_utils(ctx.fb, target_repo.inner_repo(), ty)?;
            derivers.push(utils.derive(
                ctx.clone(),
                target_repo.inner_repo().repo_derived_data_arc(),
                final_merge,
            ));
        }
        derivers.try_for_each(|_| future::ready(Ok(()))).await?;

        // Move bookmark
        self.move_bookmark_conditionally(
            ctx,
            target_repo.inner_repo(),
            target_bookmark.to_string(),
            (target_location, final_merge),
        )
        .await?;

        Ok(final_merge)
    }

    /// For all newly added sources and new versions of those already existing,
    /// applies the right move transformations and merges them all together into
    /// a single commit containing all the new stuff ready to be merged into the
    /// target.
    async fn create_commit_with_new_sources(
        &self,
        ctx: &CoreContext,
        repo: &RepoContext,
        diff: &SyncTargetConfigChanges,
        changesets_to_merge: &BTreeMap<SourceName, ChangesetId>,
        sync_config_version: SyncConfigVersion,
        message: Option<String>,
        mutable_renames: &Arc<MutableRenames>,
    ) -> Result<Option<ChangesetId>, MegarepoError> {
        if diff.added.is_empty() {
            return Ok(None);
        }

        let sources_to_add: Vec<_> = diff
            .added
            .iter()
            .map(|(source, _cs_id)| source.clone())
            .collect();
        let moved_commits = self
            .create_move_commits(
                ctx,
                repo.inner_repo(),
                &sources_to_add,
                changesets_to_merge,
                mutable_renames,
            )
            .await?;

        if moved_commits.len() == 1 {
            return Ok(Some(moved_commits[0].1.moved.get_changeset_id()));
        }

        // Now let's merge all the moved commits together
        Ok(Some(
            self.create_merge_commits(
                ctx,
                repo.inner_repo(),
                moved_commits,
                false, /* write_commit_remapping_state */
                sync_config_version,
                message,
            )
            .await?,
        ))
    }

    // If that change_target_config() call was successful, but failed to send
    // successful result to the client (e.g. network issues) then
    // client will retry a request. We need to detect this situation and
    // send a successful response to the client.
    async fn check_if_this_method_has_already_succeeded(
        &self,
        ctx: &CoreContext,
        new_version: &SyncConfigVersion,
        (expected_target_location, actual_target_location): (ChangesetId, ChangesetId),
        changesets_to_merge: &BTreeMap<SourceName, ChangesetId>,
        repo: &RepoContext,
    ) -> Result<ChangesetId, MegarepoError> {
        // Bookmark points a non-expected commit - let's see if changeset it points to was created
        // by a previous change_target_config call

        // Check that first parent is a target location
        let parents = repo
            .inner_repo()
            .changeset_fetcher()
            .get_parents(ctx, actual_target_location)
            .await?;
        if parents.get(0) != Some(&expected_target_location) {
            return Err(MegarepoError::request(anyhow!(
                "Neither {} nor its first parent {:?} point to a target location {}",
                actual_target_location,
                parents.get(0),
                expected_target_location,
            )));
        }

        self.check_if_commit_has_expected_remapping_state(
            ctx,
            actual_target_location,
            new_version,
            changesets_to_merge,
            repo,
        )
        .await?;

        Ok(actual_target_location)
    }
}

#[cfg(test)]
mod test {
    use anyhow::Error;
    use maplit::btreemap;
    use megarepo_config::Target;
    use mononoke_types::RepositoryId;
    use mononoke_types_mocks::changesetid::ONES_CSID;
    use mononoke_types_mocks::changesetid::THREES_CSID;
    use mononoke_types_mocks::changesetid::TWOS_CSID;

    use super::*;
    use crate::megarepo_test_utils::SyncTargetConfigBuilder;

    fn source_names(sources: &[(Source, ChangesetId)]) -> Vec<String> {
        sources
            .iter()
            .map(|(source, _cs_id)| source.source_name.clone())
            .collect()
    }

    #[test]
    fn test_diff_configs() -> Result<(), Error> {
        let repo_id = RepositoryId::new(1);
        let target = Target {
            repo_id: repo_id.id() as i64,
            bookmark: "target".to_string(),
        };

        let removed_source = SourceName::new("removed_source");
        let added_source = SourceName::new("added_source");
        let unchanged_source = SourceName::new("unchanged_source");
        let changed_source = SourceName::new("changed_source");
        let branch_name_changed_source = SourceName::new("branch_name_changed_source");
        let version_old = "version_old".to_string();
        let version_new = "version_old".to_string();
        let config_old = SyncTargetConfigBuilder::new(repo_id, target.clone(), version_old)
            .source_builder(removed_source.clone())
            .set_prefix_bookmark_to_source_name()
            .build_source()?
            .source_builder(unchanged_source.clone())
            .set_prefix_bookmark_to_source_name()
            .build_source()?
            .source_builder(branch_name_changed_source.clone())
            .set_prefix_bookmark_to_source_name()
            .build_source()?
            .source_builder(changed_source.clone())
            .set_prefix_bookmark_to_source_name()
            .build_source()?
            .no_storage_build();

        let old_changesets = btreemap! {
            removed_source => ONES_CSID,
            changed_source.clone() =>ONES_CSID,
            unchanged_source.clone() =>ONES_CSID,
            branch_name_changed_source.clone() => ONES_CSID,
        };

        let config_new = SyncTargetConfigBuilder::new(repo_id, target, version_new)
            .source_builder(added_source.clone())
            .set_prefix_bookmark_to_source_name()
            .build_source()?
            .source_builder(unchanged_source.clone())
            .set_prefix_bookmark_to_source_name()
            .build_source()?
            .source_builder(changed_source.clone())
            .set_prefix_bookmark_to_source_name()
            .linkfile("first", "linkfiles/first")
            .build_source()?
            .source_builder(branch_name_changed_source.clone())
            .default_prefix(branch_name_changed_source.clone())
            .bookmark("branch_named_changed_source-V2")
            .build_source()?
            .no_storage_build();

        let new_changesets = btreemap! {
            added_source => TWOS_CSID,
            changed_source => THREES_CSID,
            unchanged_source => ONES_CSID,
            branch_name_changed_source => ONES_CSID,
        };

        let diff = diff_configs(&config_old, &old_changesets, &config_new, &new_changesets)?;

        assert_eq!(
            source_names(&diff.added),
            vec!["added_source", "changed_source"]
        );
        assert_eq!(
            source_names(&diff.removed),
            vec!["changed_source", "removed_source"]
        );
        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::Arc;

use anyhow::Error;
use blobstore::Loadable;
use context::CoreContext;
use fbinit::FacebookInit;
use maplit::btreemap;
use maplit::hashmap;
use megarepo_config::MononokeMegarepoConfigs;
use megarepo_config::Target;
use megarepo_mapping::SourceName;
use megarepo_mapping::REMAPPING_STATE_FILE;
use mononoke_types::FileType;
use mononoke_types::MPath;
use repo_blobstore::RepoBlobstoreRef;
use tests_utils::bookmark;
use tests_utils::list_working_copy_utf8_with_types;
use tests_utils::resolve_cs_id;
use tests_utils::CreateCommitContext;

use crate::add_sync_target::AddSyncTarget;
use crate::change_target_config::ChangeTargetConfig;
use crate::megarepo_test_utils::MegarepoTest;
use crate::megarepo_test_utils::SyncTargetConfigBuilder;

#[fbinit::test]
async fn test_change_target_config(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let first_source_name = SourceName::new("source_1");
    let target: Target = test.target("target".to_string());

    init_megarepo(&ctx, &mut test).await?;

    let first_source_cs_id =
        resolve_cs_id(&ctx, &test.blobrepo, first_source_name.0.clone()).await?;
    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;

    let version_2 = "version_2".to_string();
    let third_source_name = SourceName::new("source_3");
    let third_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("third", "third")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, third_source_name.to_string())
        .set_to(third_source_cs_id)
        .await?;
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version_2.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .linkfile("first", "linkfiles/first_in_other_location")
        .build_source()?
        .source_builder(third_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .build_source()?
        .build(&mut test.configs_storage);

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());
    let change_target_config =
        ChangeTargetConfig::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    change_target_config
        .run(
            &ctx,
            &target,
            version_2,
            target_cs_id,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                third_source_name.clone() => third_source_cs_id,
            },
            None,
        )
        .await?;

    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;
    let mut wc = list_working_copy_utf8_with_types(&ctx, &test.blobrepo, target_cs_id).await?;

    // Remove file with commit remapping state because it's never present in source
    assert!(wc.remove(&MPath::new(REMAPPING_STATE_FILE)?).is_some());

    assert_eq!(
        wc,
        hashmap! {
            MPath::new("linkfiles/first_in_other_location")? => ("../source_1/first".to_string(), FileType::Symlink),
            MPath::new("source_1/first")? => ("first".to_string(), FileType::Regular),
            MPath::new("source_3/third")? => ("third".to_string(), FileType::Regular),
        }
    );

    let target_bonsai = target_cs_id
        .load(&ctx, &test.blobrepo.repo_blobstore())
        .await?;
    assert_eq!(
        target_bonsai
            .file_changes()
            .map(|(a, _b)| a.clone())
            .collect::<Vec<_>>(),
        vec![
            MPath::new(".megarepo/remapping_state")?,
            MPath::new("linkfiles/first")?,
            MPath::new("linkfiles/second")?,
            MPath::new("source_2/second")?
        ],
    );

    Ok(())
}

async fn init_megarepo(ctx: &CoreContext, test: &mut MegarepoTest) -> Result<(), Error> {
    let first_source_name = SourceName::new("source_1");
    let second_source_name = SourceName::new("source_2");
    let version = "version_1".to_string();
    let target: Target = test.target("target".to_string());

    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .linkfile("first", "linkfiles/first")
        .build_source()?
        .source_builder(second_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .linkfile("second", "linkfiles/second")
        .build_source()?
        .build(&mut test.configs_storage);

    println!("Create initial source commits and bookmarks");
    let first_source_cs_id = CreateCommitContext::new_root(ctx, &test.blobrepo)
        .add_file("first", "first")
        .commit()
        .await?;

    bookmark(ctx, &test.blobrepo, first_source_name.to_string())
        .set_to(first_source_cs_id)
        .await?;

    let second_source_cs_id = CreateCommitContext::new_root(ctx, &test.blobrepo)
        .add_file("second", "second")
        .commit()
        .await?;

    bookmark(ctx, &test.blobrepo, second_source_name.to_string())
        .set_to(second_source_cs_id)
        .await?;

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());

    let sync_target_config =
        test.configs_storage
            .get_config_by_version(ctx.clone(), target.clone(), version.clone())?;
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    add_sync_target
        .run(
            ctx,
            sync_target_config,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                second_source_name.clone() => second_source_cs_id,
            },
            None,
        )
        .await?;

    Ok(())
}

#[fbinit::test]
async fn test_change_target_config_invalid_config_linkfile(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let first_source_name = SourceName::new("source_1");
    let target: Target = test.target("target".to_string());

    init_megarepo(&ctx, &mut test).await?;

    let third_source_name = SourceName::new("source_3");
    let third_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("third", "third")
        .commit()
        .await?;
    bookmark(&ctx, &test.blobrepo, third_source_name.to_string())
        .set_to(third_source_cs_id)
        .await?;

    let version_2 = "version_2".to_string();
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version_2.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .linkfile("first", "linkfiles/first")
        .build_source()?
        .source_builder(third_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        // NOTE - intentially overwrite existing linkfiles to make sure the request fails
        .linkfile("third", "linkfiles/first")
        .build_source()?
        .build(&mut test.configs_storage);

    let first_source_cs_id =
        resolve_cs_id(&ctx, &test.blobrepo, first_source_name.0.clone()).await?;
    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;
    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());
    let change_target_config =
        ChangeTargetConfig::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    let err = change_target_config
        .run(
            &ctx,
            &target,
            version_2,
            target_cs_id,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                third_source_name.clone() => third_source_cs_id,
            },
            None,
        )
        .await;

    assert!(
        format!("{}", err.unwrap_err())
            .contains("path linkfiles/first cannot be added to the target - it's already present")
    );
    Ok(())
}

#[fbinit::test]
async fn test_change_target_config_invalid_config_normal_file(
    fb: FacebookInit,
) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let first_source_name = SourceName::new("source_1");
    let target: Target = test.target("target".to_string());

    init_megarepo(&ctx, &mut test).await?;

    let third_source_name = SourceName::new("source_3");
    let third_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("first", "someothercontent")
        .commit()
        .await?;
    bookmark(&ctx, &test.blobrepo, third_source_name.to_string())
        .set_to(third_source_cs_id)
        .await?;

    let version_2 = "version_2".to_string();
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version_2.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .linkfile("first", "linkfiles/first")
        .build_source()?
        .source_builder(third_source_name.clone())
        // NOTE - intentially overwrite existing paths to make sure the request fails
        .default_prefix("source_1")
        .bookmark("source_3")
        .build_source()?
        .build(&mut test.configs_storage);

    let first_source_cs_id =
        resolve_cs_id(&ctx, &test.blobrepo, first_source_name.0.clone()).await?;
    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;
    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());
    let change_target_config =
        ChangeTargetConfig::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    let err = change_target_config
        .run(
            &ctx,
            &target,
            version_2,
            target_cs_id,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                third_source_name.clone() => third_source_cs_id,
            },
            None,
        )
        .await;

    assert!(
        format!("{}", err.unwrap_err())
            .contains("path source_1/first cannot be added to the target - it's already present")
    );

    Ok(())
}

#[fbinit::test]
async fn test_change_target_config_invalid_config_file_dir_conflict(
    fb: FacebookInit,
) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let first_source_name = SourceName::new("source_1");
    let target: Target = test.target("target".to_string());

    init_megarepo(&ctx, &mut test).await?;

    let third_source_name = SourceName::new("source_3");
    let third_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("first", "someothercontent")
        .commit()
        .await?;
    bookmark(&ctx, &test.blobrepo, third_source_name.to_string())
        .set_to(third_source_cs_id)
        .await?;

    let version_2 = "version_2".to_string();
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version_2.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .linkfile("first", "linkfiles/first")
        .build_source()?
        .source_builder(third_source_name.clone())
        // NOTE - invalid config - conflict between "source_1/first/first" from source_3 and
        // "source_1/first" from source_1
        .default_prefix("source_1/first")
        .bookmark("source_3")
        .build_source()?
        .build(&mut test.configs_storage);

    let first_source_cs_id =
        resolve_cs_id(&ctx, &test.blobrepo, first_source_name.0.clone()).await?;
    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;
    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());
    let change_target_config =
        ChangeTargetConfig::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    let err = change_target_config
        .run(
            &ctx,
            &target,
            version_2,
            target_cs_id,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                third_source_name.clone() => third_source_cs_id,
            },
            None,
        )
        .await;

    assert!(
        format!("{}", err.unwrap_err())
            .contains("File in target path source_1/first conflicts with newly added files",)
    );

    Ok(())
}

// Replace "source_1/first" with "source_1/first/first"
#[fbinit::test]
async fn test_change_target_config_no_file_dir_conflict(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let first_source_name = SourceName::new("source_1");
    let target: Target = test.target("target".to_string());

    init_megarepo(&ctx, &mut test).await?;

    let third_source_name = SourceName::new("source_3");
    let third_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("first", "someothercontent")
        .commit()
        .await?;
    bookmark(&ctx, &test.blobrepo, third_source_name.to_string())
        .set_to(third_source_cs_id)
        .await?;

    let version_2 = "version_2".to_string();
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version_2.clone())
        .source_builder(third_source_name.clone())
        .default_prefix("source_1/first")
        .bookmark("source_3")
        .build_source()?
        .build(&mut test.configs_storage);

    let first_source_cs_id =
        resolve_cs_id(&ctx, &test.blobrepo, first_source_name.0.clone()).await?;
    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;
    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());
    let change_target_config =
        ChangeTargetConfig::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    change_target_config
        .run(
            &ctx,
            &target,
            version_2,
            target_cs_id,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                third_source_name.clone() => third_source_cs_id,
            },
            None,
        )
        .await?;

    Ok(())
}

// Replace "source_1/dir/first" with "source_1/dir"
#[fbinit::test]
async fn test_change_target_config_no_file_dir_conflict_2(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let first_source_name = SourceName::new("source_1");
    let target: Target = test.target("target".to_string());

    let version = "version_1".to_string();

    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .linkfile("first", "linkfiles/first")
        .build_source()?
        .build(&mut test.configs_storage);

    println!("Create initial source commits and bookmarks");
    let first_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("dir/first", "first")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, first_source_name.to_string())
        .set_to(first_source_cs_id)
        .await?;

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());

    let sync_target_config =
        test.configs_storage
            .get_config_by_version(ctx.clone(), target.clone(), version.clone())?;
    let add_sync_target =
        AddSyncTarget::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    add_sync_target
        .run(
            &ctx,
            sync_target_config,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
            },
            None,
        )
        .await?;

    let third_source_name = SourceName::new("source_3");
    let third_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("dir", "anothercontent")
        .commit()
        .await?;
    bookmark(&ctx, &test.blobrepo, third_source_name.to_string())
        .set_to(third_source_cs_id)
        .await?;

    let version_2 = "version_2".to_string();
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version_2.clone())
        .source_builder(third_source_name.clone())
        .default_prefix("source_1")
        .bookmark("source_3")
        .build_source()?
        .build(&mut test.configs_storage);

    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;
    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());
    let change_target_config =
        ChangeTargetConfig::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    println!("changing target");
    change_target_config
        .run(
            &ctx,
            &target,
            version_2,
            target_cs_id,
            btreemap! {
                third_source_name.clone() => third_source_cs_id,
            },
            None,
        )
        .await?;

    Ok(())
}

#[fbinit::test]
async fn test_change_target_config_repeat_same_request(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let first_source_name = SourceName::new("source_1");
    let target: Target = test.target("target".to_string());

    init_megarepo(&ctx, &mut test).await?;

    let first_source_cs_id =
        resolve_cs_id(&ctx, &test.blobrepo, first_source_name.0.clone()).await?;
    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;

    let version_2 = "version_2".to_string();
    let third_source_name = SourceName::new("source_3");
    let third_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("third", "third")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, third_source_name.to_string())
        .set_to(third_source_cs_id)
        .await?;
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version_2.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .linkfile("first", "linkfiles/first_in_other_location")
        .build_source()?
        .source_builder(third_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .build_source()?
        .build(&mut test.configs_storage);

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());
    let change_target_config =
        ChangeTargetConfig::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    change_target_config
        .run(
            &ctx,
            &target,
            version_2.clone(),
            target_cs_id,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                third_source_name.clone() => third_source_cs_id,
            },
            None,
        )
        .await?;

    // Now repeat the same request - it should succeed
    let change_target_config =
        ChangeTargetConfig::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    change_target_config
        .run(
            &ctx,
            &target,
            version_2.clone(),
            target_cs_id,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                third_source_name.clone() => third_source_cs_id,
            },
            None,
        )
        .await?;

    // Now send slightly different request - it should fail
    let change_target_config =
        ChangeTargetConfig::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    assert!(
        change_target_config
            .run(
                &ctx,
                &target,
                version_2,
                target_cs_id,
                btreemap! {
                    first_source_name.clone() => first_source_cs_id,
                },
                None,
            )
            .await
            .is_err()
    );
    Ok(())
}

#[fbinit::test]
async fn test_change_target_config_noop_change(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let first_source_name = SourceName::new("source_1");
    let target: Target = test.target("target".to_string());

    init_megarepo(&ctx, &mut test).await?;

    let first_source_cs_id =
        resolve_cs_id(&ctx, &test.blobrepo, first_source_name.0.clone()).await?;
    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;

    let version_2 = "version_2".to_string();
    let third_source_name = SourceName::new("source_3");
    let third_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
        .add_file("third", "third")
        .commit()
        .await?;

    bookmark(&ctx, &test.blobrepo, third_source_name.to_string())
        .set_to(third_source_cs_id)
        .await?;
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version_2.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .linkfile("first", "linkfiles/first_in_other_location")
        .build_source()?
        .source_builder(third_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .build_source()?
        .build(&mut test.configs_storage);

    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());
    let change_target_config =
        ChangeTargetConfig::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    let new_target_cs_id = change_target_config
        .run(
            &ctx,
            &target,
            version_2.clone(),
            target_cs_id,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                third_source_name.clone() => third_source_cs_id,
            },
            None,
        )
        .await?;

    // Now do a noop change on existing commit
    let change_target_config =
        ChangeTargetConfig::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    let noop_change = change_target_config
        .run(
            &ctx,
            &target,
            version_2.clone(),
            new_target_cs_id,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
                third_source_name.clone() => third_source_cs_id,
            },
            None,
        )
        .await?;

    assert_ne!(noop_change, new_target_cs_id);

    Ok(())
}

#[fbinit::test]
async fn test_change_target_config_linkfile_to_file_mapped_to_multiple_paths(
    fb: FacebookInit,
) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let mut test = MegarepoTest::new(&ctx).await?;
    let first_source_name = SourceName::new("source_1");
    let target: Target = test.target("target".to_string());

    init_megarepo(&ctx, &mut test).await?;

    let version_2 = "version_2".to_string();
    SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version_2.clone())
        .source_builder(first_source_name.clone())
        .set_prefix_bookmark_to_source_name()
        .copyfile("first", "copy_of_first")
        .linkfile("first", "linkfiles/first")
        .build_source()?
        .build(&mut test.configs_storage);

    let first_source_cs_id =
        resolve_cs_id(&ctx, &test.blobrepo, first_source_name.0.clone()).await?;
    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;
    let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());
    let change_target_config =
        ChangeTargetConfig::new(&configs_storage, &test.mononoke, &test.mutable_renames);
    change_target_config
        .run(
            &ctx,
            &target,
            version_2,
            target_cs_id,
            btreemap! {
                first_source_name.clone() => first_source_cs_id,
            },
            None,
        )
        .await
        .unwrap();

    let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;
    let mut wc = list_working_copy_utf8_with_types(&ctx, &test.blobrepo, target_cs_id).await?;

    // Remove file with commit remapping state because it's never present in source
    assert!(wc.remove(&MPath::new(REMAPPING_STATE_FILE)?).is_some());

    assert_eq!(
        wc,
        hashmap! {
            MPath::new("source_1/first")? => ("first".to_string(), FileType::Regular),
            MPath::new("linkfiles/first")? => ("../source_1/first".to_string(), FileType::Symlink),
            MPath::new("copy_of_first")? => ("first".to_string(), FileType::Regular),
        }
    );

    Ok(())
}