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

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

use anyhow::anyhow;
use anyhow::Error;
use anyhow::Result;
use async_trait::async_trait;
use cached_config::ConfigHandle;
use cached_config::ConfigStore;
use commitsync::types::RawCommitSyncAllVersions;
use commitsync::types::RawCommitSyncConfigAllVersionsOneRepo;
use metaconfig_parser::Convert;
use metaconfig_types::CommitSyncConfig;
use metaconfig_types::CommitSyncConfigVersion;
use metaconfig_types::CommonCommitSyncConfig;
use mononoke_types::RepositoryId;
use pushredirect_enable::types::MononokePushRedirectEnable;
use pushredirect_enable::types::PushRedirectEnableState;
use slog::debug;
use slog::error;
use slog::Logger;
use thiserror::Error;

mod current;

pub use current::CfgrCurrentCommitSyncConfig;
pub use current::RepoGroup;

pub const CONFIGERATOR_PUSHREDIRECT_ENABLE: &str = "scm/mononoke/pushredirect/enable";
pub const CONFIGERATOR_ALL_COMMIT_SYNC_CONFIGS: &str = "scm/mononoke/repos/commitsyncmaps/all";
pub const CONFIGERATOR_CURRENT_COMMIT_SYNC_CONFIG: &str =
    "scm/mononoke/repos/commitsyncmaps/current";

#[derive(Debug, Eq, Error, PartialEq)]
pub enum ErrorKind {
    #[error("{0:?} is not a part of any CommitSyncConfig")]
    NotPartOfAnyCommitSyncConfig(RepositoryId),
    #[error("{0:?} is a part of multiple CommitSyncConfigs")]
    PartOfMultipleCommitSyncConfigs(RepositoryId),
    #[error("Some versions of CommitSyncConfig relate to {0:?}, others don't")]
    OnlySomeVersionsRelateToRepo(RepositoryId),
    #[error("{0:?} is not a part of any CommitSyncConfig version set")]
    NotPartOfAnyCommitSyncConfigVersionSet(RepositoryId),
    #[error("{0:?} is a part of multiple CommitSyncConfigs version sets")]
    PartOfMultipleCommitSyncConfigsVersionSets(RepositoryId),
    #[error("{0:?} has no CommitSyncConfig with version name {1}")]
    UnknownCommitSyncConfigVersion(RepositoryId, String),
    #[error("{0:?} is not a part of any configs")]
    NotPartOfAnyConfigs(RepositoryId),
    #[error("{0:?} is a part of multiple config")]
    PartOfMultipleConfigs(RepositoryId),
    #[error("Multiple commit sync config with the same version {0}")]
    MultipleConfigsForSameVersion(CommitSyncConfigVersion),
}

#[async_trait]
pub trait LiveCommitSyncConfig: Send + Sync {
    /// Return whether push redirection is currently
    /// enabled for draft commits in `repo_id`
    ///
    /// NOTE: two subsequent calls may return different results
    ///       as this queries  config source
    fn push_redirector_enabled_for_draft(&self, repo_id: RepositoryId) -> bool;

    /// Return whether push redirection is currently
    /// enabled for public commits in `repo_id`
    ///
    /// NOTE: two subsequent calls may return different results
    ///       as this queries  config source
    fn push_redirector_enabled_for_public(&self, repo_id: RepositoryId) -> bool;

    /// Return all historical versions of `CommitSyncConfig`
    /// structs for a given repository
    ///
    /// NOTE: two subsequent calls may return different results
    ///       as this queries config source
    async fn get_all_commit_sync_config_versions(
        &self,
        repo_id: RepositoryId,
    ) -> Result<HashMap<CommitSyncConfigVersion, CommitSyncConfig>>;

    /// Return `CommitSyncConfig` for repo `repo_id` of version `version_name`
    async fn get_commit_sync_config_by_version(
        &self,
        repo_id: RepositoryId,
        version_name: &CommitSyncConfigVersion,
    ) -> Result<CommitSyncConfig> {
        let maybe_version = self
            .get_commit_sync_config_by_version_if_exists(repo_id, version_name)
            .await?;

        maybe_version.ok_or_else(|| {
            ErrorKind::UnknownCommitSyncConfigVersion(repo_id, version_name.0.clone()).into()
        })
    }

    /// Return `CommitSyncConfig` for repo `repo_id` of version `version_name`
    async fn get_commit_sync_config_by_version_if_exists(
        &self,
        repo_id: RepositoryId,
        version_name: &CommitSyncConfigVersion,
    ) -> Result<Option<CommitSyncConfig>>;

    /// Returns a config that applies to all config versions
    fn get_common_config(&self, repo_id: RepositoryId) -> Result<CommonCommitSyncConfig> {
        self.get_common_config_if_exists(repo_id)?
            .ok_or_else(|| ErrorKind::NotPartOfAnyConfigs(repo_id).into())
    }

    /// Returns a config that applies to all config versions if it exists
    fn get_common_config_if_exists(
        &self,
        repo_id: RepositoryId,
    ) -> Result<Option<CommonCommitSyncConfig>>;
}

#[derive(Clone)]
pub struct CfgrLiveCommitSyncConfig {
    config_handle_for_all_versions: ConfigHandle<RawCommitSyncAllVersions>,
    config_handle_for_push_redirection: ConfigHandle<MononokePushRedirectEnable>,
}

impl CfgrLiveCommitSyncConfig {
    pub fn new(logger: &Logger, config_store: &ConfigStore) -> Result<Self, Error> {
        debug!(logger, "Initializing CfgrLiveCommitSyncConfig");
        let config_handle_for_push_redirection =
            config_store.get_config_handle(CONFIGERATOR_PUSHREDIRECT_ENABLE.to_string())?;
        debug!(logger, "Initialized PushRedirect configerator config");
        let config_handle_for_all_versions =
            config_store.get_config_handle(CONFIGERATOR_ALL_COMMIT_SYNC_CONFIGS.to_string())?;
        debug!(
            logger,
            "Initialized all commit sync versions configerator config"
        );
        debug!(logger, "Done initializing CfgrLiveCommitSyncConfig");
        Ok(Self {
            config_handle_for_all_versions,
            config_handle_for_push_redirection,
        })
    }

    fn get_push_redirection_repo_state(
        &self,
        repo_id: RepositoryId,
    ) -> Option<PushRedirectEnableState> {
        let config = self.config_handle_for_push_redirection.get();
        config.per_repo.get(&(repo_id.id() as i64)).cloned()
    }

    fn related_to_repo(
        raw_all_versions: &RawCommitSyncConfigAllVersionsOneRepo,
        repo_id: RepositoryId,
    ) -> bool {
        raw_all_versions.common.large_repo_id == repo_id.id()
            || raw_all_versions
                .common
                .small_repos
                .contains_key(&repo_id.id())
    }
}

#[async_trait]
impl LiveCommitSyncConfig for CfgrLiveCommitSyncConfig {
    /// Return whether push redirection is currently
    /// enabled for draft commits in `repo_id`
    ///
    /// NOTE: two subsequent calls may return different results
    ///       as this queries  config source
    fn push_redirector_enabled_for_draft(&self, repo_id: RepositoryId) -> bool {
        match self.get_push_redirection_repo_state(repo_id) {
            Some(config) => config.draft_push,
            None => false,
        }
    }

    /// Return whether push redirection is currently
    /// enabled for public commits in `repo_id`
    ///
    /// NOTE: two subsequent calls may return different results
    ///       as this queries  config source
    fn push_redirector_enabled_for_public(&self, repo_id: RepositoryId) -> bool {
        match self.get_push_redirection_repo_state(repo_id) {
            Some(config) => config.public_push,
            None => false,
        }
    }

    /// Return all historical versions of `CommitSyncConfig`
    /// structs for a given repository
    ///
    /// NOTE: two subsequent calls may return different results
    ///       as this queries config source
    async fn get_all_commit_sync_config_versions(
        &self,
        repo_id: RepositoryId,
    ) -> Result<HashMap<CommitSyncConfigVersion, CommitSyncConfig>> {
        let large_repo_config_version_sets = &self.config_handle_for_all_versions.get().repos;

        let mut interesting_configs: Vec<_> = vec![];

        for (_, config_version_set) in large_repo_config_version_sets.iter() {
            if !Self::related_to_repo(config_version_set, repo_id) {
                continue;
            }

            for raw_commit_sync_config in config_version_set.versions.iter() {
                interesting_configs.push(raw_commit_sync_config.clone());
            }
        }

        let versions: Result<HashMap<CommitSyncConfigVersion, CommitSyncConfig>> =
            interesting_configs
                .into_iter()
                .map(|raw_commit_sync_config| {
                    let commit_sync_config = raw_commit_sync_config.convert()?;
                    let version_name = commit_sync_config.version_name.clone();
                    Ok((version_name, commit_sync_config))
                })
                .collect();

        Ok(versions?)
    }

    /// Return `CommitSyncConfig` for repo `repo_id` of version `version_name`
    async fn get_commit_sync_config_by_version_if_exists(
        &self,
        repo_id: RepositoryId,
        version_name: &CommitSyncConfigVersion,
    ) -> Result<Option<CommitSyncConfig>> {
        let large_repo_config_version_sets = &self.config_handle_for_all_versions.get().repos;

        let mut version = None;
        for (_, config_version_set) in large_repo_config_version_sets.iter() {
            if !Self::related_to_repo(config_version_set, repo_id) {
                continue;
            }
            for config in &config_version_set.versions {
                if config.version_name.as_ref() == Some(&version_name.0) {
                    if version.is_some() {
                        return Err(
                            ErrorKind::MultipleConfigsForSameVersion(version_name.clone()).into(),
                        );
                    }
                    version = Some(config.clone().convert()?);
                }
            }
        }

        Ok(version)
    }

    fn get_common_config_if_exists(
        &self,
        repo_id: RepositoryId,
    ) -> Result<Option<CommonCommitSyncConfig>> {
        let config = self.config_handle_for_all_versions.get();
        let maybe_common_config = {
            let interesting_common_configs = config
                .repos
                .iter()
                .filter(|(_, all_versions_config)| {
                    all_versions_config.common.large_repo_id == repo_id.id()
                        || all_versions_config
                            .common
                            .small_repos
                            .contains_key(&repo_id.id())
                })
                .map(|(_, all_versions_config)| all_versions_config.common.clone());

            let mut iter = interesting_common_configs;
            match (iter.next(), iter.next()) {
                (None, _) => Ok(None),
                (Some(config), None) => Ok(Some(config)),
                (Some(_), Some(_)) => Err(ErrorKind::PartOfMultipleConfigs(repo_id)),
            }?
        };
        maybe_common_config.map(Convert::convert).transpose()
    }
}

/// Inner container for `TestLiveCommitSyncConfigSource`
/// See `TestLiveCommitSyncConfigSource` for more details
struct TestLiveCommitSyncConfigSourceInner {
    version_to_config: Mutex<HashMap<CommitSyncConfigVersion, CommitSyncConfig>>,
    push_redirection_for_draft: Mutex<HashMap<RepositoryId, bool>>,
    push_redirection_for_public: Mutex<HashMap<RepositoryId, bool>>,
    common_configs: Mutex<Vec<CommonCommitSyncConfig>>,
}

/// A helper type to manage `TestLiveCommitSyncConfig` from outside
/// The idea behind `TestLiveCommitSyncConfig` is that it is going
/// to be used in type-erased contexts, behind `dyn LiveCommitSyncConfig`.
/// Therefore there will be no way to access anything beyond the
/// `LiveCommitSyncConfig` interface, so no way to edit existing config.
/// To allow test scenarios to edit underlying configs, creators of
/// `TestLiveCommitSyncConfig` also receive an accompanying
/// `TestLiveCommitSyncConfigSource`, which allows editing underlying
/// configs
#[derive(Clone)]
pub struct TestLiveCommitSyncConfigSource(Arc<TestLiveCommitSyncConfigSourceInner>);

impl TestLiveCommitSyncConfigSource {
    fn new() -> Self {
        Self(Arc::new(TestLiveCommitSyncConfigSourceInner {
            version_to_config: Mutex::new(HashMap::new()),
            push_redirection_for_draft: Mutex::new(HashMap::new()),
            push_redirection_for_public: Mutex::new(HashMap::new()),
            common_configs: Mutex::new(vec![]),
        }))
    }

    pub fn add_config(&self, config: CommitSyncConfig) {
        self.0
            .version_to_config
            .lock()
            .expect("poisoned lock")
            .insert(config.version_name.clone(), config);
    }

    pub fn set_draft_push_redirection_enabled(&self, repo_id: RepositoryId) {
        self.0
            .push_redirection_for_draft
            .lock()
            .expect("poisoned lock")
            .insert(repo_id, true);
    }

    pub fn set_public_push_redirection_enabled(&self, repo_id: RepositoryId) {
        self.0
            .push_redirection_for_public
            .lock()
            .expect("poisoned lock")
            .insert(repo_id, true);
    }

    pub fn add_common_config(&self, config: CommonCommitSyncConfig) {
        self.0
            .common_configs
            .lock()
            .expect("poisoned lock")
            .push(config);
    }

    fn push_redirector_enabled_for_draft(&self, repo_id: RepositoryId) -> bool {
        *self
            .0
            .push_redirection_for_draft
            .lock()
            .expect("poisoned lock")
            .get(&repo_id)
            .unwrap_or(&false)
    }

    fn push_redirector_enabled_for_public(&self, repo_id: RepositoryId) -> bool {
        *self
            .0
            .push_redirection_for_public
            .lock()
            .expect("poisoned lock")
            .get(&repo_id)
            .unwrap_or(&false)
    }

    fn get_all_commit_sync_config_versions(
        &self,
        repo_id: RepositoryId,
    ) -> Result<HashMap<CommitSyncConfigVersion, CommitSyncConfig>> {
        let version_to_config = { self.0.version_to_config.lock().unwrap().clone() };

        Ok(version_to_config
            .into_iter()
            .filter(|(_, config)| Self::related_to_repo(config, repo_id))
            .collect())
    }

    pub fn get_commit_sync_config_by_version_if_exists(
        &self,
        repo_id: RepositoryId,
        version_name: &CommitSyncConfigVersion,
    ) -> Result<Option<CommitSyncConfig>> {
        let maybe_config = self
            .0
            .version_to_config
            .lock()
            .unwrap()
            .get(version_name)
            .cloned();

        let config = match maybe_config {
            Some(config) => config,
            None => {
                return Ok(None);
            }
        };

        if Self::related_to_repo(&config, repo_id) {
            Ok(Some(config))
        } else {
            Err(anyhow!("{} not found", version_name))
        }
    }

    pub fn get_common_config_if_exists(
        &self,
        repo_id: RepositoryId,
    ) -> Result<Option<CommonCommitSyncConfig>> {
        let common_configs = self.0.common_configs.lock().unwrap();
        for config in common_configs.iter() {
            if config.large_repo_id == repo_id || config.small_repos.contains_key(&repo_id) {
                return Ok(Some(config.clone()));
            }
        }

        Ok(None)
    }

    fn related_to_repo(commit_sync_config: &CommitSyncConfig, repo_id: RepositoryId) -> bool {
        commit_sync_config.large_repo_id == repo_id
            || commit_sync_config
                .small_repos
                .iter()
                .any(|small_repo| small_repo.0 == &repo_id)
    }
}

/// A unit-test freindly implementor of `LiveCommitSyncConfig`
/// As this struct is meant to be held behind a type-erasing
/// `dyn LiveCommitSyncConfig`, anything beyond the interface
/// of `LiveCommitSyncConfig` won't be visible to the users.
/// Therefore, to modify internal state a `TestLiveCommitSyncConfigSource`
/// should be used.
#[derive(Clone)]
pub struct TestLiveCommitSyncConfig {
    source: TestLiveCommitSyncConfigSource,
}

impl TestLiveCommitSyncConfig {
    pub fn new_with_source() -> (Self, TestLiveCommitSyncConfigSource) {
        let source = TestLiveCommitSyncConfigSource::new();
        (
            Self {
                source: source.clone(),
            },
            source,
        )
    }

    pub fn new_empty() -> Self {
        let source = TestLiveCommitSyncConfigSource::new();
        Self { source }
    }
}

#[async_trait]
impl LiveCommitSyncConfig for TestLiveCommitSyncConfig {
    fn push_redirector_enabled_for_draft(&self, repo_id: RepositoryId) -> bool {
        self.source.push_redirector_enabled_for_draft(repo_id)
    }

    fn push_redirector_enabled_for_public(&self, repo_id: RepositoryId) -> bool {
        self.source.push_redirector_enabled_for_public(repo_id)
    }

    async fn get_all_commit_sync_config_versions(
        &self,
        repo_id: RepositoryId,
    ) -> Result<HashMap<CommitSyncConfigVersion, CommitSyncConfig>> {
        self.source.get_all_commit_sync_config_versions(repo_id)
    }

    async fn get_commit_sync_config_by_version_if_exists(
        &self,
        repo_id: RepositoryId,
        version_name: &CommitSyncConfigVersion,
    ) -> Result<Option<CommitSyncConfig>> {
        self.source
            .get_commit_sync_config_by_version_if_exists(repo_id, version_name)
    }

    fn get_common_config_if_exists(
        &self,
        repo_id: RepositoryId,
    ) -> Result<Option<CommonCommitSyncConfig>> {
        self.source.get_common_config_if_exists(repo_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 cached_config::ModificationTime;
use fbinit::FacebookInit;
use live_commit_sync_config::ErrorKind;
use live_commit_sync_config::LiveCommitSyncConfig;
use live_commit_sync_config::CONFIGERATOR_ALL_COMMIT_SYNC_CONFIGS;
use metaconfig_types::CommitSyncConfigVersion;
use mononoke_types::RepositoryId;
use pretty_assertions::assert_eq;

use crate::ensure_all_updated;
use crate::get_ctx_source_store_and_live_config;
use crate::EMPTY_PUSHREDIRECTOR;

// Since these huge blobs of json may be hard to read, I will explain
// the difference in the comment. The comparison of `ALL_COMMIT_SYNC_CONFIG_V1`
// and `ALL_COMMIT_SYNC_CONFIG_V2` and `ALL_COMMIT_SYNC_CONFIG_V3` is as follows:
// - for `large_repo` (repo_id=0), V2 adds a new version of `CommitSyncConfig`,
//   called `TEST_VERSION_NAME_LIVE_2`, and makes it a current version, V3 does not
//   change the list of versions, but makes `TEST_VERSION_NAME_LIVE_1` a current
//   version once again
// - for `large_repo_2` nothing changes between versions

const ALL_COMMIT_SYNC_CONFIG_V1: &str = r#"{
    "repos": {
        "large_repo": {
            "versions": [
                {
                    "large_repo_id": 0,
                    "common_pushrebase_bookmarks": ["b1"],
                    "small_repos": [
                        {
                            "repoid": 1,
                            "default_action": "prepend_prefix",
                            "default_prefix": "f1",
                            "bookmark_prefix": "bp1/",
                            "mapping": {"d": "dd"},
                            "direction": "large_to_small"
                        },
                        {
                            "repoid": 2,
                            "default_action": "prepend_prefix",
                            "default_prefix": "f2",
                            "bookmark_prefix": "bp2/",
                            "mapping": {"d": "ddd"},
                            "direction": "small_to_large"
                        }
                    ],
                    "version_name": "TEST_VERSION_NAME_LIVE_1"
                }
            ],
            "common": {
                "large_repo_id": 0,
                "common_pushrebase_bookmarks": ["b1"],
                "small_repos": {
                    1: {
                        "bookmark_prefix": "bp1/"
                    },
                    2: {
                        "bookmark_prefix": "bp2/"
                    }
                }
            }
        },
        "large_repo_2": {
            "versions": [
                {
                    "large_repo_id": 3,
                    "common_pushrebase_bookmarks": ["b3"],
                    "small_repos": [
                        {
                            "repoid": 4,
                            "default_action": "preserve",
                            "default_prefix": "f3",
                            "bookmark_prefix": "bp3/",
                            "mapping": {"d": "dddd"},
                            "direction": "small_to_large"
                        }
                    ],
                    "version_name": "TEST_VERSION_NAME_LIVE_R3_1"
                }
            ],
            "common": {
                "large_repo_id": 3,
                "common_pushrebase_bookmarks": ["b3"],
                "small_repos": {
                    4: {
                        "bookmark_prefix": "bp3/"
                    }
                }
            }
        }
    }
}"#;

const ALL_COMMIT_SYNC_CONFIG_V2: &str = r#"{
    "repos": {
        "large_repo": {
            "versions": [
                {
                    "large_repo_id": 0,
                    "common_pushrebase_bookmarks": ["b1"],
                    "small_repos": [
                        {
                            "repoid": 1,
                            "default_action": "prepend_prefix",
                            "default_prefix": "f1",
                            "bookmark_prefix": "bp1/",
                            "mapping": {"d": "dd"},
                            "direction": "large_to_small"
                        },
                        {
                            "repoid": 2,
                            "default_action": "prepend_prefix",
                            "default_prefix": "f2",
                            "bookmark_prefix": "bp2/",
                            "mapping": {"d": "ddd"},
                            "direction": "small_to_large"
                        }
                    ],
                    "version_name": "TEST_VERSION_NAME_LIVE_1"
                },
                {
                    "large_repo_id": 0,
                    "common_pushrebase_bookmarks": ["b1"],
                    "small_repos": [
                        {
                            "repoid": 1,
                            "default_action": "prepend_prefix",
                            "default_prefix": "f1",
                            "bookmark_prefix": "bp1/",
                            "mapping": {"d": "dd"},
                            "direction": "large_to_small"
                        },
                        {
                            "repoid": 2,
                            "default_action": "prepend_prefix",
                            "default_prefix": "f2",
                            "bookmark_prefix": "bp2/",
                            "mapping": {"d": "ddd"},
                            "direction": "small_to_large"
                        }
                    ],
                    "version_name": "TEST_VERSION_NAME_LIVE_2"
                }
            ],
            "common": {
                "large_repo_id": 0,
                "common_pushrebase_bookmarks": ["b1"],
                "small_repos": {
                    1: {
                        "bookmark_prefix": "bp1/"
                    },
                    2: {
                        "bookmark_prefix": "bp2/"
                    }
                }
            }
        },
        "large_repo_2": {
            "versions": [
                {
                    "large_repo_id": 3,
                    "common_pushrebase_bookmarks": ["b3"],
                    "small_repos": [
                        {
                            "repoid": 4,
                            "default_action": "preserve",
                            "default_prefix": "f3",
                            "bookmark_prefix": "bp3/",
                            "mapping": {"d": "dddd"},
                            "direction": "small_to_large"
                        }
                    ],
                    "version_name": "TEST_VERSION_NAME_LIVE_R3_1"
                }
            ],
            "common": {
                "large_repo_id": 3,
                "common_pushrebase_bookmarks": ["b3"],
                "small_repos": {
                    4: {
                        "bookmark_prefix": "bp3/"
                    }
                }
            }
        }
    }
}"#;

const ALL_COMMIT_SYNC_CONFIG_V3: &str = r#"{
    "repos": {
        "large_repo": {
            "versions": [
                {
                    "large_repo_id": 0,
                    "common_pushrebase_bookmarks": ["b1"],
                    "small_repos": [
                        {
                            "repoid": 1,
                            "default_action": "prepend_prefix",
                            "default_prefix": "f1",
                            "bookmark_prefix": "bp1/",
                            "mapping": {"d": "dd"},
                            "direction": "large_to_small"
                        },
                        {
                            "repoid": 2,
                            "default_action": "prepend_prefix",
                            "default_prefix": "f2",
                            "bookmark_prefix": "bp2/",
                            "mapping": {"d": "ddd"},
                            "direction": "small_to_large"
                        }
                    ],
                    "version_name": "TEST_VERSION_NAME_LIVE_1"
                },
                {
                    "large_repo_id": 0,
                    "common_pushrebase_bookmarks": ["b1"],
                    "small_repos": [
                        {
                            "repoid": 1,
                            "default_action": "prepend_prefix",
                            "default_prefix": "f1",
                            "bookmark_prefix": "bp1/",
                            "mapping": {"d": "dd"},
                            "direction": "large_to_small"
                        },
                        {
                            "repoid": 2,
                            "default_action": "prepend_prefix",
                            "default_prefix": "f2",
                            "bookmark_prefix": "bp2/",
                            "mapping": {"d": "ddd"},
                            "direction": "small_to_large"
                        }
                    ],
                    "version_name": "TEST_VERSION_NAME_LIVE_2"
                }
            ],
            "common": {
                "large_repo_id": 0,
                "common_pushrebase_bookmarks": ["b1"],
                "small_repos": {
                    1: {
                        "bookmark_prefix": "bp1/"
                    },
                    2: {
                        "bookmark_prefix": "bp2/"
                    }
                }
            }
        },
        "large_repo_2": {
            "versions": [
                {
                    "large_repo_id": 3,
                    "common_pushrebase_bookmarks": ["b3"],
                    "small_repos": [
                        {
                            "repoid": 4,
                            "default_action": "preserve",
                            "default_prefix": "f3",
                            "bookmark_prefix": "bp3/",
                            "mapping": {"d": "dddd"},
                            "direction": "small_to_large"
                        }
                    ],
                    "version_name": "TEST_VERSION_NAME_LIVE_R3_1"
                }
            ],
            "common": {
                "large_repo_id": 3,
                "common_pushrebase_bookmarks": ["b3"],
                "small_repos": {
                    4: {
                        "bookmark_prefix": "bp3/"
                    }
                }
            }
        }
    }
}"#;

#[fbinit::test]
async fn test_adding_a_new_version(fb: FacebookInit) {
    let (_ctx, test_source, _store, live_commit_sync_config) =
        get_ctx_source_store_and_live_config(fb, EMPTY_PUSHREDIRECTOR, ALL_COMMIT_SYNC_CONFIG_V1);
    let repo_1 = RepositoryId::new(1);
    let repo_3 = RepositoryId::new(3);

    let v1 = CommitSyncConfigVersion("TEST_VERSION_NAME_LIVE_1".to_string());
    let v2 = CommitSyncConfigVersion("TEST_VERSION_NAME_LIVE_2".to_string());
    let vr31 = CommitSyncConfigVersion("TEST_VERSION_NAME_LIVE_R3_1".to_string());

    // Before we apply any changes, configs are as expected
    let av1 = live_commit_sync_config
        .get_all_commit_sync_config_versions(repo_1)
        .await
        .unwrap();
    let av3 = live_commit_sync_config
        .get_all_commit_sync_config_versions(repo_3)
        .await
        .unwrap();

    assert_eq!(av1.len(), 1);
    assert!(av1.contains_key(&v1));
    assert_eq!(av3.len(), 1);
    assert!(av3.contains_key(&vr31));

    // Let's make a change to our config source: add a new version
    // and make it the current one
    test_source.insert_config(
        CONFIGERATOR_ALL_COMMIT_SYNC_CONFIGS,
        ALL_COMMIT_SYNC_CONFIG_V2,
        ModificationTime::UnixTimestamp(1),
    );
    ensure_all_updated();

    // Ensure an added version is recognized after a change
    let av1 = live_commit_sync_config
        .get_all_commit_sync_config_versions(repo_1)
        .await
        .unwrap();
    let av3 = live_commit_sync_config
        .get_all_commit_sync_config_versions(repo_3)
        .await
        .unwrap();

    assert_eq!(av1.len(), 2);
    assert!(av1.contains_key(&v1));
    assert!(av1.contains_key(&v2));
    assert_eq!(av3.len(), 1);
    assert!(av3.contains_key(&vr31));

    // Let's make a change to our config source: revert to the previous current version
    test_source.insert_config(
        CONFIGERATOR_ALL_COMMIT_SYNC_CONFIGS,
        ALL_COMMIT_SYNC_CONFIG_V3,
        ModificationTime::UnixTimestamp(2),
    );
    ensure_all_updated();

    // Ensure new current version is picked up, but len of versions is the same for r1
    let av1 = live_commit_sync_config
        .get_all_commit_sync_config_versions(repo_1)
        .await
        .unwrap();
    let av3 = live_commit_sync_config
        .get_all_commit_sync_config_versions(repo_3)
        .await
        .unwrap();

    assert_eq!(av1.len(), 2);
    assert!(av1.contains_key(&v1));
    assert!(av1.contains_key(&v2));
    assert_eq!(av3.len(), 1);
    assert!(av3.contains_key(&vr31));
}

#[fbinit::test]
async fn test_query_by_version_name(fb: FacebookInit) {
    let (_ctx, test_source, _store, live_commit_sync_config) =
        get_ctx_source_store_and_live_config(fb, EMPTY_PUSHREDIRECTOR, ALL_COMMIT_SYNC_CONFIG_V1);
    let repo_1 = RepositoryId::new(1);
    let repo_3 = RepositoryId::new(3);

    let v1 = CommitSyncConfigVersion("TEST_VERSION_NAME_LIVE_1".to_string());
    let v2 = CommitSyncConfigVersion("TEST_VERSION_NAME_LIVE_2".to_string());

    // Before we apply any changes, configs are as expected
    let r1_v1 = live_commit_sync_config
        .get_commit_sync_config_by_version(repo_1, &v1)
        .await
        .unwrap();
    let r3_v1_res = live_commit_sync_config
        .get_commit_sync_config_by_version(repo_3, &v1)
        .await;

    assert_eq!(r1_v1.version_name, v1);
    // This version is not for r3, so we did not get a repo
    assert!(is_error_kind!(
        r3_v1_res,
        ErrorKind::UnknownCommitSyncConfigVersion(_, _)
    ));

    // Let's make a change to our config source: add a new version
    // and make it the current one
    test_source.insert_config(
        CONFIGERATOR_ALL_COMMIT_SYNC_CONFIGS,
        ALL_COMMIT_SYNC_CONFIG_V2,
        ModificationTime::UnixTimestamp(1),
    );
    ensure_all_updated();

    // Ensure an added version is recognized after a change
    let r1_v1 = live_commit_sync_config
        .get_commit_sync_config_by_version(repo_1, &v1)
        .await
        .unwrap();
    let r1_v2 = live_commit_sync_config
        .get_commit_sync_config_by_version(repo_1, &v2)
        .await
        .unwrap();

    assert_eq!(r1_v1.version_name, v1);
    assert_eq!(r1_v2.version_name, v2);

    // Let's make a change to our config source: revert to the previous current version
    test_source.insert_config(
        CONFIGERATOR_ALL_COMMIT_SYNC_CONFIGS,
        ALL_COMMIT_SYNC_CONFIG_V3,
        ModificationTime::UnixTimestamp(2),
    );
    ensure_all_updated();

    // Ensure new current version is picked up,
    // but v2 is still accessible
    let r1_v1 = live_commit_sync_config
        .get_commit_sync_config_by_version(repo_1, &v1)
        .await
        .unwrap();
    let r1_v2 = live_commit_sync_config
        .get_commit_sync_config_by_version(repo_1, &v2)
        .await
        .unwrap();

    assert_eq!(r1_v1.version_name, v1);
    assert_eq!(r1_v2.version_name, v2);
}


/*
 * 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 fbinit::FacebookInit;
use live_commit_sync_config::LiveCommitSyncConfig;
use mononoke_types::RepositoryId;
use pretty_assertions::assert_eq;

use crate::get_ctx_source_store_and_live_config;
use crate::EMPTY_PUSHREDIRECTOR;

const ALL_COMMIT_SYNC_CONFIG_V1: &str = r#"{
    "repos": {
        "large_repo": {
            "versions": [
                {
                    "large_repo_id": 0,
                    "common_pushrebase_bookmarks": ["b1"],
                    "small_repos": [
                        {
                            "repoid": 1,
                            "default_action": "prepend_prefix",
                            "default_prefix": "f1",
                            "bookmark_prefix": "bp1/",
                            "mapping": {"d": "dd"},
                            "direction": "large_to_small"
                        },
                        {
                            "repoid": 2,
                            "default_action": "prepend_prefix",
                            "default_prefix": "f2",
                            "bookmark_prefix": "bp2/",
                            "mapping": {"d": "ddd"},
                            "direction": "small_to_large"
                        }
                    ],
                    "version_name": "TEST_VERSION_NAME_LIVE_1"
                },
                {
                    "large_repo_id": 0,
                    "common_pushrebase_bookmarks": ["b1"],
                    "small_repos": [
                        {
                            "repoid": 1,
                            "default_action": "prepend_prefix",
                            "default_prefix": "f1",
                            "bookmark_prefix": "bp1/",
                            "mapping": {"d": "dd"},
                            "direction": "large_to_small"
                        },
                        {
                            "repoid": 2,
                            "default_action": "prepend_prefix",
                            "default_prefix": "f2",
                            "bookmark_prefix": "bp2/",
                            "mapping": {"d": "ddd"},
                            "direction": "small_to_large"
                        }
                    ],
                    "version_name": "TEST_VERSION_NAME_LIVE_2"
                }
            ],
            "current_version": "TEST_VERSION_NAME_LIVE_2",
            "common": {
                "large_repo_id": 0,
                "common_pushrebase_bookmarks": ["b1"],
                "small_repos": {
                    1: {
                        "bookmark_prefix": "bp1/"
                    },
                    2: {
                        "bookmark_prefix": "bp2/"
                    }
                }
            }
        },
        "large_repo_2": {
            "versions": [
                {
                    "large_repo_id": 3,
                    "common_pushrebase_bookmarks": ["b3"],
                    "small_repos": [
                        {
                            "repoid": 4,
                            "default_action": "preserve",
                            "default_prefix": "f3",
                            "bookmark_prefix": "bp3/",
                            "mapping": {"d": "dddd"},
                            "direction": "small_to_large"
                        }
                    ],
                    "version_name": "TEST_VERSION_NAME_R3_1"
                }
            ],
            "current_version": "TEST_VERSION_NAME_R3_1,",
            "common": {
                "large_repo_id": 3,
                "common_pushrebase_bookmarks": ["b3"],
                "small_repos": {
                    4: {
                        "bookmark_prefix": "bp3/"
                    }
                }
            }
        }
    }
}"#;

#[fbinit::test]
async fn test_different_repos_same_group(fb: FacebookInit) {
    let (_ctx, _test_source, _store, live_commit_sync_config) =
        get_ctx_source_store_and_live_config(fb, EMPTY_PUSHREDIRECTOR, ALL_COMMIT_SYNC_CONFIG_V1);

    let repo_0 = RepositoryId::new(0);
    let repo_1 = RepositoryId::new(1);
    let repo_2 = RepositoryId::new(2);
    let repo_3 = RepositoryId::new(3);
    let repo_4 = RepositoryId::new(4);

    let av0 = live_commit_sync_config
        .get_all_commit_sync_config_versions(repo_0)
        .await
        .unwrap();
    let av1 = live_commit_sync_config
        .get_all_commit_sync_config_versions(repo_1)
        .await
        .unwrap();
    let av2 = live_commit_sync_config
        .get_all_commit_sync_config_versions(repo_2)
        .await
        .unwrap();
    let av3 = live_commit_sync_config
        .get_all_commit_sync_config_versions(repo_3)
        .await
        .unwrap();
    let av4 = live_commit_sync_config
        .get_all_commit_sync_config_versions(repo_4)
        .await
        .unwrap();

    assert_eq!(av0, av1);
    assert_eq!(av0, av2);
    assert_eq!(av3, av4);
    assert_ne!(av0, av3);
}

#[fbinit::test]
async fn test_version_counts(fb: FacebookInit) {
    let (_ctx, _test_source, _store, live_commit_sync_config) =
        get_ctx_source_store_and_live_config(fb, EMPTY_PUSHREDIRECTOR, ALL_COMMIT_SYNC_CONFIG_V1);

    let repo_0 = RepositoryId::new(0);
    let repo_4 = RepositoryId::new(4);

    let av0 = live_commit_sync_config
        .get_all_commit_sync_config_versions(repo_0)
        .await
        .unwrap();
    let av4 = live_commit_sync_config
        .get_all_commit_sync_config_versions(repo_4)
        .await
        .unwrap();

    assert_eq!(av0.len(), 2);
    assert_eq!(av4.len(), 1);
}


/*
 * 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 fbinit::FacebookInit;
use live_commit_sync_config::LiveCommitSyncConfig;
use mononoke_types::RepositoryId;

use crate::get_ctx_source_store_and_live_config;
use crate::EMPTY_PUSHREDIRECTOR;
use crate::EMTPY_COMMIT_SYNC_ALL;

#[fbinit::test]
async fn test_empty_configs(fb: FacebookInit) {
    let (_ctx, _test_source, _store, live_commit_sync_config) =
        get_ctx_source_store_and_live_config(fb, EMPTY_PUSHREDIRECTOR, EMTPY_COMMIT_SYNC_ALL);
    let repo_1 = RepositoryId::new(1);
    assert!(!live_commit_sync_config.push_redirector_enabled_for_draft(repo_1));
    assert!(!live_commit_sync_config.push_redirector_enabled_for_public(repo_1));
}


/*
 * 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 std::thread;
use std::time::Duration;

use cached_config::ConfigStore;
use cached_config::ModificationTime;
use cached_config::TestSource;
use context::CoreContext;
use fbinit::FacebookInit;
use live_commit_sync_config::CfgrLiveCommitSyncConfig;
use live_commit_sync_config::CONFIGERATOR_ALL_COMMIT_SYNC_CONFIGS;
use live_commit_sync_config::CONFIGERATOR_PUSHREDIRECT_ENABLE;

macro_rules! is_error_kind {
    ($result_expression:expr, $( $pattern:pat_param )|+ $( if $guard: expr )?) => {
        match $result_expression {
            Ok(_) => false,
            Err(e) => match e.downcast_ref::<ErrorKind>() {
                $( Some($pattern) )|+ $( if $guard )? => true,
                _ => false
            }
        }
    }
}

mod all_changes;
mod all_simple;
mod current_simple;
mod push_redirection;

const EMPTY_PUSHREDIRECTOR: &str = r#"{
     "per_repo": {}
 }"#;
const EMTPY_COMMIT_SYNC_ALL: &str = r#"{
     "repos": {}
 }"#;

fn ensure_all_updated() {
    // This is copy-pasted from `cached_config`'s own
    // unit test
    thread::yield_now();
    thread::sleep(Duration::from_secs(1));
}

fn get_ctx_source_store_and_live_config(
    fb: FacebookInit,
    pushredirector_config: &str,
    all_commit_syncs: &str,
) -> (
    CoreContext,
    Arc<TestSource>,
    ConfigStore,
    CfgrLiveCommitSyncConfig,
) {
    let ctx = CoreContext::test_mock(fb);
    let test_source = Arc::new(TestSource::new());
    test_source.insert_config(
        CONFIGERATOR_PUSHREDIRECT_ENABLE,
        pushredirector_config,
        ModificationTime::UnixTimestamp(0),
    );
    test_source.insert_config(
        CONFIGERATOR_ALL_COMMIT_SYNC_CONFIGS,
        all_commit_syncs,
        ModificationTime::UnixTimestamp(0),
    );

    // We want to always refresh these paths in the test setting
    test_source.insert_to_refresh(CONFIGERATOR_PUSHREDIRECT_ENABLE.to_string());
    test_source.insert_to_refresh(CONFIGERATOR_ALL_COMMIT_SYNC_CONFIGS.to_string());

    let store = ConfigStore::new(test_source.clone(), Duration::from_millis(2), None);
    let live_commit_sync_config = CfgrLiveCommitSyncConfig::new(ctx.logger(), &store).unwrap();
    (ctx, test_source, store, live_commit_sync_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 cached_config::ModificationTime;
use fbinit::FacebookInit;
use live_commit_sync_config::*;
use mononoke_types::RepositoryId;

use crate::ensure_all_updated;
use crate::get_ctx_source_store_and_live_config;
use crate::EMPTY_PUSHREDIRECTOR;
use crate::EMTPY_COMMIT_SYNC_ALL;

const PUSHREDIRECTOR_PUBLIC_ENABLED: &str = r#"{
    "per_repo": {
        "1": {
            "draft_push": false,
            "public_push": true
        }
    }
}"#;

const PUSHREDIRECTOR_BOTH_ENABLED: &str = r#"{
    "per_repo": {
        "1": {
            "draft_push": true,
            "public_push": true
        }
    }
}"#;

#[fbinit::test]
async fn test_enabling_push_redirection(fb: FacebookInit) {
    let (_ctx, test_source, _store, live_commit_sync_config) =
        get_ctx_source_store_and_live_config(fb, EMPTY_PUSHREDIRECTOR, EMTPY_COMMIT_SYNC_ALL);
    let repo_1 = RepositoryId::new(1);

    // Enable push-redirection of public commits
    test_source.insert_config(
        CONFIGERATOR_PUSHREDIRECT_ENABLE,
        PUSHREDIRECTOR_PUBLIC_ENABLED,
        ModificationTime::UnixTimestamp(1),
    );

    // Check that push-redirection of public commits has been picked up
    ensure_all_updated();
    assert!(!live_commit_sync_config.push_redirector_enabled_for_draft(repo_1));
    assert!(live_commit_sync_config.push_redirector_enabled_for_public(repo_1));

    // Enable push-redirection of public and draft commits
    test_source.insert_config(
        CONFIGERATOR_PUSHREDIRECT_ENABLE,
        PUSHREDIRECTOR_BOTH_ENABLED,
        ModificationTime::UnixTimestamp(2),
    );

    // Check that push-redirection of public and draft commits has been picked up
    ensure_all_updated();
    assert!(live_commit_sync_config.push_redirector_enabled_for_draft(repo_1));
    assert!(live_commit_sync_config.push_redirector_enabled_for_public(repo_1));
}


/*
 * 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::Error;
use mercurial_types::MPath;

pub type Chunker<T> = Box<dyn Fn(Vec<T>) -> Vec<Vec<T>>>;

/// Build a "chunking hint" out of a string hint with `\n`-separated
/// lists of ','-separated lists of path prefixes
/// The intended use-case is to run something like:
/// `parse_chunking_hint(read_to_string(path)?)?`
/// Chunking hint is a list of lists of `MPath`, which
/// allows a `Chunker` to group `MPath`s which start with prefixes
/// from a given list into a single chunk.
pub fn parse_chunking_hint(hint: String) -> Result<Vec<Vec<MPath>>, Error> {
    hint.split('\n')
        .filter_map(|line| {
            let line = line.trim_matches(|c| c == ' ' || c == '\n');
            if !line.is_empty() {
                let v: Result<Vec<MPath>, Error> = line
                    .split(',')
                    .filter_map(|prefix| {
                        let trimmed = prefix.trim_matches(|c| c == ' ' || c == '\n');
                        if !trimmed.is_empty() {
                            Some(MPath::new(trimmed))
                        } else {
                            None
                        }
                    })
                    .collect();

                Some(v)
            } else {
                None
            }
        })
        .collect()
}

/// Build a `Chunker<MPath>` from a "chunking hint"
/// See `parse_chunking_hint` docstrign for details
pub fn path_chunker_from_hint(prefix_lists: Vec<Vec<MPath>>) -> Result<Chunker<MPath>, Error> {
    Ok(Box::new(move |mpaths| {
        // In case some paths don't match any prefix, let's just put
        // them all in a separate commit
        let mut res: Vec<Vec<MPath>> = vec![vec![]; prefix_lists.len() + 1];
        let last_index = prefix_lists.len();

        for mpath in mpaths {
            // we need to find if `mpath` fits into any of the `prefix_lists`
            match prefix_lists.iter().position(|prefix_list| {
                prefix_list
                    .iter()
                    .any(|prefix| MPath::is_prefix_of_opt(Some(prefix), &mpath))
            }) {
                Some(chunk_index) => {
                    // we need to put this `mpath` into `chunk_index` position
                    res[chunk_index].push(mpath);
                }
                None => {
                    // Does not belong to any prefix list, let's put it into the
                    // last chunk
                    res[last_index].push(mpath);
                }
            }
        }

        // Let's clean up potential empty chunks
        let res: Vec<Vec<MPath>> = res.into_iter().filter(|v| !v.is_empty()).collect();

        res
    }))
}

pub fn even_chunker_with_max_size<T: Clone>(max_chunk_size: usize) -> Result<Chunker<T>, Error> {
    Ok(Box::new(move |items| {
        let res: Vec<Vec<T>> = items
            .chunks(max_chunk_size)
            .map(|chunk| chunk.to_vec())
            .collect();
        res
    }))
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_parse_chunking_hint() {
        let hint = r#"/a/b, /a/c

        /a/d,

        "#;

        let parsed = parse_chunking_hint(hint.to_string()).unwrap();
        assert_eq!(
            parsed,
            vec![
                vec![MPath::new("/a/b").unwrap(), MPath::new("/a/c").unwrap()],
                vec![MPath::new("/a/d").unwrap()]
            ]
        );

        let hint = "/a/b";
        let parsed = parse_chunking_hint(hint.to_string()).unwrap();
        assert_eq!(parsed, vec![vec![MPath::new("/a/b").unwrap()]]);
    }

    #[test]
    fn test_path_chunked_form_hint() {
        let hint = parse_chunking_hint(
            r#"
            /a/b, /a/c
            /a/d, /b
        "#
            .to_string(),
        )
        .unwrap();

        let chunker = path_chunker_from_hint(hint).unwrap();
        let mpaths: Vec<MPath> = vec!["/d/e/f", "/a", "/a/b/c", "/a/c", "/b/w/z", "/a/d"]
            .into_iter()
            .map(|p| MPath::new(p).unwrap())
            .collect();
        let chunked = chunker(mpaths);

        let expeected_chunk_0: Vec<MPath> = vec!["/a/b/c", "/a/c"]
            .into_iter()
            .map(|p| MPath::new(p).unwrap())
            .collect();
        let expeected_chunk_1: Vec<MPath> = vec!["/b/w/z", "/a/d"]
            .into_iter()
            .map(|p| MPath::new(p).unwrap())
            .collect();
        let expeected_chunk_2: Vec<MPath> = vec!["/d/e/f", "/a"]
            .into_iter()
            .map(|p| MPath::new(p).unwrap())
            .collect();
        assert_eq!(
            chunked,
            vec![expeected_chunk_0, expeected_chunk_1, expeected_chunk_2]
        )
    }

    #[test]
    fn test_path_chunked_form_hint_with_empty() {
        let hint = parse_chunking_hint(
            r#"
            /a/b, /a/c
            /ababagalamaga
            /a/d, /b
            /a, /d/e/f
        "#
            .to_string(),
        )
        .unwrap();

        let chunker = path_chunker_from_hint(hint).unwrap();
        let mpaths: Vec<MPath> = vec!["/d/e/f", "/a", "/a/b/c", "/a/c", "/b/w/z", "/a/d"]
            .into_iter()
            .map(|p| MPath::new(p).unwrap())
            .collect();
        let chunked = chunker(mpaths);

        let expeected_chunk_0: Vec<MPath> = vec!["/a/b/c", "/a/c"]
            .into_iter()
            .map(|p| MPath::new(p).unwrap())
            .collect();
        let expeected_chunk_1: Vec<MPath> = vec!["/b/w/z", "/a/d"]
            .into_iter()
            .map(|p| MPath::new(p).unwrap())
            .collect();
        let expeected_chunk_2: Vec<MPath> = vec!["/d/e/f", "/a"]
            .into_iter()
            .map(|p| MPath::new(p).unwrap())
            .collect();
        assert_eq!(
            chunked,
            vec![expeected_chunk_0, expeected_chunk_1, expeected_chunk_2]
        )
    }
}


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

use std::collections::HashMap;

use metaconfig_types::DefaultSmallToLargeCommitSyncPathAction;
use metaconfig_types::SmallRepoCommitSyncConfig;
use mononoke_types::MPath;

pub struct SmallRepoCommitSyncConfigDiff {
    pub default_action_change: Option<(
        DefaultSmallToLargeCommitSyncPathAction,
        DefaultSmallToLargeCommitSyncPathAction,
    )>,
    pub mapping_added: HashMap<MPath, MPath>,
    pub mapping_changed: HashMap<MPath, (MPath, MPath)>,
    pub mapping_removed: HashMap<MPath, MPath>,
}

pub fn diff_small_repo_commit_sync_configs(
    from: SmallRepoCommitSyncConfig,
    to: SmallRepoCommitSyncConfig,
) -> SmallRepoCommitSyncConfigDiff {
    let default_action_change = if from.default_action == to.default_action {
        None
    } else {
        Some((from.default_action, to.default_action))
    };

    let mut mapping_added = HashMap::new();
    let mut mapping_changed = HashMap::new();
    let mut mapping_removed = HashMap::new();

    for (from_small, from_large) in &from.map {
        match to.map.get(from_small) {
            Some(to_large) if from_large != to_large => {
                mapping_changed.insert(from_small.clone(), (from_large.clone(), to_large.clone()));
            }
            Some(_to_large) => {
                // nothing has changed
            }
            None => {
                mapping_removed.insert(from_small.clone(), from_large.clone());
            }
        };
    }

    for (to_small, to_large) in &to.map {
        if !from.map.contains_key(to_small) {
            mapping_added.insert(to_small.clone(), to_large.clone());
        }
    }

    SmallRepoCommitSyncConfigDiff {
        default_action_change,
        mapping_added,
        mapping_changed,
        mapping_removed,
    }
}


/*
 * 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::format_err;
use anyhow::Error;
use blobrepo::save_bonsai_changesets;
use bookmarks::BookmarkKey;
use bookmarks::BookmarkUpdateReason;
use bookmarks::BookmarksRef;
use changesets::ChangesetsRef;
use context::CoreContext;
use mercurial_derivation::DeriveHgChangeset;
use mercurial_types::HgChangesetId;
use mercurial_types::MPath;
use mononoke_types::BonsaiChangeset;
use mononoke_types::BonsaiChangesetMut;
use mononoke_types::ChangesetId;
use mononoke_types::DateTime;
use mononoke_types::FileChange;
use phases::PhasesRef;
use repo_blobstore::RepoBlobstoreRef;
use repo_derived_data::RepoDerivedDataRef;
use slog::info;
use sorted_vector_map::SortedVectorMap;

use crate::chunking::Chunker;

pub trait Repo =
    ChangesetsRef + RepoBlobstoreRef + PhasesRef + BookmarksRef + RepoDerivedDataRef + Send + Sync;

#[derive(Clone, Debug)]
pub struct ChangesetArgs {
    pub author: String,
    pub message: String,
    pub datetime: DateTime,
    pub bookmark: Option<BookmarkKey>,
    pub mark_public: bool,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StackPosition(pub usize);

/// For creating stacks of changesets
pub trait ChangesetArgsFactory = Fn(StackPosition) -> ChangesetArgs;

pub async fn create_save_and_generate_hg_changeset(
    ctx: &CoreContext,
    repo: &impl Repo,
    parents: Vec<ChangesetId>,
    file_changes: SortedVectorMap<MPath, FileChange>,
    changeset_args: ChangesetArgs,
) -> Result<HgChangesetId, Error> {
    let bcs_id = create_and_save_bonsai(ctx, repo, parents, file_changes, changeset_args).await?;
    generate_hg_changeset(ctx, repo, bcs_id).await
}

pub async fn create_and_save_bonsai(
    ctx: &CoreContext,
    repo: &impl Repo,
    parents: Vec<ChangesetId>,
    file_changes: SortedVectorMap<MPath, FileChange>,
    changeset_args: ChangesetArgs,
) -> Result<ChangesetId, Error> {
    let ChangesetArgs {
        author,
        message,
        datetime,
        bookmark: maybe_bookmark,
        mark_public,
    } = changeset_args;
    let bcs = create_bonsai_changeset_only(parents, file_changes, author, message, datetime)?;
    let bcs_id = save_and_maybe_mark_public(ctx, repo, bcs, mark_public).await?;

    if let Some(bookmark) = maybe_bookmark {
        create_bookmark(ctx, repo, bookmark, bcs_id).await?;
    }

    Ok(bcs_id)
}

async fn save_and_maybe_mark_public(
    ctx: &CoreContext,
    repo: &impl Repo,
    bcs: BonsaiChangeset,
    mark_public: bool,
) -> Result<ChangesetId, Error> {
    let bcs_id = bcs.get_changeset_id();
    save_bonsai_changesets(vec![bcs], ctx.clone(), repo).await?;

    if mark_public {
        repo.phases()
            .add_reachable_as_public(ctx, vec![bcs_id])
            .await?;
        info!(ctx.logger(), "Marked as public {:?}", bcs_id);
    }
    Ok(bcs_id)
}

async fn generate_hg_changeset(
    ctx: &CoreContext,
    repo: &impl Repo,
    bcs_id: ChangesetId,
) -> Result<HgChangesetId, Error> {
    info!(ctx.logger(), "Generating an HG equivalent of {:?}", bcs_id);
    let hg_cs_id = repo.derive_hg_changeset(ctx, bcs_id).await?;

    info!(
        ctx.logger(),
        "Hg equivalent of {:?} is: {:?}", bcs_id, hg_cs_id
    );
    Ok(hg_cs_id)
}

async fn create_bookmark(
    ctx: &CoreContext,
    repo: &impl BookmarksRef,
    bookmark: BookmarkKey,
    bcs_id: ChangesetId,
) -> Result<(), Error> {
    info!(
        ctx.logger(),
        "Setting bookmark {:?} to point to {:?}", bookmark, bcs_id
    );
    let mut transaction = repo.bookmarks().create_transaction(ctx.clone());
    transaction.force_set(&bookmark, bcs_id, BookmarkUpdateReason::ManualMove)?;

    let commit_result = transaction.commit().await?;

    if !commit_result {
        Err(format_err!("Logical failure while setting {:?}", bookmark))
    } else {
        info!(ctx.logger(), "Setting bookmark {:?} finished", bookmark);
        Ok(())
    }
}

fn create_bonsai_changeset_only(
    parents: Vec<ChangesetId>,
    file_changes: SortedVectorMap<MPath, FileChange>,
    author: String,
    message: String,
    datetime: DateTime,
) -> Result<BonsaiChangeset, Error> {
    BonsaiChangesetMut {
        parents,
        author,
        author_date: datetime,
        committer: None,
        committer_date: None,
        message,
        hg_extra: Default::default(),
        git_extra_headers: None,
        git_tree_hash: None,
        file_changes,
        is_snapshot: false,
        git_annotated_tag: None,
    }
    .freeze()
}

pub async fn delete_files_in_chunks<'a>(
    ctx: &'a CoreContext,
    repo: &'a impl Repo,
    parent_bcs_id: ChangesetId,
    mpaths: Vec<MPath>,
    chunker: &Chunker<MPath>,
    delete_commits_changeset_args_factory: &impl ChangesetArgsFactory,
    skip_last_chunk: bool,
) -> Result<Vec<ChangesetId>, Error> {
    info!(ctx.logger(), "Chunking mpaths");
    let mpath_chunks: Vec<Vec<MPath>> = chunker(mpaths);
    info!(ctx.logger(), "Done chunking working copy contents");

    let mut delete_commits: Vec<ChangesetId> = Vec::new();
    let mut parent = parent_bcs_id;
    let chunk_num = mpath_chunks.len();
    for (i, mpath_chunk) in mpath_chunks.into_iter().enumerate() {
        if i == chunk_num - 1 && skip_last_chunk {
            break;
        }

        let changeset_args = delete_commits_changeset_args_factory(StackPosition(i));
        let file_changes: SortedVectorMap<MPath, _> = mpath_chunk
            .into_iter()
            .map(|mp| (mp, FileChange::Deletion))
            .collect();
        info!(
            ctx.logger(),
            "Creating delete commit #{} with {:?} (deleting {} files)",
            i,
            changeset_args,
            file_changes.len()
        );
        let delete_cs_id =
            create_and_save_bonsai(ctx, repo, vec![parent], file_changes, changeset_args).await?;
        info!(ctx.logger(), "Done creating delete commit #{}", i);
        delete_commits.push(delete_cs_id);

        // move one step forward
        parent = delete_cs_id;
    }

    Ok(delete_commits)
}


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

use anyhow::Error;
use blobrepo::BlobRepo;
use context::CoreContext;
use mercurial_types::MPath;
use mononoke_types::ChangesetId;

use crate::chunking::Chunker;
use crate::common::delete_files_in_chunks;
use crate::common::ChangesetArgsFactory;
use crate::working_copy::get_changed_content_working_copy_paths;

/// A struct containing pre-merge delete information
/// Pre-merge delete commits look like this:
/// ```text
///                         C3'
///                         |
///       D2                C2'
///       |                 |
///       D1                C1'
///       |                 |
///       master         correct
/// ```
/// Where:
///   `D1`, `D2`: are gradual deletion commits of files that need their history fixed up
///   `C1`, `C2`, `C3`: are gradual deletion commits of all of other files on the branch
///                     containing correct history (so we merge only the files that needed fixup)
///   `master`: a head of an independent DAG to be merged into
///   `master`: a head of an independent DAG to be merged into
///   `correct`: branch containing correct history for paths
///
/// Note that the order of commits in `delete_commits_fixup_branch`
/// corresponds to the order of indices on the diagram:
/// - `delete_commits_fixup_branch = [D1, D2]`
pub struct HistoryFixupDeletes {
    pub delete_commits_fixup_branch: Vec<ChangesetId>,
    pub delete_commits_correct_branch: Vec<ChangesetId>,
}

/// Create `HistoryFixupDeletes` struct, implementing gradual delete strategy
/// See the struct's docstring for more details about the end state
/// See also <https://fb.quip.com/JfHhAyOZ2FBj> for strategy and discussion
pub async fn create_history_fixup_deletes<'a>(
    ctx: &'a CoreContext,
    repo: &'a BlobRepo,
    fixup_bcs_id: ChangesetId,
    chunker: Chunker<MPath>,
    delete_commits_changeset_args_factory: impl ChangesetArgsFactory,
    correct_bcs_id: ChangesetId,
    paths_to_fixup: Vec<MPath>,
) -> Result<HistoryFixupDeletes, Error> {
    let delete_commits_fixup_branch = delete_files_in_chunks(
        ctx,
        repo,
        fixup_bcs_id,
        paths_to_fixup.clone(),
        &chunker,
        &delete_commits_changeset_args_factory,
        false, /* skip last chunk */
    )
    .await?;

    let mut paths_to_remove: BTreeSet<_> =
        get_changed_content_working_copy_paths(ctx, repo, correct_bcs_id, fixup_bcs_id)
            .await?
            .into_iter()
            .collect();
    for path in paths_to_fixup.iter() {
        paths_to_remove.remove(path);
    }
    let delete_commits_correct_branch = delete_files_in_chunks(
        ctx,
        repo,
        correct_bcs_id,
        paths_to_remove.into_iter().collect(),
        &chunker,
        &delete_commits_changeset_args_factory,
        false, /* skip last chunk */
    )
    .await?;

    Ok(HistoryFixupDeletes {
        delete_commits_fixup_branch,
        delete_commits_correct_branch,
    })
}

#[cfg(test)]
mod test {
    use std::collections::BTreeSet;

    use cloned::cloned;
    use fbinit::FacebookInit;
    use fixtures::TestRepoFixture;
    use fixtures::UnsharedMergeUneven;
    use mononoke_types::DateTime;
    use tests_utils::resolve_cs_id;

    use super::*;
    use crate::common::ChangesetArgs;
    use crate::common::StackPosition;
    use crate::working_copy::get_working_copy_paths;

    #[fbinit::test]
    async fn test_create_fixup_deletes(fb: FacebookInit) -> Result<(), Error> {
        let repo = UnsharedMergeUneven::getrepo(fb).await;
        let ctx = CoreContext::test_mock(fb);

        // Side of the history that needs fixing up is one line of commit with the following
        // files in the working copy: 1 2 3 4 5
        // The "correct" history has: 1 2 3 4 5 6 7
        // This test attempts to fixup the history of files 1 and 2.
        let fixup_bcs_id =
            resolve_cs_id(&ctx, &repo, "03b0589d9788870817d03ce7b87516648ed5b33a").await?;
        let correct_bcs_id =
            resolve_cs_id(&ctx, &repo, "5a3e8d5a475ec07895e64ec1e1b2ec09bfa70e4e").await?;
        let create_delete_cs_args = |num: StackPosition| ChangesetArgs {
            author: "user".to_string(),
            message: format!("Delete: {}", num.0),
            datetime: DateTime::from_rfc3339("1985-04-12T23:20:50.52Z").unwrap(),
            bookmark: None,
            mark_public: false,
        };

        let one = MPath::new("1").unwrap();
        let two = MPath::new("2").unwrap();
        let five = MPath::new("5").unwrap();
        let six = MPath::new("6").unwrap();

        // Arrage everything into [[1], [...], [10]]
        let chunker = Box::new({
            cloned!(one);
            move |mpaths| {
                let mut v1: Vec<MPath> = vec![];
                let mut v2: Vec<MPath> = vec![];

                for mpath in mpaths {
                    if mpath == one {
                        v1.push(mpath);
                    } else {
                        v2.push(mpath);
                    }
                }

                if v1.is_empty() {
                    vec![v2]
                } else if v2.is_empty() {
                    vec![v1]
                } else {
                    vec![v1, v2]
                }
            }
        });

        let hfd = create_history_fixup_deletes(
            &ctx,
            &repo,
            fixup_bcs_id,
            chunker,
            create_delete_cs_args,
            correct_bcs_id,
            vec![one.clone(), two.clone()],
        )
        .await?;

        let HistoryFixupDeletes {
            delete_commits_fixup_branch,
            delete_commits_correct_branch,
        } = hfd;

        assert_eq!(delete_commits_fixup_branch.len(), 2);
        assert_eq!(delete_commits_correct_branch.len(), 1);

        // Validate delete commits
        let fixup_branch_after_deletions = delete_commits_fixup_branch[1];
        let correct_branch_after_deletions = delete_commits_correct_branch[0];

        // We expect that the "fixup" branch which used to have files 1-5 to
        // have just files 3-5 (no more 1 and 2)
        let fixup_working_copy: BTreeSet<MPath> =
            get_working_copy_paths(&ctx, &repo, fixup_branch_after_deletions)
                .await
                .unwrap()
                .into_iter()
                .collect();

        assert!(!fixup_working_copy.contains(&one));
        assert!(!fixup_working_copy.contains(&two));
        assert!(fixup_working_copy.contains(&five));
        assert!(!fixup_working_copy.contains(&six));

        // We expect that the "correct" branch which used to have files 1-7 to
        // have just files 1-5 (because we want to merge in 1-2 and 3-5 are the same
        // so they don't matter, 6-7 are not present in the fixup branch).
        let correct_working_copy: BTreeSet<MPath> =
            get_working_copy_paths(&ctx, &repo, correct_branch_after_deletions)
                .await
                .unwrap()
                .into_iter()
                .collect();

        assert!(correct_working_copy.contains(&one));
        assert!(correct_working_copy.contains(&two));
        assert!(correct_working_copy.contains(&five));
        assert!(!correct_working_copy.contains(&six));
        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.
 */

#![feature(trait_alias)]
use std::collections::BTreeMap;
use std::num::NonZeroU64;

use anyhow::anyhow;
use anyhow::Error;
use blobrepo::BlobRepo;
use blobstore::Loadable;
use bonsai_hg_mapping::BonsaiHgMappingRef;
use context::CoreContext;
use futures::future;
use futures::Stream;
use futures::TryStreamExt;
use itertools::Itertools;
use manifest::ManifestOps;
use mercurial_derivation::DeriveHgChangeset;
use mercurial_types::blobs::HgBlobChangeset;
use mercurial_types::blobs::HgBlobEnvelope;
use mercurial_types::HgChangesetId;
use mercurial_types::MPath;
use mononoke_types::ChangesetId;
use mononoke_types::ContentId;
use mononoke_types::FileChange;
use mononoke_types::FileType;
use movers::Mover;
use repo_blobstore::RepoBlobstoreRef;
use slog::info;

pub mod chunking;
pub mod commit_sync_config_utils;
pub mod common;
pub mod history_fixup_delete;
pub mod pre_merge_delete;
pub mod working_copy;

use crate::common::create_save_and_generate_hg_changeset;
use crate::common::ChangesetArgs;
use crate::common::ChangesetArgsFactory;
use crate::common::StackPosition;

const BUFFER_SIZE: usize = 100;
const REPORTING_INTERVAL_FILES: usize = 10000;

struct FileMove {
    old_path: MPath,
    maybe_new_path: Option<MPath>,
    file_type: FileType,
    file_size: u64,
    content_id: ContentId,
}

fn get_all_file_moves<'a>(
    ctx: &'a CoreContext,
    repo: &'a BlobRepo,
    hg_cs: HgBlobChangeset,
    path_converter: &'a Mover,
) -> impl Stream<Item = Result<FileMove, Error>> + 'a {
    hg_cs
        .manifestid()
        .list_leaf_entries(ctx.clone(), repo.repo_blobstore().clone())
        .try_filter_map(move |(old_path, (file_type, filenode_id))| async move {
            let maybe_new_path = path_converter(&old_path).unwrap();
            if Some(&old_path) == maybe_new_path.as_ref() {
                // path does not need to be changed, drop from the stream
                Ok(None)
            } else {
                // path needs to be changed (or deleted), keep in the stream
                Ok(Some((old_path, maybe_new_path, file_type, filenode_id)))
            }
        })
        .map_ok({
            move |(old_path, maybe_new_path, file_type, filenode_id)| {
                async move {
                    let file_envelope = filenode_id.load(ctx, repo.repo_blobstore()).await?;

                    // Note: it is always safe to unwrap here, since
                    // `HgFileEnvelope::get_size()` always returns `Some()`
                    // The return type is `Option` to acommodate `HgManifestEnvelope`
                    // which returns `None`.
                    let file_size = file_envelope.get_size().unwrap();

                    let file_move = FileMove {
                        old_path,
                        maybe_new_path,
                        file_type,
                        file_size,
                        content_id: file_envelope.content_id(),
                    };
                    Ok(file_move)
                }
            }
        })
        .try_buffer_unordered(BUFFER_SIZE)
}

pub async fn perform_move<'a>(
    ctx: &'a CoreContext,
    repo: &'a BlobRepo,
    parent_bcs_id: ChangesetId,
    path_converter: Mover,
    resulting_changeset_args: ChangesetArgs,
) -> Result<HgChangesetId, Error> {
    let mut stack = perform_stack_move_impl(
        ctx,
        repo,
        parent_bcs_id,
        path_converter,
        |file_moves| vec![file_moves],
        move |_| resulting_changeset_args.clone(),
    )
    .await?;

    if stack.len() == 1 {
        stack
            .pop()
            .ok_or_else(|| anyhow!("not a single commit was created"))
    } else {
        Err(anyhow!(
            "wrong number of commits was created. Expected 1, created {}: {:?}",
            stack.len(),
            stack
        ))
    }
}

/// Move files according to path_converter in a stack of commits.
/// Each commit won't have more than `max_num_of_moves_in_commit` files.
/// Creating a stack of commits might be desirable if we want to keep each commit smaller.
pub async fn perform_stack_move<'a>(
    ctx: &'a CoreContext,
    repo: &'a BlobRepo,
    parent_bcs_id: ChangesetId,
    path_converter: Mover,
    max_num_of_moves_in_commit: NonZeroU64,
    resulting_changeset_args: impl ChangesetArgsFactory + 'a,
) -> Result<Vec<HgChangesetId>, Error> {
    perform_stack_move_impl(
        ctx,
        repo,
        parent_bcs_id,
        path_converter,
        |file_changes| {
            file_changes
                .into_iter()
                .chunks(max_num_of_moves_in_commit.get() as usize)
                .into_iter()
                .map(|chunk| chunk.collect())
                .collect()
        },
        resulting_changeset_args,
    )
    .await
}

async fn perform_stack_move_impl<'a, Chunker>(
    ctx: &'a CoreContext,
    repo: &'a BlobRepo,
    mut parent_bcs_id: ChangesetId,
    path_converter: Mover,
    chunker: Chunker,
    resulting_changeset_args: impl ChangesetArgsFactory + 'a,
) -> Result<Vec<HgChangesetId>, Error>
where
    Chunker: Fn(Vec<FileMove>) -> Vec<Vec<FileMove>> + 'a,
{
    let parent_hg_cs_id = repo.derive_hg_changeset(ctx, parent_bcs_id).await?;

    let parent_hg_cs = parent_hg_cs_id.load(ctx, repo.repo_blobstore()).await?;

    let mut file_changes = get_all_file_moves(ctx, repo, parent_hg_cs, &path_converter)
        .try_fold(vec![], {
            move |mut collected, file_move| {
                collected.push(file_move);
                if collected.len() % REPORTING_INTERVAL_FILES == 0 {
                    info!(ctx.logger(), "Processed {} files", collected.len());
                }
                future::ready(Ok(collected))
            }
        })
        .await?;

    let mut res = vec![];
    file_changes.sort_unstable_by(|first, second| first.old_path.cmp(&second.old_path));
    let chunks_iter = chunker(file_changes);

    for (idx, chunk) in chunks_iter.into_iter().enumerate() {
        let mut file_changes = BTreeMap::new();
        for file_move in chunk {
            file_changes.insert(file_move.old_path.clone(), FileChange::Deletion);
            if let Some(to) = file_move.maybe_new_path {
                let fc = FileChange::tracked(
                    file_move.content_id,
                    file_move.file_type,
                    file_move.file_size,
                    Some((file_move.old_path, parent_bcs_id)),
                );
                file_changes.insert(to, fc);
            }
        }

        let hg_cs_id = create_save_and_generate_hg_changeset(
            ctx,
            repo,
            vec![parent_bcs_id],
            file_changes.into(),
            resulting_changeset_args(StackPosition(idx)),
        )
        .await?;

        parent_bcs_id = repo
            .bonsai_hg_mapping()
            .get_bonsai_from_hg(ctx, hg_cs_id)
            .await?
            .ok_or_else(|| anyhow!("not found bonsai commit for {}", hg_cs_id))?;
        res.push(hg_cs_id)
    }

    Ok(res)
}

#[cfg(test)]
mod test {
    use std::str::FromStr;
    use std::sync::Arc;

    use anyhow::Result;
    use cloned::cloned;
    use fbinit::FacebookInit;
    use fixtures::Linear;
    use fixtures::ManyFilesDirs;
    use fixtures::TestRepoFixture;
    use futures::FutureExt;
    use mercurial_types::HgChangesetId;
    use mononoke_types::BonsaiChangeset;
    use mononoke_types::BonsaiChangesetMut;
    use mononoke_types::DateTime;
    use sorted_vector_map::sorted_vector_map;
    use tests_utils::resolve_cs_id;

    use super::*;

    fn identity_mover(p: &MPath) -> Result<Option<MPath>> {
        Ok(Some(p.clone()))
    }

    fn skip_one(p: &MPath) -> Result<Option<MPath>> {
        if &MPath::new("dir1/file_1_in_dir1").unwrap() == p {
            return Ok(None);
        }

        Ok(Some(p.clone()))
    }

    fn shift_one(p: &MPath) -> Result<Option<MPath>> {
        if &MPath::new("dir1/file_1_in_dir1").unwrap() == p {
            return Ok(Some(MPath::new("newdir/dir1/file_1_in_dir1").unwrap()));
        }

        Ok(Some(p.clone()))
    }

    fn shift_one_skip_another(p: &MPath) -> Result<Option<MPath>> {
        if &MPath::new("dir1/file_1_in_dir1").unwrap() == p {
            return Ok(Some(MPath::new("newdir/dir1/file_1_in_dir1").unwrap()));
        }

        if &MPath::new("dir2/file_1_in_dir2").unwrap() == p {
            return Ok(None);
        }

        Ok(Some(p.clone()))
    }

    fn shift_all(p: &MPath) -> Result<Option<MPath>> {
        Ok(Some(MPath::new("moved_dir")?.join(p)))
    }

    async fn prepare(
        fb: FacebookInit,
    ) -> (
        CoreContext,
        BlobRepo,
        HgChangesetId,
        ChangesetId,
        ChangesetArgs,
    ) {
        let repo = ManyFilesDirs::getrepo(fb).await;
        let ctx = CoreContext::test_mock(fb);

        let hg_cs_id = HgChangesetId::from_str("2f866e7e549760934e31bf0420a873f65100ad63").unwrap();
        let bcs_id: ChangesetId = repo
            .bonsai_hg_mapping()
            .get_bonsai_from_hg(&ctx, hg_cs_id)
            .await
            .unwrap()
            .unwrap();
        let changeset_args = ChangesetArgs {
            author: "user".to_string(),
            message: "I like to move it".to_string(),
            datetime: DateTime::from_rfc3339("1985-04-12T23:20:50.52Z").unwrap(),
            bookmark: None,
            mark_public: false,
        };
        (ctx, repo, hg_cs_id, bcs_id, changeset_args)
    }

    async fn get_bonsai_by_hg_cs_id(
        ctx: CoreContext,
        repo: BlobRepo,
        hg_cs_id: HgChangesetId,
    ) -> BonsaiChangeset {
        let bcs_id = repo
            .bonsai_hg_mapping()
            .get_bonsai_from_hg(&ctx, hg_cs_id)
            .await
            .unwrap()
            .unwrap();
        bcs_id.load(&ctx, repo.repo_blobstore()).await.unwrap()
    }

    #[fbinit::test]
    async fn test_do_not_move_anything(fb: FacebookInit) {
        let (ctx, repo, _hg_cs_id, bcs_id, changeset_args) = prepare(fb).await;
        let newcs = perform_move(
            &ctx,
            &repo,
            bcs_id,
            Arc::new(identity_mover),
            changeset_args,
        )
        .await
        .unwrap();
        let newcs = get_bonsai_by_hg_cs_id(ctx.clone(), repo.clone(), newcs).await;

        let BonsaiChangesetMut {
            parents,
            file_changes,
            ..
        } = newcs.into_mut();
        assert_eq!(parents, vec![bcs_id]);
        assert_eq!(file_changes, Default::default());
    }

    #[fbinit::test]
    async fn test_drop_file(fb: FacebookInit) {
        let (ctx, repo, _hg_cs_id, bcs_id, changeset_args) = prepare(fb).await;
        let newcs = perform_move(&ctx, &repo, bcs_id, Arc::new(skip_one), changeset_args)
            .await
            .unwrap();
        let newcs = get_bonsai_by_hg_cs_id(ctx.clone(), repo.clone(), newcs).await;

        let BonsaiChangesetMut {
            parents,
            file_changes,
            ..
        } = newcs.into_mut();
        assert_eq!(parents, vec![bcs_id]);
        assert_eq!(
            file_changes,
            sorted_vector_map! {
                MPath::new("dir1/file_1_in_dir1").unwrap() => FileChange::Deletion
            }
        );
    }

    #[fbinit::test]
    async fn test_shift_path_by_one(fb: FacebookInit) {
        let (ctx, repo, _hg_cs_id, bcs_id, changeset_args) = prepare(fb).await;
        let newcs = perform_move(&ctx, &repo, bcs_id, Arc::new(shift_one), changeset_args)
            .await
            .unwrap();
        let newcs = get_bonsai_by_hg_cs_id(ctx.clone(), repo.clone(), newcs).await;

        let BonsaiChangesetMut {
            parents,
            file_changes,
            ..
        } = newcs.into_mut();
        assert_eq!(parents, vec![bcs_id]);
        let old_path = MPath::new("dir1/file_1_in_dir1").unwrap();
        let new_path = MPath::new("newdir/dir1/file_1_in_dir1").unwrap();
        assert_eq!(file_changes[&old_path], FileChange::Deletion);
        let file_change = match &file_changes[&new_path] {
            FileChange::Change(tc) => tc,
            _ => panic!(),
        };
        assert_eq!(file_change.copy_from(), Some((old_path, bcs_id)).as_ref());
    }

    async fn get_working_copy_contents(
        ctx: CoreContext,
        repo: BlobRepo,
        hg_cs_id: HgChangesetId,
    ) -> BTreeMap<MPath, (FileType, ContentId)> {
        let hg_cs = hg_cs_id.load(&ctx, repo.repo_blobstore()).await.unwrap();
        hg_cs
            .manifestid()
            .list_leaf_entries(ctx.clone(), repo.repo_blobstore().clone())
            .and_then({
                cloned!(ctx, repo);
                move |(path, (file_type, filenode_id))| {
                    cloned!(ctx, repo);
                    async move { filenode_id.load(&ctx, repo.repo_blobstore()).await }
                        .map(move |env| Ok((path, (file_type, env?.content_id()))))
                }
            })
            .try_collect()
            .await
            .unwrap()
    }

    #[fbinit::test]
    async fn test_performed_move(fb: FacebookInit) {
        let (ctx, repo, old_hg_cs_id, old_bcs_id, changeset_args) = prepare(fb).await;
        let new_hg_cs_id = perform_move(
            &ctx,
            &repo,
            old_bcs_id,
            Arc::new(shift_one_skip_another),
            changeset_args,
        )
        .await
        .unwrap();
        let mut old_wc = get_working_copy_contents(ctx.clone(), repo.clone(), old_hg_cs_id).await;
        let mut new_wc = get_working_copy_contents(ctx.clone(), repo.clone(), new_hg_cs_id).await;
        let _removed_file = old_wc.remove(&MPath::new("dir2/file_1_in_dir2").unwrap());
        let old_moved_file = old_wc.remove(&MPath::new("dir1/file_1_in_dir1").unwrap());
        let new_moved_file = new_wc.remove(&MPath::new("newdir/dir1/file_1_in_dir1").unwrap());
        // Same file should live in both locations
        assert_eq!(old_moved_file, new_moved_file);
        // After removing renamed and removed files, both working copies should be identical
        assert_eq!(old_wc, new_wc);
    }

    #[fbinit::test]
    async fn test_stack_move(fb: FacebookInit) -> Result<(), Error> {
        let repo = Linear::getrepo(fb).await;
        let ctx = CoreContext::test_mock(fb);

        let old_bcs_id = resolve_cs_id(&ctx, &repo, "master").await?;
        let create_cs_args = |num: StackPosition| ChangesetArgs {
            author: "user".to_string(),
            message: format!("I like to delete it: {}", num.0),
            datetime: DateTime::from_rfc3339("1985-04-12T23:20:50.52Z").unwrap(),
            bookmark: None,
            mark_public: false,
        };

        let stack = perform_stack_move(
            &ctx,
            &repo,
            old_bcs_id,
            Arc::new(shift_all),
            NonZeroU64::new(1).unwrap(),
            create_cs_args,
        )
        .await?;

        // 11 files, so create 1 commit for each
        assert_eq!(stack.len(), 11);

        let stack = perform_stack_move(
            &ctx,
            &repo,
            old_bcs_id,
            Arc::new(shift_all),
            NonZeroU64::new(2).unwrap(),
            create_cs_args,
        )
        .await?;
        assert_eq!(stack.len(), 6);

        let last_hg_cs_id = stack.last().unwrap();
        let last_hg_cs = last_hg_cs_id.load(&ctx, repo.repo_blobstore()).await?;

        let leaf_entries = last_hg_cs
            .manifestid()
            .list_leaf_entries(ctx.clone(), repo.repo_blobstore().clone())
            .try_collect::<Vec<_>>()
            .await?;

        assert_eq!(leaf_entries.len(), 11);
        let prefix = MPath::new("moved_dir")?;
        for (leaf, _) in &leaf_entries {
            assert!(prefix.is_prefix_of(leaf));
        }

        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 anyhow::Error;
use blobrepo::BlobRepo;
use context::CoreContext;
use mercurial_types::MPath;
use mononoke_types::ChangesetId;

use crate::chunking::Chunker;
use crate::common::delete_files_in_chunks;
use crate::common::ChangesetArgsFactory;
use crate::working_copy::get_changed_working_copy_paths;
use crate::working_copy::get_working_copy_paths;

/// A struct containing pre-merge delete information
/// Pre-merge delete commits look like this:
/// ```text
///       D3
///       |
///       D2
///       |
///       D1
///       |
/// pre-merge-bookmark
/// ```
/// Where:
///   `D1`, `D2`: are gradual deletion commits
///   `pre-merge-bookmark`: a head of an independent DAG to be merged
///
/// Note that the order of commits in `delete_commits`
/// corresponds to the order of indices on the diagram:
/// - `delete_commits = [D1, D3, D3]`
pub struct PreMergeDelete {
    pub delete_commits: Vec<ChangesetId>,
}

/// Create `PreMergeDelete` struct, implementing gradual delete strategy
/// See the struct's docstring for more details about the end state
/// See also <https://fb.quip.com/jPbqA3kK3qCi> for strategy and discussion
pub async fn create_pre_merge_delete<'a>(
    ctx: &'a CoreContext,
    repo: &'a BlobRepo,
    parent_bcs_id: ChangesetId,
    chunker: Chunker<MPath>,
    delete_commits_changeset_args_factory: impl ChangesetArgsFactory,
    base_cs_id: Option<ChangesetId>,
) -> Result<PreMergeDelete, Error> {
    let mpaths = match base_cs_id {
        Some(base_cs_id) => {
            get_changed_working_copy_paths(ctx, repo, parent_bcs_id, base_cs_id).await?
        }
        None => get_working_copy_paths(ctx, repo, parent_bcs_id).await?,
    };
    let delete_commits = delete_files_in_chunks(
        ctx,
        repo,
        parent_bcs_id,
        mpaths,
        &chunker,
        &delete_commits_changeset_args_factory,
        true, /* skip_last_chunk */
    )
    .await?;

    Ok(PreMergeDelete { delete_commits })
}

#[cfg(test)]
mod test {
    use std::collections::HashSet;

    use cloned::cloned;
    use fbinit::FacebookInit;
    use fixtures::Linear;
    use fixtures::TestRepoFixture;
    use mononoke_types::DateTime;
    use tests_utils::resolve_cs_id;
    use tests_utils::CreateCommitContext;

    use super::*;
    use crate::common::ChangesetArgs;
    use crate::common::StackPosition;

    #[fbinit::test]
    async fn test_create_pre_merge_delete(fb: FacebookInit) -> Result<(), Error> {
        let repo = Linear::getrepo(fb).await;
        let ctx = CoreContext::test_mock(fb);

        let bcs_id = resolve_cs_id(&ctx, &repo, "master").await?;
        let create_delete_cs_args = |num: StackPosition| ChangesetArgs {
            author: "user".to_string(),
            message: format!("Delete: {}", num.0),
            datetime: DateTime::from_rfc3339("1985-04-12T23:20:50.52Z").unwrap(),
            bookmark: None,
            mark_public: false,
        };

        let one = MPath::new("1").unwrap();
        let ten = MPath::new("10").unwrap();
        let two = MPath::new("2").unwrap();

        // Arrage everything into [[1], [...], [10]]
        let chunker = Box::new({
            cloned!(one, ten);
            move |mpaths| {
                let mut v1: Vec<MPath> = vec![];
                let mut v2: Vec<MPath> = vec![];
                let mut v3: Vec<MPath> = vec![];

                for mpath in mpaths {
                    if mpath == one {
                        v1.push(mpath);
                    } else if mpath == ten {
                        v3.push(mpath);
                    } else {
                        v2.push(mpath);
                    }
                }

                vec![v1, v2, v3]
            }
        });

        let pmd =
            create_pre_merge_delete(&ctx, &repo, bcs_id, chunker, create_delete_cs_args, None)
                .await?;

        let PreMergeDelete { delete_commits } = pmd;

        assert_eq!(delete_commits.len(), 2);

        // Validate delete commits
        let delete_commit_0 = delete_commits[0];
        let delete_commit_1 = delete_commits[1];

        let working_copy_0: HashSet<MPath> = get_working_copy_paths(&ctx, &repo, delete_commit_0)
            .await
            .unwrap()
            .into_iter()
            .collect();

        assert!(!working_copy_0.contains(&one));
        assert!(working_copy_0.contains(&two));
        assert!(working_copy_0.contains(&ten));

        let working_copy_1: HashSet<MPath> = get_working_copy_paths(&ctx, &repo, delete_commit_1)
            .await
            .unwrap()
            .into_iter()
            .collect();

        assert!(!working_copy_1.contains(&one));
        assert!(!working_copy_1.contains(&two));
        assert!(working_copy_1.contains(&ten));
        Ok(())
    }

    #[fbinit::test]
    async fn test_create_pre_merge_delete_with_base(fb: FacebookInit) -> Result<(), Error> {
        let repo = Linear::getrepo(fb).await;
        let ctx = CoreContext::test_mock(fb);

        let master_bcs_id = resolve_cs_id(&ctx, &repo, "master").await?;
        // Create two commits on top of master:
        //   A   B
        //    \ /
        //     |
        //     O
        //

        let create_delete_cs_args = |num: StackPosition| ChangesetArgs {
            author: "user".to_string(),
            message: format!("Delete: {}", num.0),
            datetime: DateTime::from_rfc3339("1985-04-12T23:20:50.52Z").unwrap(),
            bookmark: None,
            mark_public: false,
        };

        let commit_a = CreateCommitContext::new(&ctx, &repo, vec![master_bcs_id])
            .add_file("common", "common")
            .add_file("changed", "first")
            .add_file("added", "added_content")
            .add_file("added2", "added_content")
            .commit()
            .await?;

        let commit_b = CreateCommitContext::new(&ctx, &repo, vec![master_bcs_id])
            .add_file("common", "common")
            .add_file("changed", "second")
            .add_file("somethingelse", "content")
            .commit()
            .await?;
        let commit_b = CreateCommitContext::new(&ctx, &repo, vec![commit_b])
            .add_file("common", "common")
            // Revert the file content to the same value - it should still be
            // reported as changed
            .add_file("changed", "first")
            .add_file("somethingelse", "content")
            .commit()
            .await?;

        let changed_path = MPath::new("changed")?;
        let added_path = MPath::new("added")?;
        let added2_path = MPath::new("added2")?;

        let chunker = Box::new({
            cloned!(changed_path, added_path);
            move |mpaths| {
                let mut v1: Vec<MPath> = vec![];
                let mut v2: Vec<MPath> = vec![];
                let mut v3: Vec<MPath> = vec![];

                for mpath in mpaths {
                    if mpath == changed_path {
                        v1.push(mpath);
                    } else if mpath == added_path {
                        v2.push(mpath);
                    } else {
                        v3.push(mpath);
                    }
                }

                vec![v1, v2, v3]
            }
        });
        let pmd = create_pre_merge_delete(
            &ctx,
            &repo,
            commit_a,
            chunker,
            create_delete_cs_args,
            Some(commit_b),
        )
        .await?;

        // 2 files should be deleted - "changed" and "added" with two deletion commits
        let PreMergeDelete { delete_commits } = pmd;

        assert_eq!(delete_commits.len(), 2);
        // Validate delete commits
        let delete_commit_0 = delete_commits[0];
        let delete_commit_1 = delete_commits[1];

        let working_copy_0: HashSet<MPath> = get_working_copy_paths(&ctx, &repo, delete_commit_0)
            .await
            .unwrap()
            .into_iter()
            .collect();

        assert!(!working_copy_0.contains(&changed_path));
        assert!(working_copy_0.contains(&added_path));
        assert!(working_copy_0.contains(&added2_path));

        let working_copy_1: HashSet<MPath> = get_working_copy_paths(&ctx, &repo, delete_commit_1)
            .await
            .unwrap()
            .into_iter()
            .collect();

        assert!(!working_copy_1.contains(&changed_path));
        assert!(!working_copy_1.contains(&added_path));
        assert!(working_copy_1.contains(&added2_path));
        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 anyhow::Error;
use blobrepo::BlobRepo;
use blobstore::Loadable;
use context::CoreContext;
use derived_data::BonsaiDerived;
use fsnodes::RootFsnodeId;
use futures::future::try_join;
use futures::TryStreamExt;
use manifest::Diff;
use manifest::ManifestOps;
use mercurial_derivation::DeriveHgChangeset;
use mercurial_types::MPath;
use mononoke_types::ChangesetId;
use repo_blobstore::RepoBlobstoreRef;
use slog::info;
use unodes::RootUnodeManifestId;

pub async fn get_working_copy_paths(
    ctx: &CoreContext,
    repo: &BlobRepo,
    bcs_id: ChangesetId,
) -> Result<Vec<MPath>, Error> {
    let hg_cs_id = repo.derive_hg_changeset(ctx, bcs_id).await?;

    let hg_cs = hg_cs_id.load(ctx, repo.repo_blobstore()).await?;
    info!(ctx.logger(), "Getting working copy contents");
    let mut paths: Vec<_> = hg_cs
        .manifestid()
        .list_leaf_entries(ctx.clone(), repo.repo_blobstore().clone())
        .map_ok(|(path, (_file_type, _filenode_id))| path)
        .try_collect()
        .await?;
    paths.sort();
    info!(ctx.logger(), "Done getting working copy contents");
    Ok(paths)
}

pub async fn get_changed_content_working_copy_paths(
    ctx: &CoreContext,
    repo: &BlobRepo,
    bcs_id: ChangesetId,
    base_cs_id: ChangesetId,
) -> Result<Vec<MPath>, Error> {
    let unode_id = RootFsnodeId::derive(ctx, repo, bcs_id);
    let base_unode_id = RootFsnodeId::derive(ctx, repo, base_cs_id);

    let (unode_id, base_unode_id) = try_join(unode_id, base_unode_id).await?;

    let mut paths = base_unode_id
        .fsnode_id()
        .diff(
            ctx.clone(),
            repo.repo_blobstore().clone(),
            *unode_id.fsnode_id(),
        )
        .try_filter_map(|diff| async move {
            use Diff::*;
            let maybe_path = match diff {
                Added(maybe_path, entry) => entry.into_leaf().and(maybe_path),
                Removed(_maybe_path, _entry) => None,
                Changed(maybe_path, _old_entry, new_entry) => new_entry.into_leaf().and(maybe_path),
            };

            Ok(maybe_path)
        })
        .try_collect::<Vec<_>>()
        .await?;

    paths.sort();
    Ok(paths)
}

pub async fn get_colliding_paths_between_commits(
    ctx: &CoreContext,
    repo: &BlobRepo,
    bcs_id: ChangesetId,
    base_cs_id: ChangesetId,
) -> Result<Vec<MPath>, Error> {
    let unode_id = RootFsnodeId::derive(ctx, repo, bcs_id);
    let base_unode_id = RootFsnodeId::derive(ctx, repo, base_cs_id);

    let (unode_id, base_unode_id) = try_join(unode_id, base_unode_id).await?;

    let mut paths = base_unode_id
        .fsnode_id()
        .diff(
            ctx.clone(),
            repo.repo_blobstore().clone(),
            *unode_id.fsnode_id(),
        )
        .try_filter_map(|diff| async move {
            use Diff::*;
            let maybe_path = match diff {
                Added(_maybe_path, _entry) => None,
                Removed(_maybe_path, _entry) => None,
                Changed(maybe_path, _old_entry, new_entry) => new_entry.into_leaf().and(maybe_path),
            };

            Ok(maybe_path)
        })
        .try_collect::<Vec<_>>()
        .await?;

    paths.sort();
    Ok(paths)
}

pub async fn get_changed_working_copy_paths(
    ctx: &CoreContext,
    repo: &BlobRepo,
    bcs_id: ChangesetId,
    base_cs_id: ChangesetId,
) -> Result<Vec<MPath>, Error> {
    let unode_id = RootUnodeManifestId::derive(ctx, repo, bcs_id);
    let base_unode_id = RootUnodeManifestId::derive(ctx, repo, base_cs_id);

    let (unode_id, base_unode_id) = try_join(unode_id, base_unode_id).await?;

    let mut paths = base_unode_id
        .manifest_unode_id()
        .diff(
            ctx.clone(),
            repo.repo_blobstore().clone(),
            *unode_id.manifest_unode_id(),
        )
        .try_filter_map(|diff| async move {
            use Diff::*;
            let maybe_path = match diff {
                Added(maybe_path, entry) => entry.into_leaf().and(maybe_path),
                Removed(_maybe_path, _entry) => None,
                Changed(maybe_path, _old_entry, new_entry) => new_entry.into_leaf().and(maybe_path),
            };

            Ok(maybe_path)
        })
        .try_collect::<Vec<_>>()
        .await?;

    paths.sort();
    Ok(paths)
}

pub async fn get_working_copy_paths_by_prefixes(
    ctx: &CoreContext,
    repo: &BlobRepo,
    bcs_id: ChangesetId,
    prefixes: impl IntoIterator<Item = MPath>,
) -> Result<Vec<MPath>, Error> {
    let unode_id = RootUnodeManifestId::derive(ctx, repo, bcs_id).await?;
    let mut paths = unode_id
        .manifest_unode_id()
        .list_leaf_entries_under(ctx.clone(), repo.repo_blobstore().clone(), prefixes)
        .map_ok(|(mpath, _)| mpath)
        .try_collect::<Vec<MPath>>()
        .await?;
    paths.sort();
    Ok(paths)
}