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

import com.atlassian.bitbucket.mesh.AbstractWiredTest;
import com.atlassian.bitbucket.mesh.config.ApplicationConfig;
import com.atlassian.bitbucket.mesh.config.ProcessConfig;
import com.atlassian.bitbucket.mesh.git.GitProcessCompletionHandler;
import com.atlassian.bitbucket.mesh.git.GitQuarantineRegistry;
import com.atlassian.bitbucket.mesh.git.GitRepositories;
import com.atlassian.bitbucket.mesh.git.binary.GitBinary;
import com.atlassian.bitbucket.mesh.git.builder.revlist.GitRevListBuilder;
import com.atlassian.bitbucket.mesh.layout.StorageLayout;
import com.atlassian.bitbucket.mesh.process.ProcessCreator;
import com.atlassian.bitbucket.mesh.process.ProcessParameters;
import com.atlassian.bitbucket.mesh.process.handler.LinesStdioHandler;
import com.atlassian.bitbucket.mesh.process.handler.StringStdioHandler;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.test.annotation.Repo;
import com.google.common.collect.ImmutableMap;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;

import javax.annotation.Resource;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static com.atlassian.bitbucket.mesh.git.GitConstants.*;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
import static org.mockito.Mockito.*;

public class DefaultGitProcessBuilderTest extends AbstractWiredTest {

    @Resource
    private GitBinary binary;
    @Resource
    private ApplicationConfig config;
    @Repo(GitRepositories.COMMENT_DRIFT)
    private Repository drift;
    @Repo(GitRepositories.MERGE)
    private Repository merge;
    private ProcessConfig processConfig;
    @Resource
    private ProcessCreator processCreator;
    @Repo(GitRepositories.PULL_REQUESTS)
    private Repository pullRequests;
    @Repo
    private Repository repository;
    @Resource
    private GitQuarantineRegistry quarantineRegistry;
    @Resource
    private StorageLayout storageLayout;

    @Before
    public void setup() {
        //Applying a spy to ProcessConfig makes it simple for the tests to adjust the environment variable
        //setting without needing to restore to reflection or other tricks
        processConfig = spy(config.getProcess());
    }

    /**
     * Verifies alternates passed via the builder are written to {@code info/alternates} if merging them with an
     * existing {@code GIT_ALTERNATE_OBJECT_DIRECTORIES} would exceed the maximum allowed variable length. This
     * test also verifies that {@code info/alternates} is written to {@code GIT_OBJECT_DIRECTORY} if it's already
     * present.
     */
    @Test
    public void testApplyAlternatesAddsToExistingObjectDir() throws IOException {
        String objectsDir = repository.getPath().resolve(PATH_OBJECTS).toString();
        doReturn(objectsDir.length() + 1).when(processConfig).getEnvironmentVariableSize();

        runRevListWithAlternates(ImmutableMap.of(
                ENV_ALTERNATE_OBJECT_DIRS, objectsDir,
                //We should not modify a real repository by writing alternates to it, so rather than using
                //objectsDir here we create a temp directory and use that
                ENV_OBJECT_DIR, Files.createTempDirectory(storageLayout.getTempDir(), "alts-via-env").toString()));
    }

    /**
     * Verifies alternates passed via the builder are merged with {@code GIT_ALTERNATE_OBJECT_DIRECTORIES} if the
     * environment variable is already present and the resulting variable is still within the maximum length.
     */
    @Test
    public void testApplyAlternatesMergesExistingEnvironmentVariable() {
        String objectsDir = repository.getPath().resolve(PATH_OBJECTS).toString();

        runRevListWithAlternates(ImmutableMap.of(
                ENV_ALTERNATE_OBJECT_DIRS, objectsDir,
                ENV_OBJECT_DIR, objectsDir));
    }

    /**
     * Verifies passing alternates via an environment variable works.
     */
    @Test
    public void testApplyAlternatesUsingEnvironmentVariable() {
        runRevListWithAlternates(Collections.emptyMap());
    }

    /**
     * Verifies a temporary {@code GIT_OBJECT_DIRECTORY} is used to set alternates when the resulting environment
     * variable is too long.
     */
    @Test
    public void testApplyAlternatesUsingObjectDir() {
        doReturn(1).when(processConfig).getEnvironmentVariableSize();

        runRevListWithAlternates(ImmutableMap.of());
    }

    @Test
    public void testExecutionTimeoutDefaultsToRpcDeadline() {
        ProcessParameters<?> parameters = createProcessWithRpcDeadline(Duration.ofHours(1L), null);

        assertNotNull(parameters.getExecutionInterval());
        assertThat(parameters.getExecutionInterval().get(ChronoUnit.SECONDS),
                both(greaterThanOrEqualTo(3599L)).and(lessThanOrEqualTo(3600L)));
    }

    @Test
    public void testExecutionTimeoutRestrictedByRpcDeadlineWhenExecutionTimeoutIsLonger() {
        //If execution timeout > RPC deadline, the RPC deadline should be used instead
        ProcessParameters<?> parameters = createProcessWithRpcDeadline(Duration.ofHours(1L), Duration.ofHours(6L));

        assertNotNull(parameters.getExecutionInterval());
        assertThat(parameters.getExecutionInterval().get(ChronoUnit.SECONDS),
                both(greaterThanOrEqualTo(3599L)).and(lessThanOrEqualTo(3600L)));
    }

    @Test
    public void testExecutionTimeoutNotRestrictedByRpcDeadlineWhenExecutionTimeoutIsShorter() {
        //If execution timeout < RPC deadline, the configured execution timeout should be used unchanged
        ProcessParameters<?> parameters = createProcessWithRpcDeadline(Duration.ofHours(1L), Duration.ofMinutes(1L));

        assertNotNull(parameters.getExecutionInterval());
        assertThat(parameters.getExecutionInterval().get(ChronoUnit.SECONDS),
                both(greaterThanOrEqualTo(59L)).and(lessThanOrEqualTo(60L)));
    }

    private DefaultGitProcessBuilder builder(Repository repository) {
        return new DefaultGitProcessBuilder(binary, processConfig, processCreator,
                quarantineRegistry, storageLayout, repository);
    }

    private ProcessParameters<?> createProcessWithRpcDeadline(Duration rpcTimeout, Duration executionTimeout) {
        ProcessCreator processCreator = mock(ProcessCreator.class);
        ArgumentCaptor<ProcessParameters<String>> captor = ArgumentCaptor.forClass(ProcessParameters.class);
        when(processCreator.run(captor.capture())).thenReturn("version");

        DefaultGitProcessBuilder builder = new DefaultGitProcessBuilder(binary, processConfig, processCreator,
                quarantineRegistry, storageLayout, repository);
        if (rpcTimeout != null) {
            builder.withDeadline(Instant.now().plus(rpcTimeout));
        }
        if (executionTimeout != null) {
            builder.withExecutionInterval(executionTimeout);
        }
        builder.version()
                .build(new StringStdioHandler())
                .run();

        return captor.getValue();
    }

    private void runRevListWithAlternates(Map<String, String> environment) {
        GitRevListBuilder revListBuilder = builder(repository).revList()
                .alternates(drift, merge)
                .alternate(pullRequests.getPath().resolve(PATH_OBJECTS))
                .completionHandler(new GitProcessCompletionHandler())
                .ignoreMissing(true)
                .revs("2d8897c9ac29ce42c3442cf80ac977057045e7f6",
                        "74de5497dfca9731e455d60552f9a8906e5dc1ac",
                        "f230fad3ef7004aa7a79d4df26b293dad695ca32",
                        "^6053a1eaa1c009dd11092d09a72f3c41af1b59ad",
                        "^017caf31eca7c46eb3d1800fcac431cfa7147a01",
                        "^2c7eda7bdbdd660cf241f77ed5736d2d89aa159c");
        environment.forEach(revListBuilder::withEnvironment);

        List<String> lines = revListBuilder.build(new LinesStdioHandler()).run();
        assertThat(lines, contains("f230fad3ef7004aa7a79d4df26b293dad695ca32",
                "74de5497dfca9731e455d60552f9a8906e5dc1ac",
                "3528cf690cb37f6adb85b7bd40cc7a6118d4b598",
                "2d8897c9ac29ce42c3442cf80ac977057045e7f6",
                "9c05f43f859375e392d90d23a13717c16d0fdcda"));
    }
}