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

import com.atlassian.bitbucket.repository.ConflictingRefNameException;
import com.atlassian.bitbucket.repository.DuplicateRefException;
import com.atlassian.bitbucket.repository.NoSuchBranchException;
import com.atlassian.stash.internal.scm.git.AbstractGitTest;
import com.atlassian.bitbucket.repository.Branch;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.annotation.DirtiesRepo;
import com.atlassian.bitbucket.scm.annotation.Repo;
import com.atlassian.bitbucket.scm.git.GitAgent;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.scm.git.GitScmConfig;
import com.atlassian.bitbucket.scm.git.command.branch.GitBranch;
import com.atlassian.bitbucket.scm.git.command.branch.GitBranchNotMergedException;
import org.apache.commons.lang3.SystemUtils;
import org.junit.Assume;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Queue;

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

@SuppressWarnings("ConstantConditions")
public class BranchCommandTest extends AbstractGitTest {

    @Autowired
    private GitCommandBuilderFactory builderFactory;
    @Autowired
    private GitScmConfig config;
    @Autowired
    private GitAgent agent;
    @Repo
    private Repository repository;
    @Repo("classpath:git/merge.zip")
    private Repository remoteRepository;

    @Test
    @DirtiesRepo("repository")
    public void testCreateNewBranch() throws Exception {
        Branch originalHead = agent.resolveHead(repository);
        String branchName = "new-" + System.currentTimeMillis();

        createBuilder().create(branchName).build().call();

        Branch expected = agent.resolveBranch(repository, branchName);
        assertEquals(originalHead.getLatestCommit(), expected.getLatestCommit());
    }

    @Test(expected = DuplicateRefException.class)
    @DirtiesRepo("repository")  // in case it does succeed
    public void testCreateExistingBranch() throws Exception {
        Branch existing = agent.resolveHead(repository);

        createBuilder().create(existing.getDisplayId()).build().call();
    }

    @Test(expected = ConflictingRefNameException.class)
    @DirtiesRepo("repository")
    public void testCreateConflictingBranchFileDirLoose() throws Exception {
        createBuilder().create("bugfix/BUG-1").build().call();
        createBuilder().create("bugfix").build().call();
    }

    @Test(expected = ConflictingRefNameException.class)
    @DirtiesRepo("repository")
    public void testCreateConflictingBranchDirFileLoose() throws Exception {
        // Git on Windows produces an ambiguous error message when loose refs are involved, so we don't throw
        // a ConflictingRefNameException, but instead the more general CommandFailedException
        Assume.assumeFalse(SystemUtils.IS_OS_WINDOWS);

        createBuilder().create("bugfix").build().call();
        createBuilder().create("bugfix/BUG-1").build().call();
    }

    @Test(expected = ConflictingRefNameException.class)
    @DirtiesRepo("repository")
    public void testCreateConflictingBranchFileDirPacked() throws Exception {
        createBuilder().create("bugfix/BUG-1").build().call();
        runGarbageCollection();
        createBuilder().create("bugfix").build().call();
    }

    @Test(expected = ConflictingRefNameException.class)
    @DirtiesRepo("repository")
    public void testCreateConflictingBranchDirFilePacked() throws Exception {
        createBuilder().create("bugfix").build().call();
        runGarbageCollection();
        createBuilder().create("bugfix/BUG-1").build().call();
    }

    @Test
    @DirtiesRepo("repository")
    public void testForceCreateExistingBranch() throws Exception {
        Branch master = agent.resolveBranch(repository, "master");
        Branch existing = agent.resolveBranch(repository, "basic_branching");

        createBuilder().create(existing.getDisplayId()).force(true).startPoint(master.getDisplayId()).build().call();

        Branch updated = agent.resolveBranch(repository, "basic_branching");
        assertEquals(master.getLatestCommit(), updated.getLatestCommit());
    }

    @Test
    @DirtiesRepo("repository")
    public void testCreateNewBranchWithRefLog() throws Exception {
        Branch master = agent.resolveBranch(repository, "master");
        String newBranch = "new-" + System.currentTimeMillis();

        createBuilder().create(newBranch).reflog(true).startPoint(master.getDisplayId()).build().call();

        Queue<String> reflog = getReflog(config.getRepositoryDir(repository), 1, newBranch, builderFactory);
        assertThat(reflog.poll(), allOf(containsString(newBranch), containsString(master.getDisplayId()),
                //The reflog entry is created by git, so it will use a 7 character short hash, not 11
                containsString(master.getLatestCommit().substring(0, 7))));
    }

    @Test
    @DirtiesRepo("repository")
    public void testCreateNewBranchWithRemoteTracking() throws Exception {
        String remoteName = "new-remote-" + System.currentTimeMillis();
        Branch remoteMaster = agent.resolveBranch(remoteRepository, "master");
        addRemote(config.getRepositoryDir(repository), config.getRepositoryDir(remoteRepository), remoteName, builderFactory);

        String newBranch = "new-" + System.currentTimeMillis();
        createBuilder().create(newBranch).track(true).startPoint(remoteName + "/" + remoteMaster.getDisplayId()).build().call();

        String tracked = getTrackedRemote(config.getRepositoryDir(repository), newBranch, builderFactory);
        assertEquals(remoteName, tracked);
    }

    @Test
    @DirtiesRepo("repository")
    public void testDeleteBranch() throws Exception {
        createAndDeleteBranch("master", false);
    }

    @Test(expected = GitBranchNotMergedException.class)
    @DirtiesRepo("repository")
    public void testDeleteBranchNonForceUnmerged() throws Exception {
        createAndDeleteBranch("basic_branching", false);
    }

    @DirtiesRepo("repository")
    public void testDeleteBranchForceUnmerged() throws Exception {
        createAndDeleteBranch("basic_branching", true);
    }

    private void createAndDeleteBranch(String start, boolean force) {
        String branchName = "new-" + System.currentTimeMillis();

        createBuilder().create(branchName).startPoint(start).build().call();
        createBuilder().delete(branchName).force(force).build().call();

        assertNull("Branch should no longer exist", agent.resolveBranch(repository, branchName));
    }

    @Test(expected = NoSuchBranchException.class)
    @DirtiesRepo("repository")
    public void testDeleteMissingBranch() throws Exception {
        createBuilder().delete("some_branch").build().call();
    }

    private GitBranch createBuilder() {
        return builderFactory.builder(repository).branch();
    }

    private void runGarbageCollection() {
        builderFactory.builder(repository).gc().build().call();
    }
}
