package com.atlassian.stash.internal.scm.git.home;

import com.atlassian.bitbucket.scm.annotation.Repo;
import com.atlassian.bitbucket.home.SimpleHomeUpdate;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.stash.internal.scm.git.AbstractGitTest;
import com.atlassian.bitbucket.scm.git.command.LoggingCommandOutputHandler;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.scm.git.GitScmConfig;
import com.atlassian.bitbucket.util.FileUtils;
import com.google.common.collect.Lists;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.beans.factory.annotation.Autowired;

import java.io.File;
import java.io.IOException;
import java.util.List;

import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

public class GitAlternatesHomeUpdateHandlerTest extends AbstractGitTest {

    @Rule
    public TemporaryFolder folder = new TemporaryFolder();

    @Autowired
    private GitCommandBuilderFactory builderFactory;
    @Autowired
    private GitScmConfig config;
    @Autowired
    private I18nService i18nService;
    @Repo
    private Repository root;

    @Test
    public void testApply() throws IOException {
        //Step 1: Produce a pair of directories to be the old and new home directories
        File oldHome = folder.newFolder();
        File oldData = FileUtils.mkdir(oldHome, "data");
        File oldRepositories = FileUtils.mkdir(oldData, "repositories");

        File newHome = folder.newFolder();
        File newRepositories = FileUtils.construct(newHome, "data", "repositories");
        assertTrue("The new home directory could not be created", newHome.delete()); //Just need the name, not a folder

        //Step 2: Produce a pair of top-level repositories in the home directory.
        //- One should have forks, one shouldn't
        //- Do NOT use the "root" repository directly in this test; it may corrupt it
        List<File> repositories = Lists.newArrayListWithCapacity(8);
        for (int i = 0; i < 2; ++i) {
            //Note that these clones _are not_ shared; we don't want alternates pointing out of the home directory,
            //and we also want to be certain we don't somehow corrupt the "root" repository for other tests
            repositories.add(clone(config.getRepositoryDir(root), oldRepositories, i + 1, false));
        }

        //Step 3: Create a fork hierarchy under one of the two top-level repositories
        File parent = repositories.get(0);
        for (int i = 0; i < 3; ++i) {
            //As with the forks created by GitCommandFactory.fork, these forks should use alternates (shared clones)
            File fork = clone(parent, oldRepositories, i + repositories.size() + 1, true);
            repositories.add(fork);

            if (i == 1) {
                //Just for grins, deepen the hierarchy on one of the forks. The ID used here is intentionally not
                //contiguous with other IDs
                repositories.add(clone(fork, oldRepositories, 42, true));
            }
        }

        //Step 4: Sanity test by verifying that all repositories fsck cleanly
        fsckAll(oldRepositories, repositories);

        //Step 5: Move the home directory
        assertTrue("The home directory could not be moved", oldHome.renameTo(newHome));

        //The @Autowired GitScmConfig cannot be used directly on the update handler, because its repositoriesDir will
        //not take the spoof home directories being used for the test into consideration. Since that's the only method
        //that will be "wrong", use a spy to replace that one method
        GitScmConfig spyConfig = spy(config);
        when(spyConfig.getRepositoriesDir()).thenReturn(newRepositories);

        //With all the preparation taken care of, actually update all the alternates
        GitAlternatesHomeUpdateHandler handler = new GitAlternatesHomeUpdateHandler(spyConfig, i18nService);
        handler.apply(new SimpleHomeUpdate.Builder()
                .oldDir(oldHome.getAbsolutePath())
                .newDir(newHome.getAbsolutePath())
                .build());

        //All alternates should have been updated, which means fsck should still run cleanly
        fsckAll(newRepositories, repositories);
    }

    @Test
    public void testApplyWithNoForks() throws IOException {
        //Step 1: Produce a pair of directories to be the old and new home directories
        File oldHome = folder.newFolder();
        File oldData = FileUtils.mkdir(oldHome, "data");
        File oldRepositories = FileUtils.mkdir(oldData, "repositories");

        File newHome = folder.newFolder();
        File newRepositories = FileUtils.construct(newHome, "data", "repositories");
        assertTrue("The new home directory could not be created", newHome.delete()); //Just need the name, not a folder

        //Step 2: Produce a pair of top-level repositories in the home directory.
        //- One should have forks, one shouldn't
        //- Do NOT use the "root" repository directly in this test; it may corrupt it
        List<File> repositories = Lists.newArrayListWithCapacity(8);
        for (int i = 0; i < 2; ++i) {
            //Note that these clones _are not_ shared; we don't want alternates pointing out of the home directory,
            //and we also want to be certain we don't somehow corrupt the "root" repository for other tests
            repositories.add(clone(config.getRepositoryDir(root), oldRepositories, i + 1, false));
        }

        //Step 3: Sanity test by verifying that all repositories fsck cleanly
        fsckAll(oldRepositories, repositories);

        //Step 4: Move the home directory
        assertTrue("The home directory could not be moved", oldHome.renameTo(newHome));

        //Step 5: Top-level repositories should not care about the home directory location, so they should still fsck
        //        cleanly even after the move, without applying the update
        fsckAll(newRepositories, repositories);

        GitScmConfig spyConfig = spy(config);
        when(spyConfig.getRepositoriesDir()).thenReturn(newRepositories);

        GitAlternatesHomeUpdateHandler handler = new GitAlternatesHomeUpdateHandler(spyConfig, i18nService);
        handler.apply(new SimpleHomeUpdate.Builder()
                .oldDir(oldHome.getAbsolutePath())
                .newDir(newHome.getAbsolutePath())
                .build());

        //Nothing should have been updated, and all of the repositories should still fsck cleanly
        fsckAll(newRepositories, repositories);
    }

    private File clone(File origin, File repositoriesDir, int id, boolean shared) {
        File clone = new File(repositoriesDir, String.valueOf(id));

        builderFactory.builder().clone()
                .bare()
                .directory(clone)
                .origin(origin)
                .shared(shared)
                .build()
                .call();

        return clone;
    }

    private void fsckAll(File repositoriesDir, List<File> repositories) {
        for (File repository : repositories) {
            //Note: --no-dangling isn't supported until 1.7.10 and later, so it can't be used
            LoggingCommandOutputHandler outputHandler = new LoggingCommandOutputHandler();
            outputHandler.setCommand(builderFactory.builder()
                    .command("fsck")
                    //Use the repositoriesDir because the repository File is wrong after the move
                    .workingDirectory(new File(repositoriesDir, repository.getName()))
                    .build(outputHandler))
                    .call();
        }
    }
}
