package com.atlassian.bitbucket.mesh.git.revert;

import com.atlassian.bitbucket.mesh.AbstractGrpcTest;
import com.atlassian.bitbucket.mesh.MockResponseObserver;
import com.atlassian.bitbucket.mesh.git.GitAgent;
import com.atlassian.bitbucket.mesh.git.GitRepositories;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilder;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.commit.Commit;
import com.atlassian.bitbucket.mesh.git.commit.CommitManager;
import com.atlassian.bitbucket.mesh.git.commit.GetCommitParameters;
import com.atlassian.bitbucket.mesh.git.ref.Branch;
import com.atlassian.bitbucket.mesh.process.handler.StringStdioHandler;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.*;
import com.atlassian.bitbucket.mesh.test.annotation.DirtiesRepo;
import com.atlassian.bitbucket.mesh.test.annotation.Repo;
import com.google.protobuf.ByteString;
import io.grpc.ManagedChannel;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import org.junit.Before;
import org.junit.Test;

import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeoutException;

import static com.atlassian.bitbucket.mesh.git.GitRefPattern.HEADS;
import static com.atlassian.bitbucket.mesh.rpc.v1.RpcMergeStrategy.MERGE_STRATEGY_NO_FF;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.Matchers.contains;

public class RevertRequestTest extends AbstractGrpcTest {

    private static final String SUCCESS_MESSAGE = "This merge should succeed and also be pushed";

    private final RpcPerson author = RpcPerson.newBuilder()
            .setEmail("jdoe@test.com")
            .setName(ByteString.copyFromUtf8("John Doe"))
            .build();

    private final RpcPerson committer = RpcPerson.newBuilder()
            .setEmail("janed@test.com")
            .setName(ByteString.copyFromUtf8("Jane Doe"))
            .build();

    @Resource
    private GitAgent agent;
    @Resource
    private GitProcessBuilderFactory builderFactory;
    @Resource
    private CommitManager commitManager;
    @Resource
    private ManagedChannel managedChannel;
    @Repo(GitRepositories.MERGE)
    private Repository merge;
    @Repo
    private Repository repository;
    private CommitServiceGrpc.CommitServiceStub stub;

    @Before
    public void setup() {
        stub = CommitServiceGrpc.newStub(managedChannel);
    }

    @DirtiesRepo("repository")
    @Test
    public void testRevertCommit() throws TimeoutException, InterruptedException {
        Branch branch = agent.resolveHead(repository);
        RpcRevertRequest.Builder builder = RpcRevertRequest.newBuilder()
                .setRepository(repository.getId())
                .setCommitter(committer)
                .setCommitId("57150c54c38d6570b2fd5e6d6cfc19476de44e84")
                .setBranch(branch.toBranch());

        RpcBranch revertBranch = revert(builder).getBranch();

        assertNotNull(revertBranch);
        assertEquals(branch.getId(), revertBranch.getId().toStringUtf8());

        Commit revertCommit = commitManager.getCommit(new GetCommitParameters.Builder(repository,
                revertBranch.getLatestCommit())
                .build());

        assertNotNull(revertCommit);
        assertEquals(committer, revertCommit.getAuthor().toPerson());
        assertEquals(committer, revertCommit.getCommitter().toPerson());
        assertThat(revertCommit.getParents(), contains(branch.getLatestCommit()));

        String diff = processBuilder(repository).diff()
                .ancestor(branch.getLatestCommit())
                .rev(revertCommit.getId())
                .build(new StringStdioHandler(UTF_8))
                .run();
        assertEquals("diff --git a/mv_file/moved_file.txt b/mv_file/moved_file.txt\n" +
                "deleted file mode 100644\n" +
                "index f49abb7..0000000\n" +
                "--- a/mv_file/moved_file.txt\n" +
                "+++ /dev/null\n" +
                "@@ -1 +0,0 @@\n" +
                "-This file will be moved\n" +
                "diff --git a/mv_file/mv_file.txt b/mv_file/mv_file.txt\n" +
                "new file mode 100644\n" +
                "index 0000000..f49abb7\n" +
                "--- /dev/null\n" +
                "+++ b/mv_file/mv_file.txt\n" +
                "@@ -0,0 +1 @@\n" +
                "+This file will be moved", diff);
    }

    @DirtiesRepo("repository")
    @Test
    public void testRevertCommitWithTargetBranchCreated() throws TimeoutException, InterruptedException {
        Branch branch = agent.resolveHead(repository);
        RpcRevertRequest.Builder builder = RpcRevertRequest.newBuilder()
                .setRepository(repository.getId())
                .setCommitter(committer)
                .setCommitId("57150c54c38d6570b2fd5e6d6cfc19476de44e84")
                .setBranch(branch.toBranch())
                .setTargetBranch("revert-branch");

        RpcBranch revertBranch = revert(builder).getBranch();

        assertNotNull(revertBranch);
        assertEquals("refs/heads/revert-branch", revertBranch.getId().toStringUtf8());

        Branch expectedRevertBranch = agent.resolveBranch(repository, "revert-branch");
        assertNotNull(expectedRevertBranch);
        assertEquals(expectedRevertBranch.toBranch(), revertBranch);

        Commit revertCommit = commitManager.getCommit(new GetCommitParameters.Builder(repository,
                revertBranch.getLatestCommit()).build());

        assertNotNull(revertCommit);
        assertEquals(committer, revertCommit.getAuthor().toPerson());
        assertEquals(committer, revertCommit.getCommitter().toPerson());
        assertThat(revertCommit.getParents(), contains(branch.getLatestCommit()));

        String diff = processBuilder(repository).diff()
                .ancestor(branch.getLatestCommit())
                .rev(revertCommit.getId())
                .build(new StringStdioHandler(UTF_8))
                .run();
        assertEquals("diff --git a/mv_file/moved_file.txt b/mv_file/moved_file.txt\n" +
                "deleted file mode 100644\n" +
                "index f49abb7..0000000\n" +
                "--- a/mv_file/moved_file.txt\n" +
                "+++ /dev/null\n" +
                "@@ -1 +0,0 @@\n" +
                "-This file will be moved\n" +
                "diff --git a/mv_file/mv_file.txt b/mv_file/mv_file.txt\n" +
                "new file mode 100644\n" +
                "index 0000000..f49abb7\n" +
                "--- /dev/null\n" +
                "+++ b/mv_file/mv_file.txt\n" +
                "@@ -0,0 +1 @@\n" +
                "+This file will be moved", diff);

        Branch branchAfterRevert = agent.resolveBranch(repository, branch.getId());
        assertEquals(branch, branchAfterRevert);
    }

    @DirtiesRepo("merge")
    @Test
    public void testRevertMergedCommit() throws TimeoutException, InterruptedException {
        RpcBranch branch = runMergeWithStrategy(
                "branch_that_has_same_text_file_modified_on_both_clean_merge_src",
                "306d584aa574fa74901d3e288b2931b2c4f0e927", "master",
                "017caf31eca7c46eb3d1800fcac431cfa7147a01", MERGE_STRATEGY_NO_FF);
        String commit = branch.getLatestCommit();

        RpcRevertRequest.Builder builder = RpcRevertRequest.newBuilder()
                .setRepository(merge.getId())
                .setCommitter(committer)
                .setCommitId(commit)
                .setBranch(branch)
                .setMergedCommitParentNumber(1)
                .setTargetBranch("revert-branch");
        RpcBranch revertBranch = revert(builder).getBranch();

        assertNotNull(revertBranch);
        assertEquals("revert-branch", HEADS.unqualify(revertBranch.getId().toStringUtf8()));

        Branch expectedRevertBranch = agent.resolveBranch(merge, "revert-branch");
        assertNotNull(expectedRevertBranch);
        assertEquals(expectedRevertBranch.toBranch(), revertBranch);

        Commit revertCommit = commitManager.getCommit(new GetCommitParameters.Builder(merge,
                revertBranch.getLatestCommit())
                .build());

        assertNotNull(revertCommit);
        assertEquals(committer, revertCommit.getAuthor().toPerson());
        assertEquals(committer, revertCommit.getCommitter().toPerson());
        assertThat(revertCommit.getParents(), contains(commit));

        String diff = processBuilder(merge).diff()
                .ancestor(commit)
                .rev(revertCommit.getId())
                .build(new StringStdioHandler(UTF_8))
                .run();

        assertEquals("diff --git a/file.txt b/file.txt\n" +
                "deleted file mode 100644\n" +
                "index 2b22581..0000000\n" +
                "--- a/file.txt\n" +
                "+++ /dev/null\n" +
                "@@ -1,3 +0,0 @@\n" +
                "-Line one modified\n" +
                "-Line two\n" +
                "-Line three", diff);

        Branch branchAfterRevert = agent.resolveBranch(merge, branch.getId().toStringUtf8());
        assertNotNull(branchAfterRevert);
        assertEquals(branch, branchAfterRevert.toBranch());
    }

    @DirtiesRepo("merge")
    @Test
    public void testRevertMergedCommitToNonDefaultBranch() throws TimeoutException, InterruptedException {
        RpcBranch branch = runMergeWithStrategy(
                "branch_that_has_same_text_file_modified_on_both_clean_merge_src",
                "306d584aa574fa74901d3e288b2931b2c4f0e927",
                "branch_that_has_same_text_file_modified_on_both_clean_merge_trgt",
                "7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2", RpcMergeStrategy.MERGE_STRATEGY_SQUASH);
        String commit = branch.getLatestCommit();

        RpcRevertRequest.Builder builder = RpcRevertRequest.newBuilder()
                .setRepository(merge.getId())
                .setCommitter(committer)
                .setCommitId(commit)
                .setBranch(branch)
                .setTargetBranch("revert-branch");
        RpcBranch revertBranch = revert(builder).getBranch();

        assertNotNull(revertBranch);
        assertEquals("refs/heads/revert-branch", revertBranch.getId().toStringUtf8());

        Branch expectedRevertBranch = agent.resolveBranch(merge, "revert-branch");
        assertNotNull(expectedRevertBranch);
        assertEquals(expectedRevertBranch.toBranch(), revertBranch);

        Commit revertCommit = commitManager.getCommit(new GetCommitParameters.Builder(merge,
                revertBranch.getLatestCommit()).build());

        assertNotNull(revertCommit);
        assertEquals(committer, revertCommit.getAuthor().toPerson());
        assertEquals(committer, revertCommit.getCommitter().toPerson());
        assertThat(revertCommit.getParents(), contains(commit));

        String diff = processBuilder(merge).diff()
                .ancestor(commit)
                .rev(revertCommit.getId())
                .build(new StringStdioHandler(UTF_8))
                .run();

        assertEquals("diff --git a/file.txt b/file.txt\n" +
                "index ff30e87..5446783 100644\n" +
                "--- a/file.txt\n" +
                "+++ b/file.txt\n" +
                "@@ -1,3 +1,3 @@\n" +
                "-Line one modified\n" +
                "+Line one\n" +
                " Line two\n" +
                " Line three modified", diff);
    }

    @DirtiesRepo("repository")
    @Test
    public void testRevertWhenTargetBranchAlreadyPresent() {
        Branch branch = agent.resolveHead(repository);
        RpcRevertRequest.Builder builder = RpcRevertRequest.newBuilder()
                .setRepository(repository.getId())
                .setCommitter(committer)
                .setCommitId("57150c54c38d6570b2fd5e6d6cfc19476de44e84")
                .setBranch(branch.toBranch())
                .setTargetBranch("branch_mod_merge");

        StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, () -> revert(builder));
        assertThat(exception.getMessage(),
                containsString("INVALID_ARGUMENT: [refs/heads/branch_mod_merge] is not a valid branch name"));
    }

    @DirtiesRepo("repository")
    @Test
    public void testRevertWhenTargetBranchSameAsBranch() throws InterruptedException, TimeoutException {
        Branch branch = agent.resolveHead(repository);
        RpcRevertRequest.Builder builder = RpcRevertRequest.newBuilder()
                .setRepository(repository.getId())
                .setCommitter(committer)
                .setCommitId("57150c54c38d6570b2fd5e6d6cfc19476de44e84")
                .setBranch(branch.toBranch())
                .setTargetBranch(HEADS.unqualify(branch.getId()));

        RpcBranch revertBranch = revert(builder).getBranch();

        assertNotNull(revertBranch);
        assertEquals(branch.getId(), revertBranch.getId().toStringUtf8());

        Commit revertCommit = commitManager.getCommit(
                new GetCommitParameters.Builder(repository, revertBranch.getLatestCommit()).build());

        assertNotNull(revertCommit);
        assertEquals(committer, revertCommit.getAuthor().toPerson());
        assertEquals(committer, revertCommit.getCommitter().toPerson());
        assertThat(revertCommit.getParents(), contains(branch.getLatestCommit()));

        String diff = processBuilder(repository).diff()
                .ancestor(branch.getLatestCommit())
                .rev(revertCommit.getId())
                .build(new StringStdioHandler(UTF_8))
                .run();
        assertEquals("diff --git a/mv_file/moved_file.txt b/mv_file/moved_file.txt\n" +
                "deleted file mode 100644\n" +
                "index f49abb7..0000000\n" +
                "--- a/mv_file/moved_file.txt\n" +
                "+++ /dev/null\n" +
                "@@ -1 +0,0 @@\n" +
                "-This file will be moved\n" +
                "diff --git a/mv_file/mv_file.txt b/mv_file/mv_file.txt\n" +
                "new file mode 100644\n" +
                "index 0000000..f49abb7\n" +
                "--- /dev/null\n" +
                "+++ b/mv_file/mv_file.txt\n" +
                "@@ -0,0 +1 @@\n" +
                "+This file will be moved", diff);
    }

    @DirtiesRepo("repository")
    @Test
    public void testRevertWithConflicts() {
        Branch branch = agent.resolveHead(repository);
        RpcRevertRequest.Builder builder = RpcRevertRequest.newBuilder()
                .setRepository(repository.getId())
                .setCommitter(committer)
                .setCommitId("9c05f43f859375e392d90d23a13717c16d0fdcda")
                .setBranch(branch.toBranch())
                .setTargetBranch("revert-branch");

        StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, () -> revert(builder));
        assertThat(exception.getMessage(),
                containsString("The revert could not be completed due to file conflicts"));
    }

    @DirtiesRepo("merge")
    @Test
    public void testRevertFromABranchThatDoesNotHaveTheCommit() {
        Branch branch = agent.resolveBranch(merge, "master");
        Branch commitBranch = agent.resolveBranch(merge, "branch_that_has_same_text_file_modified_on_both_clean_merge_src");
        RpcRevertRequest.Builder builder = RpcRevertRequest.newBuilder()
                .setRepository(merge.getId())
                .setCommitter(committer)
                .setCommitId(commitBranch.getLatestCommit())
                .setBranch(branch.toBranch())
                .setTargetBranch("revert-branch");

        StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, () -> revert(builder));
        assertThat(exception.getMessage(),
                containsString("The revert could not be completed due to file conflicts"));
    }

    @DirtiesRepo("repository")
    @Test
    public void testRevertNonExistingCommit() {
        Branch branch = agent.resolveBranch(repository, "master");
        RpcRevertRequest.Builder builder = RpcRevertRequest.newBuilder()
                .setRepository(repository.getId())
                .setCommitter(committer)
                .setCommitId("dsasdfgtdsasdfgtdsasdfgtdsasdfgt")
                .setBranch(branch.toBranch())
                .setTargetBranch("revert-branch");

        StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, () -> revert(builder));
        assertThat(exception.getMessage(), containsString("No such commit: dsasdfgtdsasdfgtdsasdfgtdsasdfgt"));
    }

    @DirtiesRepo("repository")
    @Test
    public void testRevertNonExistingBranch() {
        Branch branch = new Branch.Builder(agent.resolveBranch(repository, "master"))
                .id("refs/heads/non-existing-branch")
                .build();

        RpcRevertRequest.Builder builder = RpcRevertRequest.newBuilder()
                .setRepository(repository.getId())
                .setCommitter(committer)
                .setCommitId("57150c54c38d6570b2fd5e6d6cfc19476de44e84")
                .setBranch(branch.toBranch());

        StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, () -> revert(builder));
        assertThat(exception.getMessage(), containsString("NOT_FOUND: No such ref: refs/heads/non-existing-branch"));
    }

    @DirtiesRepo("merge")
    @Test
    public void testRevertWithInvalidParentNumber() throws InterruptedException, TimeoutException {
        RpcBranch branch = runMergeWithStrategy(
                "branch_that_has_same_text_file_modified_on_both_clean_merge_src",
                "306d584aa574fa74901d3e288b2931b2c4f0e927", "master",
                "017caf31eca7c46eb3d1800fcac431cfa7147a01", MERGE_STRATEGY_NO_FF);
        String commit = branch.getLatestCommit();

        RpcRevertRequest.Builder builder = RpcRevertRequest.newBuilder()
                .setRepository(merge.getId())
                .setCommitter(committer)
                .setCommitId(commit)
                .setBranch(branch)
                .setMergedCommitParentNumber(3)
                .setTargetBranch("revert-branch");

        StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, () -> revert(builder));
        assertThat(exception.getMessage(),
                containsString("The revert could not be completed as the commit " + commit + " does not have parent 3"));
    }

    private RpcMergeResponse merge(RpcMergeRequest.Builder builder)
            throws TimeoutException, InterruptedException {
        MockResponseObserver<RpcMergeResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcMergeFragment> observer = stub.merge(responseObserver);

        observer.onNext(RpcMergeFragment.newBuilder()
                .setMergeRequest(builder.build())
                .build());

        observer.onNext(RpcMergeFragment.newBuilder()
                .setUpdateRef(RpcUpdateRef.getDefaultInstance())
                .build());

        observer.onCompleted();
        responseObserver.await();

        List<RpcMergeResponseFragment> fragments = responseObserver.getFragments();
        //Fragment 0 is the merge hooks fragment, fragment 1 is the merge result
        assertNotNull(fragments);
        assertEquals(2, fragments.size());
        RpcMergeResponseFragment fragment = fragments.get(1);
        assertTrue(fragment.hasMergeResponse());
        return fragment.getMergeResponse();
    }

    private GitProcessBuilder processBuilder(Repository repository) {
        return builderFactory.builder(repository);
    }

    private RpcRevertResponse revert(RpcRevertRequest.Builder builder)
            throws TimeoutException, InterruptedException {
        MockResponseObserver<RpcRevertResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcRevertFragment> observer = stub.revert(responseObserver);

        observer.onNext(RpcRevertFragment.newBuilder()
                .setRevertRequest(builder.build())
                .build());
        observer.onNext(RpcRevertFragment.newBuilder()
                .setUpdateRef(RpcUpdateRef.getDefaultInstance())
                .build());
        observer.onCompleted();

        responseObserver.await();

        List<RpcRevertResponseFragment> fragments = responseObserver.getFragments();
        assertNotNull(fragments);
        assertEquals(2, fragments.size());
        RpcRevertResponseFragment fragment = fragments.get(1);
        assertTrue(fragment.hasRevertResponse());
        return fragment.getRevertResponse();
    }

    private RpcBranch runMergeWithStrategy(String fromBranch, String fromHash, String toBranch, String toHash,
                                           RpcMergeStrategy mergeStrategy)
            throws TimeoutException, InterruptedException {
        RpcMergeRequest.Builder builder = RpcMergeRequest.newBuilder()
                .setRepository(merge.getId())
                .setAuthor(author)
                .setCommitter(committer)
                .setFromBranch(ByteString.copyFromUtf8(fromBranch))
                .setFromCommitId(fromHash)
                .setMessage(ByteString.copyFromUtf8(SUCCESS_MESSAGE))
                .setStrategy(mergeStrategy)
                .setToBranch(ByteString.copyFromUtf8(toBranch));
        RpcBranch target = merge(builder).getBranch();
        assertNotNull(target);
        assertNotEquals(toHash, target.getLatestCommit());

        return target;
    }
}
