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

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.event.EventPublisher;
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.fetch.GitFetchBuilder;
import com.atlassian.bitbucket.mesh.process.OsProcess;
import com.atlassian.bitbucket.mesh.process.ProcessCompletionHandler;
import com.atlassian.bitbucket.mesh.process.ProcessStdioHandler;
import com.atlassian.bitbucket.mesh.process.ProcessSummary;
import com.atlassian.bitbucket.mesh.repository.RefsUpdatedEvent;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcGitTimeouts;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.junit.MockitoJUnitRunner;

import java.nio.ByteBuffer;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.*;
import static org.mockito.AdditionalAnswers.returnsFirstArg;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultFetchManagerTest {

    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
    private EventPublisher eventPublisher;
    @Mock(answer = Answers.RETURNS_SELF)
    private GitFetchBuilder fetchBuilder;
    @Mock
    private FetchCallback fetchCallback;
    private int fetchExitCode;
    @InjectMocks
    private DefaultFetchManager fetchManager;
    private ProcessStdioHandler<Void> handler;
    @Mock
    private GitProcess<Void> process;
    @Captor
    private ArgumentCaptor<ProcessSummary> summaryCaptor;
    @Mock
    private Repository repository;
    private String stdout;

    @Before
    public void setUp() {
        when(authenticationHelper.configure(any(GitFetchBuilder.class), anyString(), any()))
                .thenAnswer(returnsFirstArg());
        when(builderFactory.builder(repository).fetch()).thenReturn(fetchBuilder);
        when(fetchBuilder.<Void>build(any())).thenAnswer(invocation -> {
            handler = invocation.getArgument(0);
            return process;
        });
        fetchExitCode = 0;
        //Mock a single line of refs updated
        stdout = "   35fc57af718..bcca01a5446  release/6.10" + StringUtils.repeat(' ', 74) + "-> origin/release/6.10\n";
        when(process.run()).thenAnswer(invocation -> {
            OsProcess osProcess = mock(OsProcess.class);
            handler.onPreStart(osProcess);
            handler.onStart(osProcess);
            if (stdout != null) {
                handler.onStdout(ByteBuffer.wrap(stdout.getBytes(UTF_8)), true);
            }
            handler.onExit(fetchExitCode);
            if (handler instanceof ProcessCompletionHandler) {
                ((ProcessCompletionHandler) handler).onComplete(new ProcessSummary.Builder("cmd line")
                        .exitCode(fetchExitCode)
                        .build());
            }
            return null;
        });
    }

    @Test
    public void testAuthenticationHelperInvoked() {
        Credentials credentials = new UsernamePasswordCredentials("user", "password");
        fetchManager.fetch(new FetchParameters.Builder(repository, TIMEOUTS)
                        .credentials(credentials)
                        .refspecs("refs/heads/*:refs/heads/*")
                        .sourceUrl("../1234")
                        .build(),
                fetchCallback);

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

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

        fetchManager.fetch(new FetchParameters.Builder(repository, TIMEOUTS)
                .refspecs("refs/heads/*:refs/heads/*")
                .sourceUrl("../1234")
                .build(),
                fetchCallback);

        InOrder inOrder = inOrder(fetchCallback);
        inOrder.verify(fetchCallback).onStart();
        inOrder.verify(fetchCallback).onEnd(summaryCaptor.capture());
        assertThat(summaryCaptor.getValue().getThrown(), instanceOf(NullPointerException.class));
        verify(eventPublisher, never()).publish(any(RefsUpdatedEvent.class));
    }

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

        try {
            fetchManager.fetch(new FetchParameters.Builder(repository, TIMEOUTS)
                            .refspecs("refs/heads/*:refs/heads/*")
                            .sourceUrl("../1234")
                            .build(),
                    fetchCallback);
            fail("Exception expected");
        } catch (IllegalStateException e) {
            // expected
        }

        InOrder inOrder = inOrder(fetchCallback);
        inOrder.verify(fetchCallback).onStart();
        inOrder.verify(fetchCallback).onRefUpdate(any());
        inOrder.verify(fetchCallback).onEnd(summaryCaptor.capture());
        assertNull(summaryCaptor.getValue().getThrown());
        verify(eventPublisher).publish(any(RefsUpdatedEvent.class));
    }

    @Test
    public void testPublishesFetchFailedEventWhenFailed() {
        //Mock no output with a non-zero exit code
        fetchExitCode = 128;
        stdout = null;

        fetchManager.fetch(new FetchParameters.Builder(repository, TIMEOUTS)
                        .refspecs("refs/heads/*:refs/heads/*")
                        .sourceUrl("../1234")
                        .build(),
                fetchCallback);

        verify(eventPublisher).publish(any(FetchFailedEvent.class));
    }

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

        FetchParameters.Builder builder = new FetchParameters.Builder(repository, TIMEOUTS).sourceUrl("../1234");
        for (int i = 100; i < 200; ++i) {
            builder.refspecs("refs/heads/b-" + i + ":refs/heads/b-" + i); // 33 characters per refspec
        }
        FetchParameters parameters = builder.build();
        fetchManager.fetch(parameters, fetchCallback);

        //Verify that multiple fetch processes have been created
        int refspecsPerFetch = 250 / 33;
        int expectedProcesses = 1 + (parameters.getRefspecs().size() - 1) / refspecsPerFetch;
        InOrder inOrder = inOrder(fetchCallback, process);
        inOrder.verify(fetchCallback).onStart();
        inOrder.verify(process, times(expectedProcesses)).run();
        inOrder.verify(fetchCallback).onEnd(summaryCaptor.capture());
        //Only one event should be published
        verify(eventPublisher).publish(any(RefsUpdatedEvent.class));
    }
}