/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

use anyhow::Result;
use mockall::{predicate::*, Sequence};
use once_cell::sync::Lazy;
use sdk_sync::fs::Fs;
use sdk_sync::git::{Commit, CommitHash};
use sdk_sync::init_tracing;
use sdk_sync::sync::gen::{GeneratedSdk, SdkGenerator};
use sdk_sync::sync::{Sync, BOT_EMAIL, BOT_NAME, MODEL_STASH_BRANCH_NAME};
use sdk_sync::versions::VersionsManifest;
use std::path::{Path, PathBuf};
use std::sync::Arc;

static INIT_TRACING: Lazy<bool> = Lazy::new(|| {
    init_tracing();
    true
});

mockall::mock! {
    CreateSdkGenerator {}
    impl sdk_sync::sync::CreateSdkGenerator for CreateSdkGenerator {
        fn create_sdk_generator(
            &self,
            aws_doc_sdk_examples_revision: &CommitHash,
            examples_path: &Path,
            fs: Arc<dyn Fs>,
            reset_to_commit: Option<CommitHash>,
            original_smithy_rs_path: &Path,
        ) -> Result<Box<dyn SdkGenerator>>;
    }
}

mockall::mock! {
    Fs {}
    impl sdk_sync::fs::Fs for Fs {
        fn delete_all_generated_files_and_folders(&self, directory: &Path) -> Result<()>;
        fn find_handwritten_files_and_folders(
            &self,
            aws_sdk_path: &Path,
            build_artifacts_path: &Path,
        ) -> Result<Vec<PathBuf>>;
        fn remove_dir_all_idempotent(&self, path: &Path) -> Result<()>;
        fn read_to_string(&self, path: &Path) -> Result<String>;
        fn remove_file_idempotent(&self, path: &Path) -> Result<()>;
        fn recursive_copy(&self, source: &Path, destination: &Path) -> Result<()>;
    }
}

mockall::mock! {
    Git {}
    impl sdk_sync::git::Git for Git {
        fn path(&self) -> &Path;
        fn clone_to(&self, path: &Path) -> Result<()>;
        fn get_head_revision(&self) -> Result<CommitHash>;
        fn stage(&self, path: &Path) -> Result<()>;
        fn commit_on_behalf(
            &self,
            bot_name: &str,
            bot_email: &str,
            author_name: &str,
            author_email: &str,
            message: &str,
        ) -> Result<()>;
        fn commit(&self, name: &str, email: &str, message: &str) -> Result<()>;
        fn rev_list<'a>(
            &self,
            start_inclusive_revision: &str,
            end_exclusive_revision: &str,
            path: Option<&'a Path>,
        ) -> Result<Vec<CommitHash>>;
        fn show(&self, revision: &str) -> Result<Commit>;
        fn hard_reset(&self, revision: &str) -> Result<()>;
        fn current_branch_name(&self) -> Result<String>;
        fn create_branch(&self, branch_name: &str, revision: &str) -> Result<()>;
        fn fast_forward_merge(&self, branch_name: &str) -> Result<()>;
        fn untracked_files(&self) -> Result<Vec<PathBuf>>;
        fn changed_files(&self) -> Result<Vec<PathBuf>>;
    }
}

mockall::mock! {
    SdkGenerator {}
    impl sdk_sync::sync::gen::SdkGenerator for SdkGenerator {
        fn generate_sdk(&self) -> Result<GeneratedSdk>;
    }
}

mockall::mock! {
    Versions {}
    impl sdk_sync::versions::Versions for Versions {
        fn load(&self, aws_sdk_rust_path: &Path) -> Result<VersionsManifest>;
    }
}

fn set_path(mock_git: &mut MockGit, path: &str) {
    mock_git.expect_path().return_const(PathBuf::from(path));
}

fn expect_get_head_revision(repo: &mut MockGit, seq: &mut Sequence, head: &'static str) {
    repo.expect_get_head_revision()
        .once()
        .in_sequence(seq)
        .returning(move || Ok(CommitHash::from(head)));
}

fn expect_show_commit(repo: &mut MockGit, commit: Commit) {
    let hash = commit.hash.as_ref().to_string();
    repo.expect_show()
        .withf(move |h| hash == h)
        .once()
        .returning(move |_| Ok(commit.clone()));
}

fn expect_hard_reset(repo: &mut MockGit, seq: &mut Sequence, hash: &str) {
    let hash = hash.to_string();
    repo.expect_hard_reset()
        .withf(move |h| h == hash)
        .once()
        .in_sequence(seq)
        .returning(|_| Ok(()));
}

fn expect_stage(repo: &mut MockGit, seq: &mut Sequence, path: &'static str) {
    repo.expect_stage()
        .withf(move |p| p.to_string_lossy() == path)
        .once()
        .in_sequence(seq)
        .returning(|_| Ok(()));
}

fn expect_has_changes(repo: &mut MockGit, seq: &mut Sequence, changes: bool) {
    repo.expect_untracked_files()
        .once()
        .in_sequence(seq)
        .returning(move || Ok(Vec::new()));
    repo.expect_changed_files()
        .once()
        .in_sequence(seq)
        .returning(move || {
            Ok(if changes {
                vec![PathBuf::from("some-file")]
            } else {
                Vec::new()
            })
        });
}

#[derive(Default)]
struct Mocks {
    aws_doc_sdk_examples: MockGit,
    aws_sdk_rust: MockGit,
    smithy_rs: MockGit,
    fs: MockFs,
    versions: MockVersions,
    create_sdk_generator: MockCreateSdkGenerator,
}

impl Mocks {
    fn into_sync(self) -> Sync {
        Sync::new_with(
            self.aws_doc_sdk_examples,
            self.aws_sdk_rust,
            self.smithy_rs,
            self.fs,
            self.versions,
            self.create_sdk_generator,
        )
    }

    fn set_smithyrs_commits_to_sync(
        &mut self,
        previous_synced_commit: &'static str,
        hashes: &'static [&'static str],
    ) {
        self.smithy_rs
            .expect_rev_list()
            .withf(move |begin, end, path| {
                begin == "HEAD" && end == previous_synced_commit && path.is_none()
            })
            .once()
            .returning(|_, _, _| Ok(hashes.iter().map(|&hash| CommitHash::from(hash)).collect()));
    }

    fn expect_recursive_copy(
        &mut self,
        seq: &mut Sequence,
        source: &'static str,
        dest: &'static str,
    ) {
        self.fs
            .expect_recursive_copy()
            .withf(move |src, dst| src.to_string_lossy() == source && dst.to_string_lossy() == dest)
            .once()
            .in_sequence(seq)
            .returning(|_, _| Ok(()));
    }

    fn expect_delete_all_generated_files_and_folders(
        &mut self,
        seq: &mut Sequence,
        sdk_path: &'static str,
    ) {
        self.fs
            .expect_delete_all_generated_files_and_folders()
            .withf(move |p| p.to_string_lossy() == sdk_path)
            .once()
            .in_sequence(seq)
            .returning(|_| Ok(()));
    }

    fn expect_find_handwritten_files_and_folders(
        &mut self,
        seq: &mut Sequence,
        sdk_path: &'static str,
        artifacts_path: &'static str,
        files: &'static [&'static str],
    ) {
        self.fs
            .expect_find_handwritten_files_and_folders()
            .withf(move |aws_sdk_p, artifacts_p| {
                aws_sdk_p.to_string_lossy() == sdk_path
                    && artifacts_p.to_string_lossy() == artifacts_path
            })
            .once()
            .in_sequence(seq)
            .returning(move |_, _| Ok(files.iter().map(PathBuf::from).collect()));
    }
}

fn expect_copy_sdk(mocks: &mut Mocks) {
    let mut seq = Sequence::new();
    mocks.expect_delete_all_generated_files_and_folders(&mut seq, "/p2/aws-sdk-rust");
    mocks.expect_find_handwritten_files_and_folders(
        &mut seq,
        "/p2/aws-sdk-rust",
        "/p2/some-temp-cloned-smithy-rs/aws/sdk/build/aws-sdk",
        &[], // no handwritten files found
    );
    mocks.expect_recursive_copy(
        &mut seq,
        "/p2/some-temp-cloned-smithy-rs/aws/sdk/build/aws-sdk/.",
        "/p2/aws-sdk-rust",
    );
}

fn expect_generate_sdk(
    mocks: &mut Mocks,
    expected_examples_revision: &'static str,
    expected_examples_path: &'static str,
    expected_reset_to_commit: Option<&str>,
    expected_original_smithy_rs_path: &'static str,
) {
    let expected_reset_to_commit = expected_reset_to_commit.map(|c| CommitHash::from(c));
    mocks
        .create_sdk_generator
        .expect_create_sdk_generator()
        .withf(
            move |examples_revision,
                  examples_path,
                  _fs,
                  reset_to_commit,
                  original_smithy_rs_path| {
                examples_revision.as_ref() == expected_examples_revision
                    && examples_path.to_string_lossy() == expected_examples_path
                    && reset_to_commit == &expected_reset_to_commit
                    && original_smithy_rs_path.to_string_lossy() == expected_original_smithy_rs_path
            },
        )
        .once()
        .returning(|_, _, _, _, _| {
            let mut mock = MockSdkGenerator::new();
            mock.expect_generate_sdk().once().returning(|| {
                Ok(GeneratedSdk::new(
                    "/p2/some-temp-cloned-smithy-rs/aws/sdk/build/aws-sdk",
                ))
            });
            Ok(Box::new(mock))
        });
}

fn expect_successful_smithyrs_sync(
    mocks: &mut Mocks,
    seq: &mut Sequence,
    commit: Commit,
    expected_commit_message: &str,
) {
    expect_show_commit(&mut mocks.smithy_rs, commit.clone());
    expect_generate_sdk(
        mocks,
        "old-examples-hash",
        "/p2/aws-sdk-rust/examples",
        Some(commit.hash.as_ref()),
        "/p2/smithy-rs",
    );
    expect_copy_sdk(mocks);

    // Commit generated SDK
    expect_has_changes(&mut mocks.aws_sdk_rust, seq, true);
    expect_stage(&mut mocks.aws_sdk_rust, seq, ".");
    let expected_commit_message = expected_commit_message.to_string();
    mocks
        .aws_sdk_rust
        .expect_commit_on_behalf()
        .withf(
            move |bot_name, bot_email, author_name, author_email, message| {
                bot_name == BOT_NAME
                    && bot_email == BOT_EMAIL
                    && author_name == commit.author_name
                    && author_email == commit.author_email
                    && message == expected_commit_message
            },
        )
        .once()
        .returning(|_, _, _, _, _| Ok(()));
    mocks
        .aws_sdk_rust
        .expect_get_head_revision()
        .once()
        .returning(|| Ok(CommitHash::from("newly-synced-hash")));
}

fn expect_successful_example_sync(
    mocks: &mut Mocks,
    seq: &mut Sequence,
    old_examples_hash: &'static str,
    example_commits: &[Commit],
) {
    // Example revision discovery
    {
        let example_commits = example_commits.to_vec();
        mocks
            .aws_doc_sdk_examples
            .expect_rev_list()
            .withf(move |begin, end, path| {
                begin == "HEAD"
                    && end == old_examples_hash
                    && *path == Some(&PathBuf::from("rust_dev_preview"))
            })
            .once()
            .in_sequence(seq)
            .returning(move |_, _, _| {
                Ok(example_commits.iter().cloned().map(|c| c.hash).collect())
            });
    }

    // Codegen
    expect_generate_sdk(
        mocks,
        "hash2",
        "/p2/aws-doc-sdk-examples/rust_dev_preview",
        None,
        "/p2/smithy-rs",
    );
    expect_copy_sdk(mocks);

    // Commit generated SDK
    expect_stage(&mut mocks.aws_sdk_rust, seq, ".");
    for commit in example_commits {
        expect_show_commit(&mut mocks.aws_doc_sdk_examples, commit.clone());
    }
    mocks
        .aws_sdk_rust
        .expect_commit()
        .withf(|name, email, message| {
            name == BOT_NAME
                && email == BOT_EMAIL
                && message.starts_with("[examples] Sync SDK examples")
        })
        .once()
        .in_sequence(seq)
        .returning(|_, _, _| Ok(()));
}

fn expect_model_changes(mocks: &mut Mocks, seq: &mut Sequence, models_changed: bool) {
    let (old_hash, new_hash) = if models_changed {
        ("with-new-models", "without-new-models")
    } else {
        ("no-new-models", "no-new-models")
    };
    expect_get_head_revision(&mut mocks.smithy_rs, seq, old_hash);

    mocks
        .smithy_rs
        .expect_create_branch()
        .with(eq(MODEL_STASH_BRANCH_NAME), eq("HEAD"))
        .once()
        .in_sequence(seq)
        .returning(|_, _| Ok(()));

    mocks
        .smithy_rs
        .expect_current_branch_name()
        .once()
        .in_sequence(seq)
        .returning(|| Ok("main".to_string()));

    expect_hard_reset(&mut mocks.smithy_rs, seq, "origin/main");
    expect_get_head_revision(&mut mocks.smithy_rs, seq, new_hash);
}

fn expect_sync_model_changes(mocks: &mut Mocks, seq: &mut Sequence) {
    // Expect merge of the models back into the main branch
    mocks
        .smithy_rs
        .expect_fast_forward_merge()
        .with(eq(MODEL_STASH_BRANCH_NAME))
        .once()
        .in_sequence(seq)
        .returning(|_| Ok(()));

    // HEAD has the model changes; set the commit info for it
    expect_show_commit(
        &mut mocks.smithy_rs,
        Commit {
            hash: "HEAD".into(),
            author_name: BOT_NAME.into(),
            author_email: BOT_EMAIL.into(),
            message_subject: "Some model changes".into(),
            message_body: "".into(),
        },
    );

    // Codegen
    expect_generate_sdk(
        mocks,
        "old-examples-hash",
        "/p2/aws-sdk-rust/examples",
        None,
        "/p2/smithy-rs",
    );
    expect_copy_sdk(mocks);

    // Commit generated SDK
    expect_has_changes(&mut mocks.aws_sdk_rust, seq, true);
    expect_stage(&mut mocks.aws_sdk_rust, seq, ".");
    mocks
        .aws_sdk_rust
        .expect_commit()
        .withf(|name, email, message| {
            name == BOT_NAME && email == BOT_EMAIL && message.starts_with("Some model changes")
        })
        .once()
        .in_sequence(seq)
        .returning(|_, _, _| Ok(()));
}

#[test]
fn mocked_e2e_without_model_changes() {
    assert!(*INIT_TRACING);
    let mut mocks = Mocks::default();
    let mut seq = Sequence::new();

    set_path(&mut mocks.aws_doc_sdk_examples, "/p2/aws-doc-sdk-examples");
    set_path(&mut mocks.aws_sdk_rust, "/p2/aws-sdk-rust");
    set_path(&mut mocks.smithy_rs, "/p2/smithy-rs");

    mocks
        .versions
        .expect_load()
        .withf(|p| p.to_string_lossy() == "/p2/aws-sdk-rust")
        .once()
        .returning(|_| {
            Ok(VersionsManifest {
                smithy_rs_revision: "some-previous-commit-hash".into(),
                aws_doc_sdk_examples_revision: "old-examples-hash".into(),
            })
        });
    mocks
        .set_smithyrs_commits_to_sync("some-previous-commit-hash", &["hash-newest", "hash-oldest"]);

    expect_model_changes(&mut mocks, &mut seq, false);

    expect_successful_smithyrs_sync(
        &mut mocks,
        &mut seq,
        Commit {
            hash: "hash-oldest".into(),
            author_name: "Some Dev".into(),
            author_email: "somedev@example.com".into(),
            message_subject: "Some commit subject".into(),
            message_body: "".into(),
        },
        "[smithy-rs] Some commit subject",
    );
    expect_successful_smithyrs_sync(
        &mut mocks,
        &mut seq,
        Commit {
            hash: "hash-newest".into(),
            author_name: "Another Dev".into(),
            author_email: "anotherdev@example.com".into(),
            message_subject: "Another commit subject".into(),
            message_body: "This one has a body\n\n- bullet\n- bullet\n\nmore".into(),
        },
        "[smithy-rs] Another commit subject\n\nThis one has a body\n\n- bullet\n- bullet\n\nmore",
    );

    expect_successful_example_sync(
        &mut mocks,
        &mut seq,
        "old-examples-hash",
        &[
            Commit {
                hash: "hash2".into(),
                author_name: "Some Example Writer".into(),
                author_email: "someexamplewriter@example.com".into(),
                message_subject: "More examples".into(),
                message_body: "".into(),
            },
            Commit {
                hash: "hash1".into(),
                author_name: "Another Example Writer".into(),
                author_email: "anotherexamplewriter@example.com".into(),
                message_subject: "Another example".into(),
                message_body: "This one has a body\n\n- bullet\n- bullet\n\nmore".into(),
            },
        ],
    );

    let sync = mocks.into_sync();
    sync.sync().expect("success");
}

#[test]
fn mocked_e2e_with_model_changes() {
    assert!(*INIT_TRACING);
    let mut mocks = Mocks::default();
    let mut seq = Sequence::new();

    set_path(&mut mocks.aws_doc_sdk_examples, "/p2/aws-doc-sdk-examples");
    set_path(&mut mocks.aws_sdk_rust, "/p2/aws-sdk-rust");
    set_path(&mut mocks.smithy_rs, "/p2/smithy-rs");

    mocks
        .versions
        .expect_load()
        .withf(|p| p.to_string_lossy() == "/p2/aws-sdk-rust")
        .once()
        .returning(|_| {
            Ok(VersionsManifest {
                smithy_rs_revision: "some-previous-commit-hash".into(),
                aws_doc_sdk_examples_revision: "old-examples-hash".into(),
            })
        });
    mocks
        .set_smithyrs_commits_to_sync("some-previous-commit-hash", &["hash-newest", "hash-oldest"]);

    expect_model_changes(&mut mocks, &mut seq, true);

    expect_successful_smithyrs_sync(
        &mut mocks,
        &mut seq,
        Commit {
            hash: "hash-oldest".into(),
            author_name: "Some Dev".into(),
            author_email: "somedev@example.com".into(),
            message_subject: "Some commit subject".into(),
            message_body: "".into(),
        },
        "[smithy-rs] Some commit subject",
    );
    expect_successful_smithyrs_sync(
        &mut mocks,
        &mut seq,
        Commit {
            hash: "hash-newest".into(),
            author_name: "Another Dev".into(),
            author_email: "anotherdev@example.com".into(),
            message_subject: "Another commit subject".into(),
            message_body: "This one has a body\n\n- bullet\n- bullet\n\nmore".into(),
        },
        "[smithy-rs] Another commit subject\n\nThis one has a body\n\n- bullet\n- bullet\n\nmore",
    );

    expect_sync_model_changes(&mut mocks, &mut seq);

    expect_successful_example_sync(
        &mut mocks,
        &mut seq,
        "old-examples-hash",
        &[
            Commit {
                hash: "hash2".into(),
                author_name: "Some Example Writer".into(),
                author_email: "someexamplewriter@example.com".into(),
                message_subject: "More examples".into(),
                message_body: "".into(),
            },
            Commit {
                hash: "hash1".into(),
                author_name: "Another Example Writer".into(),
                author_email: "anotherexamplewriter@example.com".into(),
                message_subject: "Another example".into(),
                message_body: "This one has a body\n\n- bullet\n- bullet\n\nmore".into(),
            },
        ],
    );

    let sync = mocks.into_sync();
    sync.sync().expect("success");
}
