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

import com.atlassian.bitbucket.mesh.config.ProcessConfig;
import com.atlassian.bitbucket.mesh.credentials.Credentials;
import com.atlassian.bitbucket.mesh.credentials.UsernamePasswordCredentials;
import com.atlassian.bitbucket.mesh.git.GitAgent;
import com.atlassian.bitbucket.mesh.git.auth.GitAuthenticationHelper;
import com.atlassian.bitbucket.mesh.git.builder.GitProcess;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.builder.push.GitPushBuilder;
import com.atlassian.bitbucket.mesh.process.ProcessStdioHandler;
import com.atlassian.bitbucket.mesh.process.ProcessSummary;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcGitTimeouts;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.junit.MockitoJUnitRunner;

import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import static org.mockito.AdditionalAnswers.returnsFirstArg;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultPushManagerTest {

    private static final RpcGitTimeouts TIMEOUTS = RpcGitTimeouts.newBuilder().setExecution(3600L).setIdle(120L).build();

    @Mock
    private GitAgent agent;
    @Mock
    private GitAuthenticationHelper authenticationHelper;
    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private GitProcessBuilderFactory builderFactory;
    @Mock
    private ProcessConfig config;
    @Mock(answer = Answers.RETURNS_SELF)
    private GitPushBuilder pushBuilder;
    @Mock
    private PushCallback pushCallback;
    @InjectMocks
    private DefaultPushManager pushManager;
    @Mock
    private GitProcess<Void> process;
    @Captor
    private ArgumentCaptor<ProcessSummary> summaryCaptor;
    @Mock
    private Repository repository;

    @Before
    public void setUp() {
        when(authenticationHelper.configure(any(GitPushBuilder.class), anyString(), any()))
                .thenAnswer(returnsFirstArg());
        when(builderFactory.builder(repository).push()).thenReturn(pushBuilder);
        when(pushBuilder.build((ProcessStdioHandler<Void>) any())).thenReturn(process);
    }

    @Test
    public void testAuthenticationHelperInvoked() {
        Credentials credentials = new UsernamePasswordCredentials("user", "password");
        pushManager.push(new PushParameters.Builder(repository, TIMEOUTS)
                        .credentials(credentials)
                        .refspecs("refs/heads/*:refs/heads/*")
                        .targetUrl("../1234")
                        .build(),
                pushCallback);

        verify(authenticationHelper).configure(pushBuilder, "../1234", credentials);
    }

    @Test
    public void testCallbackOnEndIsCalledIfOnStartThrows() {
        doThrow(new NullPointerException("mock NPE")).when(pushCallback).onStart();

        pushManager.push(new PushParameters.Builder(repository, TIMEOUTS)
                        .refspecs("refs/heads/*:refs/heads/*")
                        .targetUrl("../1234")
                        .build(),
                pushCallback);

        InOrder inOrder = inOrder(pushCallback);
        inOrder.verify(pushCallback).onStart();
        inOrder.verify(pushCallback).onEnd(summaryCaptor.capture());
        assertThat(summaryCaptor.getValue().getThrown(), instanceOf(NullPointerException.class));
    }

    @Test
    public void testCallbackOnEndIsCalledOnceIfOnEndThrows() {
        doThrow(new IllegalStateException("mock exception")).when(pushCallback).onEnd(any());

        try {
            pushManager.push(new PushParameters.Builder(repository, TIMEOUTS)
                            .refspecs("refs/heads/*:refs/heads/*")
                            .targetUrl("../1234")
                            .build(),
                    pushCallback);
            fail("Exception expected");
        } catch (IllegalStateException e) {
            // expected
        }

        InOrder inOrder = inOrder(pushCallback);
        inOrder.verify(pushCallback).onStart();
        inOrder.verify(pushCallback).onEnd(summaryCaptor.capture());
        assertNull(summaryCaptor.getValue().getThrown());
    }

    @Test
    public void testRunsMultiplePushesIfCommandLineTooLong() {
        when(config.getCommandLineSize()).thenReturn(500);

        PushParameters.Builder builder = new PushParameters.Builder(repository, TIMEOUTS).targetUrl("../1234");
        for (int i = 100; i < 200; ++i) {
            builder.refspecs("refs/heads/b-" + i + ":refs/heads/b-" + i); // 33 characters per refspec
        }
        PushParameters parameters = builder.build();
        pushManager.push(parameters, pushCallback);

        //Verify that multiple push processes have been created
        int refspecsPerPush = 250 / 33;
        int expectedProcesses = 1 + (parameters.getRefspecs().size() - 1) / refspecsPerPush;
        InOrder inOrder = inOrder(pushCallback, process);
        inOrder.verify(pushCallback).onStart();
        inOrder.verify(process, times(expectedProcesses)).run();
        inOrder.verify(pushCallback).onEnd(summaryCaptor.capture());
    }
}