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

import com.atlassian.bitbucket.mesh.junit.LogListener;
import com.atlassian.bitbucket.mesh.util.Version;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import io.atlassian.fugue.Option;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;

import static com.atlassian.bitbucket.mesh.git.binary.GitBinaryHelper.MINIMUM_VERSION;
import static com.atlassian.bitbucket.mesh.git.binary.GitBinaryHelper.NEXT_MINIMUM_VERSION;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultGitBinaryHelperTest {

    private static final Version OLD_VERSION = new Version(2, 30, 0);
    private static final String PROP_BINARY_PATH = "path.executable";
    private static final String PROP_PREFIX = "plugin.bitbucket-git.";

    @Rule
    public final LogListener logListener = new LogListener(DefaultGitBinaryHelper.class);
    @Rule
    public final TemporaryFolder tempFolder = new TemporaryFolder();

    private File binary;
    private DefaultGitBinaryHelper binaryHelper;

    @Before
    public void setup() throws Exception {
        // create a fake binary
        binary = tempFolder.newFile("git").getAbsoluteFile();
        assertTrue(binary.setExecutable(true));

        binaryHelper = spy(new DefaultGitBinaryHelper());
    }

    /**
     * Tests that an acceptable version of git is being used to run the unit tests. This should help prevent developers
     * from wasting time troubleshooting random unit test failures caused because the version of git the tests happened
     * to end up using was not supported.
     */
    @Test
    public void testFind() {
        //The "configured" here makes this test sensitive to being run in the version matrix build
        String configured = System.getProperty(PROP_PREFIX + PROP_BINARY_PATH);
        new DefaultGitBinaryHelper().find(configured);
    }

    @Test
    public void testFindBinaryWorldWritableForPosix() {
        assertTrue(binary.setWritable(true, false));

        doReturn(Collections.singletonList(binary.getParent())).when(binaryHelper).getSearchPaths();

        IllegalStateException e = assertThrows(IllegalStateException.class, () -> binaryHelper.find(null));
        assertThat(e.getMessage(), containsString("Git was not found on the PATH. Install Git 2.31.0+"));
        logListener.assertWarn("will be ignored because it is world-writable and could be exploited");
    }

    /**
     * Tests that the directories in the {@code PATH} are searched in order for a {@code git} binary and that the first
     * binary with an acceptable version is immediately returned.
     *
     * @throws Exception if test files/folders cannot be created
     */
    @Test
    public void testFindRespectsPathOrder() throws Exception {
        File extraGit = new File(tempFolder.newFolder(), binary.getName());
        assertTrue(extraGit.createNewFile());
        assertTrue(extraGit.setExecutable(true));

        doReturn(Arrays.asList(extraGit.getParent(), binary.getParent()))
                .when(binaryHelper)
                .getSearchPaths();
        doReturn(Option.some(MINIMUM_VERSION)).when(binaryHelper).versionOf(any(File.class));

        GitBinary found = binaryHelper.find(null);
        assertEquals(extraGit.getAbsolutePath(), found.getPath());
        assertSame(MINIMUM_VERSION, found.getVersion());

        assertTrue(extraGit.delete()); //Delete "extraGit", next find should return "binary"
        found = binaryHelper.find(null);
        assertEquals(binary.getAbsolutePath(), found.getPath());
        assertSame(MINIMUM_VERSION, found.getVersion());
    }

    /**
     * Tests that if multiple unsupported versions of {@code git} are found in the path, the "best" version of the
     * various binaries is used in the exception that is thrown.
     *
     * @throws Exception if test files/folders cannot be created
     */
    @Test
    public void testFindReturnsBestVersionForOnlyUnsupportedVersionsPresent() throws Exception {
        Version expected = new Version(2, 9, 5);

        File first = new File(tempFolder.newFolder(), "git");
        assertTrue(first.createNewFile());
        assertTrue(first.setExecutable(true));
        File second = new File(tempFolder.newFolder(), "git");
        assertTrue(second.createNewFile());
        assertTrue(second.setExecutable(true));
        File third = new File(tempFolder.newFolder(), "git");
        assertTrue(third.createNewFile());
        assertTrue(third.setExecutable(true));

        doReturn(Arrays.asList(first.getParent(), second.getParent(), third.getParent()))
                .when(binaryHelper)
                .getSearchPaths();
        doReturn(Option.some(new Version(1, 7, 1))).when(binaryHelper).versionOf(eq(first));
        doReturn(Option.some(expected)).when(binaryHelper).versionOf(eq(second));
        doReturn(Option.some(new Version(1, 7, 5))).when(binaryHelper).versionOf(eq(third));

        IllegalStateException e = assertThrows(IllegalStateException.class, () -> binaryHelper.find(null));
        assertThat(e.getMessage(), containsString("You are using Git 2.9.5. " +
                "The minimum supported version is 2.31.0. Upgrade to Git 2.31.0+"));
    }

    /**
     * Tests that if multiple unsupported versions of {@code git} are found in the path, the first supported version
     * that is higher than NEXT_MINIMUM_VERSION is returned.
     *
     * @throws Exception if test files/folders cannot be created
     */
    @Test
    public void testFindReturnsBestVersionWhenUnsupportedVersionsPresent() throws Exception {
        Version tooOld = new Version(MINIMUM_VERSION.getMajor(), MINIMUM_VERSION.getMinor() - 1, 3);
        Version goodVersion = new Version(NEXT_MINIMUM_VERSION.getMajor(), NEXT_MINIMUM_VERSION.getMinor() + 1);
        Version betterVersion = new Version(goodVersion.getMajor(), goodVersion.getMinor(), 3, 4);
        Version unsupported = new Version(NEXT_MINIMUM_VERSION.getMajor() + 2, 0);
        binaryHelper.setUnsupportedFromVersion(unsupported);

        File first = new File(tempFolder.newFolder(), "git");
        assumeTrue(first.createNewFile());
        assumeTrue(first.setExecutable(true));
        File second = new File(tempFolder.newFolder(), "git");
        assumeTrue(second.createNewFile());
        assumeTrue(second.setExecutable(true));
        File third = new File(tempFolder.newFolder(), "git");
        assumeTrue(third.createNewFile());
        assumeTrue(third.setExecutable(true));
        File fourth = new File(tempFolder.newFolder(), "git");
        assumeTrue(fourth.createNewFile());
        assumeTrue(fourth.setExecutable(true));

        doReturn(Arrays.asList(first.getParent(), second.getParent(), third.getParent(), fourth.getParent()))
                .when(binaryHelper)
                .getSearchPaths();
        doReturn(Option.some(unsupported)).when(binaryHelper).versionOf(eq(first));
        doReturn(Option.some(tooOld)).when(binaryHelper).versionOf(eq(second));
        doReturn(Option.some(betterVersion)).when(binaryHelper).versionOf(eq(third));
        lenient().doReturn(Option.some(goodVersion)).when(binaryHelper).versionOf(eq(fourth));

        GitBinary found = binaryHelper.find(null);
        assertNotNull(found);
        assertSame(betterVersion, found.getVersion());
    }

    @Test
    public void testFindThrowsIfUnsupportedGitVersionFound() throws IOException {
        Version unsupported = new Version(NEXT_MINIMUM_VERSION.getMajor() + 2, 0);
        binaryHelper.setUnsupportedFromVersion(unsupported);

        File executable = new File(tempFolder.newFolder(), "git");
        assumeTrue(executable.createNewFile());
        assumeTrue(executable.setExecutable(true));
        doReturn(Option.some(unsupported)).when(binaryHelper).versionOf(eq(executable));

        doReturn(Collections.singleton(executable.getParent()))
                .when(binaryHelper)
                .getSearchPaths();

        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> binaryHelper.find(executable.getPath()));
        assertThat(e.getMessage(), containsString("You are using Git " + unsupported + ", which is not supported. " +
                "Downgrade Git to a version between " + getVersionMajorMinorString(MINIMUM_VERSION) +
                " and up to but not including " + getVersionMajorMinorString(unsupported) + " then restart."));
    }

    /**
     * Tests that the searched directories include the extra locations.
     */
    @Test
    public void testFindSearchesDefaultLocations() {
        assertThat(Lists.newArrayList(binaryHelper.getSearchPaths()),
                hasItems(Iterables.toArray(DefaultGitBinaryHelper.DEFAULT_LOCATIONS, String.class)));
    }

    /**
     * Tests that if the configured binary does not exist, the helper falls back to searching the path.
     */
    @Test
    public void testFindSearchesPathWhenConfiguredBinaryDoesNotExist() {
        doReturn(Collections.singletonList(binary.getParent())).when(binaryHelper).getSearchPaths();
        doReturn(Option.some(MINIMUM_VERSION)).when(binaryHelper).versionOf(eq(binary));

        GitBinary found = binaryHelper.find("/nonexistent/path/to/nonexistent/git");
        assertEquals(binary.getAbsolutePath(), found.getPath());
        assertSame(MINIMUM_VERSION, found.getVersion());

        logListener.assertWarn("does not exist or is not executable");
    }

    /**
     * Tests that if the configured binary fails while trying to determine its version (for example, it's not a real
     * {@code git} binary), the helper falls back to searching the path.
     *
     * @throws Exception if test files/folders cannot be created
     */
    @Test
    public void testFindSearchesPathWhenConfiguredBinaryFails() throws Exception {
        File configuredGit = new File(tempFolder.newFolder(), "git");
        assertTrue(configuredGit.createNewFile());
        assertTrue(configuredGit.setExecutable(true));

        doReturn(Collections.singletonList(binary.getParent())).when(binaryHelper).getSearchPaths();
        doReturn(Option.none()).when(binaryHelper).versionOf(eq(configuredGit));
        doReturn(Option.some(MINIMUM_VERSION)).when(binaryHelper).versionOf(eq(binary));

        GitBinary found = binaryHelper.find(configuredGit.getAbsolutePath());
        assertEquals(binary.getAbsolutePath(), found.getPath());
        assertSame(MINIMUM_VERSION, found.getVersion());

        logListener.assertWarn("is not valid");
    }

    /**
     * Tests that if the configured binary does not meet the required minimum version, the helper falls back to
     * searching the path.
     *
     * @throws Exception if test files/folders cannot be created
     */
    @Test
    public void testFindSearchesPathWhenConfiguredBinaryIsUnsupported() throws Exception {
        File configuredGit = new File(tempFolder.newFolder(), "git");
        assertTrue(configuredGit.createNewFile());
        assertTrue(configuredGit.setExecutable(true));

        doReturn(Collections.singletonList(binary.getParent())).when(binaryHelper).getSearchPaths();
        doReturn(Option.some(OLD_VERSION)).when(binaryHelper).versionOf(eq(configuredGit));
        doReturn(Option.some(MINIMUM_VERSION)).when(binaryHelper).versionOf(eq(binary));

        GitBinary found = binaryHelper.find(configuredGit.getAbsolutePath());
        assertEquals(binary.getAbsolutePath(), found.getPath());
        assertSame(MINIMUM_VERSION, found.getVersion());

        logListener.assertWarn(" is the minimum required version");
    }

    /**
     * Tests that the configured path is used if it's valid.
     */
    @Test
    public void testFindWhenConfigured() {
        doReturn(Option.some(MINIMUM_VERSION)).when(binaryHelper).versionOf(eq(binary));

        GitBinary found = binaryHelper.find(binary.getAbsolutePath());
        assertEquals(binary.getAbsolutePath(), found.getPath());
        assertSame(MINIMUM_VERSION, found.getVersion());

        verify(binaryHelper, never()).getSearchPaths();
    }

    @Test
    public void testFindWhenNotFound() {
        doReturn(Collections.<String>emptySet()).when(binaryHelper).getSearchPaths();

        IllegalStateException e = assertThrows(IllegalStateException.class, () -> binaryHelper.find(null));
        assertThat(e.getMessage(), containsString("Git was not found on the PATH. Install Git 2.31.0+"));
    }

    @Test
    public void testFindWithVersionOlderThanMinimum() {
        doReturn(Arrays.asList("/nonexistent/path", binary.getParent())).when(binaryHelper).getSearchPaths();
        doReturn(Option.some(OLD_VERSION)).when(binaryHelper).versionOf(eq(binary));

        IllegalStateException e = assertThrows(IllegalStateException.class, () -> binaryHelper.find(null));
        assertThat(e.getMessage(), containsString("You are using Git 2.30.0. " +
                "The minimum supported version is 2.31.0. Upgrade to Git 2.31.0+"));
    }

    private String getVersionMajorMinorString(Version version) {
        return version.getMajor() + "." + version.getMinor();
    }
}