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

import com.atlassian.bitbucket.mesh.AbstractGrpcTest;
import com.atlassian.bitbucket.mesh.git.GitProcessCompletionHandler;
import com.atlassian.bitbucket.mesh.git.GitRepositories;
import com.atlassian.bitbucket.mesh.git.builder.difftree.CallbackChangeDiffTreeStdioHandler;
import com.atlassian.bitbucket.mesh.git.builder.difftree.CallbackChangesetDiffTreeStdioHandler;
import com.atlassian.bitbucket.mesh.git.builder.revlist.CommitPump;
import com.atlassian.bitbucket.mesh.git.builder.revlist.CommitPumpSettings;
import com.atlassian.bitbucket.mesh.git.commit.Commit;
import com.atlassian.bitbucket.mesh.git.content.Change;
import com.atlassian.bitbucket.mesh.git.content.ChangeCallback;
import com.atlassian.bitbucket.mesh.git.content.Changeset;
import com.atlassian.bitbucket.mesh.git.exception.CommitNotFoundException;
import com.atlassian.bitbucket.mesh.grpc.BidirectionalFragmentResponseObserver;
import com.atlassian.bitbucket.mesh.grpc.FragmentResponseObserver;
import com.atlassian.bitbucket.mesh.grpc.RequestFragmentCreator;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.*;
import com.atlassian.bitbucket.mesh.test.annotation.Repo;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.ByteString;
import io.grpc.ManagedChannel;
import io.grpc.Status;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;

import javax.annotation.Nonnull;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

import static java.util.Objects.requireNonNull;
import static org.mockito.Mockito.*;

//Parallels DiffTreeCommandTest in the Git SCM
public class DiffTreeRequestTest extends AbstractGrpcTest {

    //protobuf doesn't have the concept of null/unset, so this provides some reasonable defaults that
    //can then be customized as necessary inside various tests, rather than repeating them everywhere
    private static final RpcDiffOptions DEFAULT_OPTIONS = RpcDiffOptions.newBuilder()
            .setRenames(RpcDiffRenames.RENAMES_OFF)
            .build();

    @Repo(GitRepositories.DIFF_TREE)
    private Repository diffTree;
    @Resource
    private ManagedChannel managedChannel;
    @Repo
    private Repository repository;
    private PlumbingServiceGrpc.PlumbingServiceStub stub;

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

    @Test
    public void testChangesetsForEmptyDiffToFirstParent() {
        RpcDiffTreeRequest request = createDiffTreeRequest(diffTree);

        List<Changeset> changesets = diffTreeStdin(request, ImmutableList.of("62abb7aa35f8928baa23be2e6cf5cbf1ca43dfdb"));

        assertNotNull(changesets);
        assertEquals(1, changesets.size());

        Changeset changeset = changesets.get(0);
        assertNotNull(changeset);
        assertEquals("f17a3325dfb2701a1f2c261b3b90252041af7537", changeset.getFromCommitId());

        Commit toCommit = changeset.getToCommit();
        assertEquals("62abb7aa35f8928baa23be2e6cf5cbf1ca43dfdb", toCommit.getId());
        assertEquals("Bryan Turner", toCommit.getAuthor().getName());
        assertEquals(2, toCommit.getParents().size());

        List<Change> changes = changeset.getChanges();
        assertNotNull(changes);
        assertEquals(0, changes.size()); //Diff to the second parent is not empty, diff to the first is
    }

    @Test
    public void testChangesetsForMultipleDiffTrees() {
        RpcDiffTreeRequest request = createDiffTreeRequest(repository);

        List<Changeset> changesets =
                diffTreeStdin(request, ImmutableList.of("57150c54c38d6570b2fd5e6d6cfc19476de44e84",
                        "6053a1eaa1c009dd11092d09a72f3c41af1b59ad"));

        assertNotNull(changesets);
        assertEquals(2, changesets.size());

        Changeset changeset = changesets.get(0);
        assertNotNull(changeset);
        assertEquals("9fbc34a0d905950131d73f352abe68520c6db2a3", changeset.getFromCommitId());

        Commit toCommit = changeset.getToCommit();
        assertEquals("57150c54c38d6570b2fd5e6d6cfc19476de44e84", toCommit.getId());
        assertEquals("Michael Heemskerk", toCommit.getAuthor().getName());
    }

    @Test
    public void testChangesetsCommitNotFound() {
        RpcDiffTreeRequest request = createDiffTreeRequest(repository);

        CommitNotFoundException thrown = assertThrows(CommitNotFoundException.class,
                () -> diffTreeStdin(request, ImmutableList.of("62abb7aa35f8928baa23be2e6cf5cbf1ca43dfdb")));
        assertSame(Status.Code.NOT_FOUND, thrown.getStatus().getCode());
    }

    @Test
    public void testChangesetsNoSuchCommitIgnoreMissing() {
        RpcDiffTreeRequest request = createDiffTreeRequest(repository);

        List<Changeset> changesets =
                diffTreeStdin(request, ImmutableList.of("62abb7aa35f8928baa23be2e6cf5cbf1ca43dfdb"), true);

        assertEquals(0, changesets.size());
    }

    @Test
    public void testForEmptyDiffToFirstParentWithCallback() {
        ChangeCallback callback = mock(ChangeCallback.class);
        CallbackChangeDiffTreeStdioHandler handler = new CallbackChangeDiffTreeStdioHandler(callback, 5);

        RpcDiffTreeRequest request = createBuilder("62abb7aa35f8928baa23be2e6cf5cbf1ca43dfdb")
                .setRepository(diffTree.getId())
                .build();

        diffTree(request, handler);

        InOrder ordered = inOrder(callback);
        ordered.verify(callback, never()).onChange(any());
        ordered.verifyNoMoreInteractions();
    }

    @Test
    public void testCommitNotFoundForChangesWithCallback() {
        ChangeCallback callback = mock(ChangeCallback.class);
        CallbackChangeDiffTreeStdioHandler handler = new CallbackChangeDiffTreeStdioHandler(callback, 100);

        RpcDiffTreeRequest request =
                createBuilder("57150c54c38d6570b2fd5e6d6cfc19476de44e83", "9fbc34a0d905950131d73f352abe68520c6db2a3")
                        .setRepository(diffTree.getId())
                        .build();

        CommitNotFoundException thrown =
                assertThrows(CommitNotFoundException.class, () -> diffTree(request, handler));
        assertSame(Status.Code.NOT_FOUND, thrown.getStatus().getCode());
        //If the command fails (and throwing CommitNotFound should fail), ensure the callback is never used
        verifyNoInteractions(callback);
    }

    @Test
    public void testPageRequestStartsBeyondCommitIds() {
        List<Change> changes = new ArrayList<>();
        CallbackChangeDiffTreeStdioHandler handler = new CallbackChangeDiffTreeStdioHandler(changes::add, -1);

        RpcDiffTreeRequest request =
                createBuilder("57150c54c38d6570b2fd5e6d6cfc19476de44e84")
                        .setRepository(repository.getId())
                        .build();

        diffTree(request, handler);

        assertNotNull(changes);
        assertEquals(0, changes.size());
    }

    private RpcDiffTreeRequest.Builder createBuilder() {
        return RpcDiffTreeRequest.newBuilder()
                .setDiffOptions(DEFAULT_OPTIONS.toBuilder());
    }

    private RpcDiffTreeRequest.Builder createBuilder(String rev) {
        return createBuilder(rev, null);
    }

    private RpcDiffTreeRequest.Builder createBuilder(String rev, String ancestor) {
        RpcDiffTreeRequest.Builder builder = createBuilder();
        if (StringUtils.isBlank(ancestor)) {
            builder.setAlways(true)
                    .setRoot(true);
        } else {
            builder.setAncestor(ByteString.copyFromUtf8(ancestor));
        }

        return builder.setRecursive(true)
                .setDiffOptions(DEFAULT_OPTIONS.toBuilder()
                        .setRenames(RpcDiffRenames.RENAMES_COPIES))
                .setRev(ByteString.copyFromUtf8(rev));
    }

    private RpcDiffTreeRequest createDiffTreeRequest(Repository repository) {
        CommitPumpSettings settings = new CommitPumpSettings.Builder().build();

        return RpcDiffTreeRequest.newBuilder()
                .setAlways(true)
                .setDiffOptions(DEFAULT_OPTIONS)
                .setFormat("commit %H%n" + CommitPump.getFormat(settings))
                .setRecursive(true)
                .setRepository(repository.getId())
                .setRoot(true)
                .build();
    }

    private void diffTree(RpcDiffTreeRequest request, CallbackChangeDiffTreeStdioHandler handler) {
        FragmentResponseObserver<Void> observer =
                new FragmentResponseObserver<>(handler, new GitProcessCompletionHandler());

        stub.diffTree(request, observer);
        observer.asResult();
    }

    private List<Changeset> diffTreeStdin(RpcDiffTreeRequest request, List<String> commitIds) {
        return diffTreeStdin(request, commitIds, false);
    }

    private List<Changeset> diffTreeStdin(RpcDiffTreeRequest request, List<String> commitIds, boolean ignoreMissing) {
        List<Changeset> changesets = new ArrayList<>();

        CallbackChangesetDiffTreeStdioHandler handler =
                new CallbackChangesetDiffTreeStdioHandler(commitIds, changesets::add, ignoreMissing, 5,
                        -1);

        BidirectionalFragmentResponseObserver<RpcDiffTreeStdinFragment, Void> observer =
                new BidirectionalFragmentResponseObserver<>(
                        handler,
                        new GitProcessCompletionHandler(),
                        new RequestFragmentCreator<RpcDiffTreeStdinFragment>() {
                            @Nonnull
                            @Override
                            public RpcDiffTreeStdinFragment forStdin(@Nonnull RpcStreamChunk stdin) {
                                requireNonNull(stdin, "stdin");

                                return RpcDiffTreeStdinFragment.newBuilder()
                                        .setStdin(stdin)
                                        .build();
                            }

                            @Nonnull
                            @Override
                            public RpcDiffTreeStdinFragment getInitial() {
                                return RpcDiffTreeStdinFragment.newBuilder()
                                        .setRequest(request)
                                        .build();
                            }
                        });

        stub.diffTreeStdin(observer);
        observer.asResult();

        return changesets;
    }
}
