package com.atlassian.stash.internal.scm.git.command.gc;

import com.atlassian.bitbucket.scm.CommandFailedException;
import com.atlassian.stash.internal.scm.git.AbstractGitTest;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.Command;
import com.atlassian.bitbucket.scm.annotation.DirtiesRepo;
import com.atlassian.bitbucket.scm.annotation.Repo;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.scm.git.GitRefPattern;
import com.atlassian.bitbucket.scm.git.GitScmConfig;
import com.atlassian.bitbucket.scm.git.command.gc.GitGcBuilder;
import com.atlassian.bitbucket.util.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.Nonnull;
import java.io.File;

import static com.atlassian.stash.internal.scm.git.GitFactoriesTestHelper.*;
import static org.hamcrest.Matchers.containsString;

public class GcCommandTest extends AbstractGitTest {

    private static final String DANGLING_COMMIT = "1bd8a62a123e38819492293d02dbb6c663420aba";
    private static final String TEST_REF = GitRefPattern.HEADS.qualify("test");

    @Autowired
    private GitScmConfig config;
    @Autowired
    private GitCommandBuilderFactory builderFactory;
    @Repo("classpath:git/garbage.zip")
    private Repository repository;
    private File repositoryDir;

    @Before
    public void setup() {
        repositoryDir = config.getRepositoryDir(repository);

        assertEquals(21, countLooseObjects(repositoryDir, builderFactory));
        assertEquals(1, countPackFiles(repositoryDir));
    }

    @DirtiesRepo
    @Test
    public void testGc() {
        addTestRef(); //Protect some garbage so we can tell --prune works later

        gc().build().call(); //Should pull all the newly-reachable objects into a pack
        assertEquals(3, countLooseObjects(repositoryDir, builderFactory));
        assertEquals(1, countPackFiles(repositoryDir));

        prune().build().call(); //Should prune the remaining three unreachable objects
        assertEquals(0, countLooseObjects(repositoryDir, builderFactory));
        assertEquals(1, countPackFiles(repositoryDir));
    }

    @Test
    public void testGcWithWorkingCopy() throws Exception {
        File cloneDir = FileUtils.createTempDir("gc-test", null, config.getTempDir());

        //Cloning with a filesystem path uses bulk operations to copy all files. That means
        //a clone is created with all of its origin's garbage present
        builderFactory.builder().clone()
                .normal()
                .directory(cloneDir)
                .quiet(true)
                .origin(repositoryDir)
                .build()
                .call();
        //Verify that our garbage was retained through the clone
        assertEquals(21, countLooseObjects(cloneDir, builderFactory));
        assertEquals(1, countPackFiles(cloneDir));
        addTestRef(cloneDir); //Protect some garbage so we can tell --prune=now works later

        gc().workingDirectory(cloneDir).build().call();
        assertEquals(3, countLooseObjects(cloneDir, builderFactory));
        assertEquals(1, countPackFiles(cloneDir));

        prune().workingDirectory(cloneDir).build().call();
        assertEquals(0, countLooseObjects(cloneDir, builderFactory));
        assertEquals(1, countPackFiles(cloneDir));
    }

    @DirtiesRepo
    @Test
    public void testGcAuto() {
        addTestRef(); //Protect some garbage so we can tell git gc --auto packed loose objects

        //Build the command once and just reuse it. The first run will be a no-op
        StringHandler handler = new StringHandler();
        Command<?> command = gc().auto(true).errorHandler(handler).build();

        //There should be not enough objects for gc --auto to trigger, so this should do nothing
        command.call();
        assertTrue(StringUtils.isEmpty(handler.getOutput())); //Verify git gc --auto didn't actually run
        assertEquals(21, countLooseObjects(repositoryDir, builderFactory));
        assertEquals(1, countPackFiles(repositoryDir));

        configureGcAuto(); //Lower thresholds so gc --auto will run

        command.call();
        assertThat(handler.getOutput(), containsString("Auto packing the repository"));
        assertEquals(3, countLooseObjects(repositoryDir, builderFactory));
        assertEquals(2, countPackFiles(repositoryDir)); //Should create a second pack, not rewrite the first one
    }

    //This must be tested separately because there are not enough loose objects in the repository, if this is
    //verified after git gc  --auto in testGcAuto(), to trigger auto GC again
    @DirtiesRepo
    @Test
    public void testGcAutoWithPruneNow() {
        configureGcAuto(); //Lower thresholds so gc --auto will run

        //This should kill both the 18 objects associated with the dangling commit, since we haven't
        //added a ref for it, and it should kill the three always-unreachable blobs as well
        StringHandler handler = new StringHandler();
        prune().auto(true).errorHandler(handler).build().call();
        assertThat(handler.getOutput(), containsString("Auto packing the repository"));
        assertEquals(0, countLooseObjects(repositoryDir, builderFactory));
        assertEquals(1, countPackFiles(repositoryDir));
    }

    @DirtiesRepo
    @Test
    public void testGcQuiet() {
        String output = prune()
                .quiet(true)
                .build(new StringHandler())
                .call();
        assertTrue(StringUtils.isEmpty(output));
        assertEquals(0, countLooseObjects(repositoryDir, builderFactory));
        assertEquals(1, countPackFiles(repositoryDir));
    }

    /**
     * The available test repositories are not large enough for {@code git gc --aggressive} to make a difference
     * compared to {@code git gc}. Manually generating suboptimal packs using a command like {@code git repack
     * --depth=1 --window=1 --window-memory=1 -A -d} leads to same packs as {@code git gc --aggressive} on those
     * repositories.
     *
     * Given those limitations in test data, this is just a sanity check that garbage collection is triggered as
     * expected when {@code --aggressive} is applied.
     */
    @DirtiesRepo
    @Test
    public void testGcAggressive() {
        addTestRef();

        gc().aggressive(true).build().call();
        assertEquals(3, countLooseObjects(repositoryDir, builderFactory));
        assertEquals(1, countPackFiles(repositoryDir));

        prune().aggressive(true).build().call();
        assertEquals(0, countLooseObjects(repositoryDir, builderFactory));
        assertEquals(1, countPackFiles(repositoryDir));
    }

    @Test(expected = CommandFailedException.class)
    public void testGcOnEmptyDir() throws Exception {
        File emptyDir = FileUtils.createTempDir("gc-test", null, config.getTempDir());

        gc().workingDirectory(emptyDir).build().call();
    }

    private void addTestRef() {
        addTestRef(repositoryDir);
    }

    private void addTestRef(@Nonnull File workingDir) {
        builderFactory.builder().updateRef()
                .set(TEST_REF, DANGLING_COMMIT)
                .author("John Developer", "stash@atlassian.com")
                .workingDirectory(workingDir)
                .build()
                .call();
    }

    private void configureGcAuto() {
        //Lower the auto threshold. The garbage repository meets gc --auto requirements immediately
        //when using these settings
        builderFactory.builder(repository).config()
                .set("gc.auto", "2")
                .build()
                .call();
        //As of 2.0.0 Git can run gc --auto in the background on systems that support it, so
        //we disable that feature here
        //Details: http://permalink.gmane.org/gmane.comp.version-control.git/241821
        builderFactory.builder(repository).config()
                .set("gc.autodetach", "false")
                .build()
                .call();
    }

    private GitGcBuilder gc() {
        return builderFactory.builder(repository).gc();
    }

    private GitGcBuilder prune() {
        return gc()
                .prune(true)
                .pruneOlderThan("now");
    }
}
