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

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.binary.GitBinary;
import com.atlassian.bitbucket.mesh.git.ref.Branch;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.*;
import com.atlassian.bitbucket.mesh.test.HasGitError;
import com.atlassian.bitbucket.mesh.test.annotation.DirtiesRepo;
import com.atlassian.bitbucket.mesh.test.annotation.Repo;
import com.atlassian.bitbucket.mesh.util.Version;
import com.google.protobuf.ByteString;
import io.grpc.ManagedChannel;
import io.grpc.Status;
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;

public class RebaseRequestTest extends AbstractGrpcTest {

    @Resource
    private GitAgent agent;
    @Resource
    private GitBinary binary;
    @Resource
    private ManagedChannel managedChannel;
    @Repo(GitRepositories.MERGE)
    private Repository merge;
    @Repo
    private Repository repository;
    private CommitServiceGrpc.CommitServiceStub stub;
    private Version version;

    @Before
    public void setUp() {
        stub = CommitServiceGrpc.newStub(managedChannel);
        version = binary.getVersion();
    }

    @DirtiesRepo("merge")
    @Test
    public void testRebase() throws Exception {
        RpcRebaseRequest.Builder builder =
                createBuilder("branch_that_has_same_text_file_modified_on_both_clean_merge_src")
                        .setRepository(merge.getId())
                        .setUpstream(ByteString.copyFromUtf8("7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2"));

        RpcBranch branch = rebase(builder).getBranch();

        assertNotNull(branch);
        assertNotEquals("306d584aa574fa74901d3e288b2931b2c4f0e927", branch.getLatestCommit());
    }

    @Test
    public void testRebaseAllowsDiscardingAllCommitsByDefault() throws Exception {
        RpcRebaseRequest.Builder builder = createBuilder("branch_that_differ_by_empty_commit_src")
                .setRepository(merge.getId())
                .setUpstream(ByteString.copyFromUtf8("298924b8c403240eaf89dcda0cce7271620ab2f6"));

        RpcBranch branch = rebase(builder).getBranch();

        assertNotNull(branch);
        assertEquals("298924b8c403240eaf89dcda0cce7271620ab2f6", branch.getLatestCommit());
    }

    @DirtiesRepo("merge")
    @Test
    public void testRebaseCrossRepository() throws Exception {
        RpcRebaseRequest.Builder builder = createBuilder("refs/heads/master")
                .setRepository(merge.getId())
                .setUpstream(ByteString.copyFromUtf8("0a943a29376f2336b78312d99e65da17048951db"))
                .setUpstreamRepository(repository.getId());

        RpcBranch branch = rebase(builder).getBranch();

        assertNotNull(branch);
        assertNotEquals("e15bebda897605b1b8ab324e3c8339e4d1814c59", branch.getLatestCommit());
    }

    @Test
    public void testRebaseWhenCommitterHasNoEmail() {
        RpcRebaseRequest.Builder builder = createBuilder("branch_that_differ_by_empty_commit_src")
                .setCommitter(RpcPerson.newBuilder()
                        .setName(ByteString.copyFromUtf8("bob"))
                        .build())
                .setRepository(merge.getId());

        StatusRuntimeException thrown = assertRebaseThrows(builder);

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidRebase(RpcInvalidRebase.Reason.COMMITTER_EMAIL_BLANK));
    }

    @Test
    public void testRebaseWhenCommitterHasNoName() {
        RpcRebaseRequest.Builder builder = createBuilder("branch_that_differ_by_empty_commit_src")
                .setCommitter(RpcPerson.getDefaultInstance())
                .setRepository(merge.getId());

        StatusRuntimeException thrown = assertRebaseThrows(builder);

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidRebase(RpcInvalidRebase.Reason.COMMITTER_NAME_BLANK));
    }

    @Test
    public void testRebaseWhenBranchIsInvalid() {
        RpcRebaseRequest.Builder builder = createBuilder("branch-does-not-exist")
                .setRepository(merge.getId())
                .setUpstream(ByteString.copyFromUtf8("c1654113b22f65f988f5a570d8cf32c7288ee7d2"));

        StatusRuntimeException thrown = assertRebaseThrows(builder);

        assertSame(Status.Code.NOT_FOUND, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.noSuchObject("branch-does-not-exist"));
    }

    @Test
    public void testRebaseWhenUpstreamIsInvalid() {
        RpcRebaseRequest.Builder builder = createBuilder("branch_that_has_same_text_file_modified_src")
                .setRepository(merge.getId())
                .setUpstream(ByteString.copyFromUtf8("abc100"));

        StatusRuntimeException thrown = assertRebaseThrows(builder);

        assertSame(Status.Code.INVALID_ARGUMENT, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.invalidRefName(RpcInvalidRefName.Reason.INVALID, "abc100"));
    }

    @Test
    public void testRebaseThrowsOnConflicts() {
        RpcRebaseRequest.Builder builder = createBuilder("branch_that_has_same_text_file_modified_src")
                .setRepository(merge.getId())
                .setUpstream(ByteString.copyFromUtf8("c1654113b22f65f988f5a570d8cf32c7288ee7d2"));

        StatusRuntimeException thrown = assertRebaseThrows(builder);

        assertSame(Status.Code.FAILED_PRECONDITION, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.rebaseConflicted("5bb214e6dd15643dabfe4b6b0739becfb1e59d9f"));
    }

    @Test
    public void testRebaseThrowsIfAllCommitsAreDiscardedIfRequested() {
        RpcRebaseRequest.Builder builder = createBuilder("branch_that_differ_by_empty_commit_src")
                .setAllowEmpty(false)
                .setRepository(merge.getId())
                .setUpstream(ByteString.copyFromUtf8("298924b8c403240eaf89dcda0cce7271620ab2f6"));

        StatusRuntimeException thrown = assertRebaseThrows(builder);

        assertSame(Status.Code.FAILED_PRECONDITION, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.emptyRebase());
    }

    @DirtiesRepo("merge")
    @Test
    public void testRebaseThrowsOnEmptyCommit() throws Exception {
        //Git 2.25 and earlier use the "apply" (formerly "am") backend for rebase. 2.26+ default to the "merge"
        //backend instead, which changes the behavior
        boolean isApplyBased = version.compareTo(new Version(2, 26)) < 0;

        //The two branches used must not be fast-forward from each other, or the rebase is skipped. These two
        //branches are diverged, each with one commit the other doesn't have, with "empty_commit_trgt" having
        //a single empty commit that's not present on "one_file_src" (7549846524f)
        RpcRebaseRequest.Builder builder = createBuilder("branch_that_differ_by_empty_commit_trgt")
                .setAllowEmpty(false)
                .setRepository(merge.getId())
                .setUpstream(ByteString.copyFromUtf8("7549846524f8aed2bd1c0249993ae1bf9d3c9998"));

        if (isApplyBased) {
            StatusRuntimeException thrown = assertRebaseThrows(builder);

            assertSame(Status.Code.FAILED_PRECONDITION, thrown.getStatus().getCode());
            assertThat(thrown, HasGitError.emptyRebase());

            //For 2.25 and prior, the branch should not be updated because the rebase should fail
            Branch branch = agent.resolveBranch(merge, "branch_that_differ_by_empty_commit_trgt");
            assertNotNull("The test branch could not be resolved", branch);
            assertEquals("298924b8c403240eaf89dcda0cce7271620ab2f6", branch.getLatestCommit());
        } else {
            RpcBranch branch = rebase(builder).getBranch();

            //For 2.26 and later, the rebase should be successful and the branch should be updated
            //with a rebased version of the empty commit
            assertNotNull(branch);
        }
    }

    @Test
    public void testRebaseThrowsOnOutdatedBranch() {
        RpcRebaseRequest.Builder builder = createBuilder("refs/heads/branch_that_differ_by_empty_commit_src")
                .setCommitId("e527f8731b48bca46d7e99e270d05a9a3fa91107")
                .setRepository(merge.getId())
                .setUpstream(ByteString.copyFromUtf8("017caf31eca7c46eb3d1800fcac431cfa7147a01"));

        StatusRuntimeException thrown = assertRebaseThrows(builder);

        assertSame(Status.Code.ABORTED, thrown.getStatus().getCode());
        assertThat(thrown, HasGitError.nonFastForwardUpdate("refs/heads/branch_that_differ_by_empty_commit_src"));
    }

    @DirtiesRepo("merge") // only ref update is prevented, the rebase commit is still fetched to the 'merge' repo
    @Test
    public void testRebaseWithoutUpdateRef() throws Exception {
        MockResponseObserver<RpcRebaseResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcRebaseFragment> observer = stub.rebase(responseObserver);

        RpcRebaseRequest.Builder builder =
                createBuilder("branch_that_has_same_text_file_modified_on_both_clean_merge_src")
                        .setRepository(merge.getId())
                        .setUpstream(ByteString.copyFromUtf8("7fa77c6e34c6c53de0bb880fc2b0b7924a601cd2"));

        observer.onNext(RpcRebaseFragment.newBuilder()
                .setRebaseRequest(builder)
                .build());

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

        List<RpcRebaseResponseFragment> fragments = responseObserver.getFragments();
        assertEquals(1, fragments.size());

        Branch branch =
                agent.resolveBranch(merge, "branch_that_has_same_text_file_modified_on_both_clean_merge_src");
        assertNotNull("The test branch could not be resolved", branch);
        assertEquals("306d584aa574fa74901d3e288b2931b2c4f0e927", branch.getLatestCommit());
    }

    private StatusRuntimeException assertRebaseThrows(RpcRebaseRequest.Builder builder) {
        return assertThrows(StatusRuntimeException.class, () -> rebase(builder));
    }

    private RpcRebaseRequest.Builder createBuilder(String branch) {
        return RpcRebaseRequest.newBuilder()
                .setAllowEmpty(true)
                .setCommitter(RpcPerson.newBuilder()
                        .setEmail("janed@test.com")
                        .setName(ByteString.copyFromUtf8("Jane Doe"))
                        .build())
                .setBranch(ByteString.copyFromUtf8(branch));
    }

    private RpcRebaseResponse rebase(RpcRebaseRequest.Builder builder) throws Exception {
        MockResponseObserver<RpcRebaseResponseFragment> responseObserver = new MockResponseObserver<>();
        StreamObserver<RpcRebaseFragment> observer = stub.rebase(responseObserver);

        observer.onNext(RpcRebaseFragment.newBuilder()
                .setRebaseRequest(builder)
                .build());

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

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

        List<RpcRebaseResponseFragment> fragments = responseObserver.getFragments();
        //Fragment 0 is the rebase hooks fragment, fragment 1 is the rebase result
        assertNotNull(fragments);
        assertEquals(2, fragments.size());
        RpcRebaseResponseFragment fragment = fragments.get(1);
        assertTrue(fragment.hasRebaseResponse());
        return fragment.getRebaseResponse();
    }
}
