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

import com.atlassian.bitbucket.NoSuchObjectException;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.scm.annotation.DirtiesRepo;
import com.atlassian.bitbucket.scm.annotation.Repo;
import com.atlassian.bitbucket.scm.git.GitRefPattern;
import com.atlassian.bitbucket.scm.git.GitScmConfig;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.scm.git.command.GitCommandFactory;
import com.atlassian.bitbucket.scm.git.command.branch.GitBranchNotMergedException;
import com.atlassian.bitbucket.scm.git.command.tag.GitTagType;
import com.atlassian.bitbucket.scm.git.ref.GitCreateBranchCommandParameters;
import com.atlassian.bitbucket.scm.git.ref.GitCreateTagCommandParameters;
import com.atlassian.bitbucket.scm.git.ref.GitDeleteBranchCommandParameters;
import com.atlassian.bitbucket.scm.git.ref.GitDeleteTagCommandParameters;
import com.atlassian.bitbucket.scm.ref.CreateBranchCommandParameters;
import com.atlassian.bitbucket.scm.ref.CreateTagCommandParameters;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.stash.internal.scm.git.AbstractGitTest;
import com.atlassian.stash.internal.scm.git.DefaultGitRefCommandFactory;
import com.atlassian.stash.internal.scm.git.GitRepositoryConfig;
import com.atlassian.stash.internal.scm.git.InternalGitAgent;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;

import javax.annotation.Resource;
import java.util.Queue;

import static com.atlassian.stash.internal.scm.git.GitFactoriesTestHelper.getReflog;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.Mockito.when;

public class DefaultGitRefCommandFactoryTest extends AbstractGitTest {

    @Rule
    public final ExpectedException expectedException = ExpectedException.none();

    @Resource
    private InternalGitAgent agent;
    @Mock
    private AuthenticationContext authenticationContext;
    @Mock
    ApplicationUser author;
    @Resource
    private GitCommandBuilderFactory builderFactory;
    @Resource
    private GitCommandFactory commandFactory;
    @Resource
    private GitScmConfig config;
    private I18nService i18nService = new SimpleI18nService();
    private DefaultGitRefCommandFactory refCommandFactory;
    @Repo
    private Repository repository;
    @Resource
    private GitRepositoryConfig repositoryConfig;

    @Before
    public void setup() {
        refCommandFactory = new DefaultGitRefCommandFactory(agent, authenticationContext, builderFactory, commandFactory, i18nService, repositoryConfig);
        when(author.getDisplayName()).thenReturn("John Doe");
        when(author.getEmailAddress()).thenReturn("jdoe@test.com");
    }

    @DirtiesRepo
    @Test
    public void testCreateBranch() {
        Branch originalHead = agent.resolveHead(repository);
        String branchName = "new_branch";
        CreateBranchCommandParameters commandParameters = new CreateBranchCommandParameters.Builder()
                .author(author)
                .name(branchName)
                .startPoint("HEAD").build();

        Branch branch = refCommandFactory.createBranch(repository, commandParameters).call();
        assertNotNull(branch);
        assertEquals(branchName, branch.getDisplayId());
        assertEquals(GitRefPattern.HEADS.qualify(branchName), branch.getId());
        assertEquals(originalHead.getLatestCommit(), branch.getLatestCommit());
    }

    @DirtiesRepo
    @Test
    public void testCreateBranchWithFullyQualifiedBranchName() {
        Branch originalHead = agent.resolveHead(repository);
        String branchName = "fq_new_branch";
        String fqBranchName = GitRefPattern.HEADS.qualify(branchName);
        CreateBranchCommandParameters commandParameters = new CreateBranchCommandParameters.Builder()
                .author(author)
                .name(fqBranchName)
                .startPoint("HEAD").build();

        Branch branch = refCommandFactory.createBranch(repository, commandParameters).call();
        assertNotNull(branch);
        assertEquals(branchName, branch.getDisplayId());
        assertEquals(fqBranchName, branch.getId());
        assertEquals(originalHead.getLatestCommit(), branch.getLatestCommit());
    }

    @DirtiesRepo
    @Test
    public void testCreateBranchShouldSetCurrentUserAsAuthorIfAuthorNotProvided() {
        Branch originalHead = agent.resolveHead(repository);
        String branchName = "new_branch";
        CreateBranchCommandParameters commandParameters = new CreateBranchCommandParameters.Builder()
                .name(branchName)
                .startPoint("HEAD").build();
        when(authenticationContext.getCurrentUser()).thenReturn(author);

        Branch branch = refCommandFactory.createBranch(repository, commandParameters).call();
        assertNotNull(branch);
        assertEquals(branchName, branch.getDisplayId());
        assertEquals(GitRefPattern.HEADS.qualify(branchName), branch.getId());
        assertEquals(originalHead.getLatestCommit(), branch.getLatestCommit());
    }

    @Test(expected = InvalidAuthorException.class)
    public void testCreateBranchShouldFailIfAuthorIsNotAvailable() {
        String branchName = "new_branch";
        CreateBranchCommandParameters commandParameters = new CreateBranchCommandParameters.Builder()
                .name(branchName)
                .startPoint("HEAD").build();

        refCommandFactory.createBranch(repository, commandParameters).call();
    }

    @Test(expected = InvalidAuthorException.class)
    public void testCreateBranchShouldFailIfAuthorsNameIsBlank() {
        when(author.getDisplayName()).thenReturn("");
        String branchName = "new_branch";
        CreateBranchCommandParameters commandParameters = new CreateBranchCommandParameters.Builder()
                .author(author)
                .name(branchName)
                .startPoint("HEAD").build();

        refCommandFactory.createBranch(repository, commandParameters).call();
    }

    @Test(expected = InvalidAuthorException.class)
    public void testCreateBranchShouldFailIfAuthorsEmailIsBlank() {
        when(author.getEmailAddress()).thenReturn("");
        String branchName = "new_branch";
        CreateBranchCommandParameters commandParameters = new CreateBranchCommandParameters.Builder()
                .author(author)
                .name(branchName)
                .startPoint("HEAD").build();

        refCommandFactory.createBranch(repository, commandParameters).call();
    }

    @DirtiesRepo
    @Test
    public void testCreateBranchWithGitSpecificForceParameter() {
        Branch master = agent.resolveHead(repository);
        Branch existing = agent.resolveBranch(repository, "basic_branching");
        assertNotNull(existing);

        GitCreateBranchCommandParameters parameters = new GitCreateBranchCommandParameters.Builder()
                .author(author)
                .force(true)
                .name(existing.getDisplayId())
                .startPoint(master.getLatestCommit())
                .build();

        refCommandFactory.createBranch(repository, parameters).call();

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

    @DirtiesRepo
    @Test
    public void testCreateBranchWithGitSpecificReflogParameter() {
        String newBranch = "new-" + System.currentTimeMillis();

        Branch master = agent.resolveBranch(repository, "master");
        assertNotNull(master);

        GitCreateBranchCommandParameters parameters = new GitCreateBranchCommandParameters.Builder()
                .author(author)
                .name(newBranch)
                .reflog(true)
                .startPoint(master.getDisplayId())
                .build();

        refCommandFactory.createBranch(repository, parameters).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(expected = DuplicateRefException.class)
    public void testCreateBranchShouldFailIfBranchWithTheSameNameExists() {
        Branch existing = agent.resolveBranch(repository, "basic_branching");
        assertNotNull(existing);

        GitCreateBranchCommandParameters parameters = new GitCreateBranchCommandParameters.Builder()
                .author(author)
                .name(existing.getDisplayId())
                .startPoint("HEAD")
                .build();

        refCommandFactory.createBranch(repository, parameters).call();
    }

    @Test(expected = InvalidRefNameException.class)
    public void testCreateBranchShouldFailIfBranchNameIsInValid() {
        CreateBranchCommandParameters commandParameters = new CreateBranchCommandParameters.Builder()
                .author(author)
                .name("!@#$%^new_branch^&*(")
                .startPoint("HEAD").build();

        refCommandFactory.createBranch(repository, commandParameters).call();
    }

    @Test(expected = NoSuchObjectException.class)
    public void testCreateBranchShouldFailIfStartPointIsInValid() {
        CreateBranchCommandParameters commandParameters = new CreateBranchCommandParameters.Builder()
                .author(author)
                .name("new_branch")
                .startPoint("&*(HEAD^&*").build();

        refCommandFactory.createBranch(repository, commandParameters).call();
    }

    @DirtiesRepo
    @Test
    public void testCreateTag() {
        Branch head = agent.resolveHead(repository);
        String tagName = "new_tag";
        CreateTagCommandParameters parameters = new CreateTagCommandParameters.Builder()
                .author(author)
                .message("A new annotated Tag")
                .name(tagName)
                .startPoint("HEAD").build();

        Tag tag = refCommandFactory.createTag(repository, parameters).call();
        assertNotNull(tag);
        assertEquals(tagName, tag.getDisplayId());
        assertEquals(GitRefPattern.TAGS.qualify(tagName), tag.getId());
        assertEquals(head.getLatestCommit(), tag.getLatestCommit());
    }

    @DirtiesRepo
    @Test
    public void testCreateTagWithFullyQualifiedTagName() {
        Branch head = agent.resolveHead(repository);
        String tagName = "new_tag";
        String fqTagName = GitRefPattern.TAGS.qualify(tagName);
        CreateTagCommandParameters parameters = new CreateTagCommandParameters.Builder()
                .author(author)
                .message("A new annotated Tag")
                .name(fqTagName)
                .startPoint("HEAD").build();

        Tag tag = refCommandFactory.createTag(repository, parameters).call();
        assertNotNull(tag);
        assertEquals(tagName, tag.getDisplayId());
        assertEquals(fqTagName, tag.getId());
        assertEquals(head.getLatestCommit(), tag.getLatestCommit());
    }

    @Test(expected = InvalidRefNameException.class)
    public void testCreateTagShouldFailIfTagNameIsInValid() {
        CreateTagCommandParameters commandParameters = new CreateTagCommandParameters.Builder()
                .author(author)
                .name("!@#$%^new_branch^&*(")
                .startPoint("HEAD").build();

        refCommandFactory.createTag(repository, commandParameters).call();
    }


    @Test(expected = InvalidAuthorException.class)
    public void testCreateTagShouldFailIfAuthorIsNotAvailable() {
        String tagName = "new_tag";
        CreateTagCommandParameters parameters = new CreateTagCommandParameters.Builder()
                .message("A new annotated Tag")
                .name(tagName)
                .startPoint("HEAD").build();

        refCommandFactory.createTag(repository, parameters).call();
    }

    @DirtiesRepo
    @Test
    public void testCreateTagWithGitSpecificForceAndTypeParameter() {
        Branch head = agent.resolveHead(repository);
        String tagName = "new_tag";
        GitCreateTagCommandParameters parameters = new GitCreateTagCommandParameters.Builder()
                .author(author)
                .message("A new lightweight Tag pointing to basic_branching")
                .name(tagName)
                .startPoint("basic_branching")
                .type(GitTagType.LIGHTWEIGHT).build();

        refCommandFactory.createTag(repository, parameters).call();

        //change the start point
        parameters = new GitCreateTagCommandParameters.Builder()
                .author(author)
                .force(true)
                .message("A lightweight Tag pointing to HEAD")
                .name(tagName)
                .startPoint("HEAD")
                .type(GitTagType.LIGHTWEIGHT).build();

        Tag tag = refCommandFactory.createTag(repository, parameters).call();
        assertNotNull(tag);
        assertEquals(tagName, tag.getDisplayId());
        assertEquals(GitRefPattern.TAGS.qualify(tagName), tag.getId());
        assertEquals(head.getLatestCommit(), tag.getLatestCommit());
    }

    @DirtiesRepo
    @Test
    public void testCreateTagWithTheSameNameAsAnExistingBranch() {
        Branch head = agent.resolveHead(repository);
        String tagName = "basic_branching";
        CreateTagCommandParameters parameters = new CreateTagCommandParameters.Builder()
                .author(author)
                .message("A new annotated Tag")
                .name(tagName)
                .startPoint("HEAD").build();

        Tag tag = refCommandFactory.createTag(repository, parameters).call();
        assertNotNull(tag);
        assertEquals(tagName, tag.getDisplayId());
        assertEquals(GitRefPattern.TAGS.qualify(tagName), tag.getId());
        assertEquals(head.getLatestCommit(), tag.getLatestCommit());
    }

    @DirtiesRepo
    @Test(expected = DuplicateRefException.class)
    public void testCreateTagShouldFailIfTagWithSameNameExist() {
        String tagName = "new_tag";
        CreateTagCommandParameters parameters = new CreateTagCommandParameters.Builder()
                .author(author)
                .message("A new annotated Tag")
                .name(tagName)
                .startPoint("HEAD").build();

        refCommandFactory.createTag(repository, parameters).call();
        refCommandFactory.createTag(repository, parameters).call();
    }

    @Test(expected = NoSuchObjectException.class)
    public void testCreateTagShouldFailIfStartPointIsNotValid() {
        String tagName = "new_tag";
        CreateTagCommandParameters parameters = new CreateTagCommandParameters.Builder()
                .author(author)
                .message("A new annotated Tag")
                .name(tagName)
                .startPoint("HEAD_THAT_DOES_NOT_EXIST").build();

        refCommandFactory.createTag(repository, parameters).call();
    }

    @Test(expected = InvalidRefNameException.class)
    public void testCreateTagShouldFailIfInvalidTagNamePassed() {
        String tagName = "##$%^^&invalid_tag_name%%^&*(";
        CreateTagCommandParameters parameters = new CreateTagCommandParameters.Builder()
                .author(author)
                .message("A new annotated Tag")
                .name(tagName)
                .startPoint("HEAD").build();

        refCommandFactory.createTag(repository, parameters).call();
    }

    @DirtiesRepo
    @Test
    public void testDeleteBranch() {
        GitDeleteBranchCommandParameters parameters = new GitDeleteBranchCommandParameters.Builder()
                .author(author)
                .message("Deleting basic_branching")
                .name("basic_branching").build();

        refCommandFactory.deleteBranch(repository, parameters).call();

        assertNull(agent.resolveBranch(repository, "basic_branching"));
    }

    @Test(expected = InvalidAuthorException.class)
    public void testDeleteBranchShouldFailIfAuthorIsNotAvailable() {
        GitDeleteBranchCommandParameters parameters = new GitDeleteBranchCommandParameters.Builder()
                .message("Deleting basic_branching")
                .name("basic_branching").build();

        refCommandFactory.deleteBranch(repository, parameters).call();
    }

    @DirtiesRepo
    @Test
    public void testDeleteBranchWithForceEqualsFalseShouldDeleteAMergedBranch() {
        GitDeleteBranchCommandParameters parameters = new GitDeleteBranchCommandParameters.Builder()
                .author(author)
                .message("Deleting basic_branching")
                .name("branch_mod_merge").build();

        refCommandFactory.deleteBranch(repository, parameters).call();

        assertNull(agent.resolveBranch(repository, "branch_mod_merge"));
    }

    @DirtiesRepo
    @Test
    public void testDeleteBranchWithForceForUnmergedBranch() {
        GitDeleteBranchCommandParameters parameters = new GitDeleteBranchCommandParameters.Builder()
                .author(author)
                .force(true)
                .message("Deleting basic_branching")
                .name("basic_branching").build();

        refCommandFactory.deleteBranch(repository, parameters).call();

        assertNull(agent.resolveBranch(repository, "basic_branching"));
    }

    @Test(expected = GitBranchNotMergedException.class)
    public void testDeleteBranchShouldFailIfTheBranchIsNotMergedToHead() {
        GitDeleteBranchCommandParameters parameters = new GitDeleteBranchCommandParameters.Builder()
                .author(author)
                .force(false)
                .message("Deleting basic_branching")
                .name("basic_branching").build();

        refCommandFactory.deleteBranch(repository, parameters).call();
    }

    @Test(expected = NoSuchBranchException.class)
    public void testDeleteBranchShouldFailIfTheBranchDoesNotExist() {
        GitDeleteBranchCommandParameters parameters = new GitDeleteBranchCommandParameters.Builder()
                .author(author)
                .message("Deleting basic_branching")
                .name("non_existent_branch").build();

        refCommandFactory.deleteBranch(repository, parameters).call();
    }

    @DirtiesRepo
    @Test
    public void testDeleteTag() {
        String tagName = "backdated_annotated_tag";

        GitDeleteTagCommandParameters parameters = new GitDeleteTagCommandParameters.Builder()
                .author(author)
                .name(tagName).build();

        refCommandFactory.deleteTag(repository, parameters).call();

        assertNull(commandFactory.resolveRef(repository, tagName).call());
    }

    @Test(expected = InvalidAuthorException.class)
    public void testDeleteTagShouldFailIfAuthorIsNotAvailable() {
        String tagName = "backdated_annotated_tag";

        GitDeleteTagCommandParameters parameters = new GitDeleteTagCommandParameters.Builder()
                .name(tagName).build();

        refCommandFactory.deleteTag(repository, parameters).call();
    }

    @Test(expected = NoSuchTagException.class)
    public void testDeleteTagShouldFailIfTagDoesNotExist() {
        GitDeleteTagCommandParameters parameters = new GitDeleteTagCommandParameters.Builder()
                .author(author)
                .name("invalid_tag_name").build();

        refCommandFactory.deleteTag(repository, parameters).call();
    }

    @Test
    public void testCreateBranchValidatesRefNameContainsWhitespace() {
        expectedException.expect(InvalidRefNameException.class);
        expectedException.expectMessage("bitbucket.service.repository.refname.whitespace");

        refCommandFactory.createBranch(repository, new CreateBranchCommandParameters.Builder()
                .name("-- malicious")
                .startPoint("HEAD")
                .build());
    }

    @Test
    public void testCreateBranchValidatesRefNameTooLong() {
        expectedException.expect(InvalidRefNameException.class);
        expectedException.expectMessage("bitbucket.git.ref.qualified.name.too.long");

        refCommandFactory.createBranch(repository, new CreateBranchCommandParameters.Builder()
                // qualifying should add extra 11 characters
                .name(StringUtils.repeat("a", 90))
                .startPoint("HEAD")
                .build());
    }

    @Test
    public void testCreateBranchValidatesRefNameGitCheckRefNameFormatFails() {
        expectedException.expect(InvalidRefNameException.class);
        expectedException.expectMessage("bitbucket.service.repository.refname.invalid");

        refCommandFactory.createBranch(repository, new CreateBranchCommandParameters.Builder()
                // .lock suffix is not allowed on ref names because it conflicts with git's ref locking mechanism
                .name("a/wrong/name.lock")
                .startPoint("HEAD")
                .build());
    }

    @Test
    public void testCreateTagValidatesRefNameContainsWhitespace() {
        expectedException.expect(InvalidRefNameException.class);
        expectedException.expectMessage("bitbucket.service.repository.refname.whitespace");

        refCommandFactory.createTag(repository, new CreateTagCommandParameters.Builder()
                .name("-- malicious")
                .startPoint("HEAD")
                .build());
    }

    @Test
    public void testCreateTagValidatesRefNameTooLong() {
        expectedException.expect(InvalidRefNameException.class);
        expectedException.expectMessage("bitbucket.git.ref.qualified.name.too.long");

        refCommandFactory.createTag(repository, new CreateTagCommandParameters.Builder()
                // qualifying should add extra 11 characters
                .name(StringUtils.repeat("a", 90))
                .startPoint("HEAD")
                .build());
    }

    @Test
    public void testCreateTagValidatesRefNameGitCheckRefNameFormatFails() {
        expectedException.expect(InvalidRefNameException.class);
        expectedException.expectMessage("bitbucket.service.repository.refname.invalid");

        refCommandFactory.createTag(repository, new CreateTagCommandParameters.Builder()
                // .lock suffix is not allowed on ref names because it conflicts with git's ref locking mechanism
                .name("a/wrong/name.lock")
                .startPoint("HEAD")
                .build());
    }
}