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

import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.scm.git.GitAgent;
import com.atlassian.bitbucket.scm.git.GitScm;
import com.atlassian.bitbucket.scm.git.GitScmConfig;
import com.atlassian.bitbucket.scm.git.command.GitCommand;
import com.atlassian.bitbucket.scm.git.command.GitCommandFactory;
import com.atlassian.bitbucket.event.repository.RepositoryDefaultBranchModifiedEvent;
import com.atlassian.bitbucket.event.repository.RepositoryPushEvent;
import com.atlassian.bitbucket.content.NoSuchPathException;
import com.atlassian.bitbucket.ServerException;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.stash.internal.scm.git.command.SimpleGitCommand;
import com.atlassian.bitbucket.io.TypeAwareOutputSupplier;
import com.atlassian.bitbucket.scm.RawFileCommandParameters;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
import org.apache.commons.io.FileUtils;
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.InjectMocks;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;

import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;

import static com.atlassian.bitbucket.repository.RefTestUtils.minimalBranch;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class GitMailmapSupportMockTest {

    private static final String MASTER = "refs/heads/master";
    private static final RefChange REF_CHANGE_MASTER = new SimpleRefChange.Builder()
            .ref(minimalBranch("master")).fromHash("a").toHash("b").type(RefChangeType.UPDATE).build();
    private static final RefChange REF_CHANGE_OTHER = new SimpleRefChange.Builder()
            .ref(minimalBranch("other")).fromHash("a").toHash("b").type(RefChangeType.UPDATE).build();

    @Mock
    private GitAgent agent;
    @Mock
    private GitCommandFactory commandFactory;
    @Mock
    private GitScmConfig config;
    @Mock
    private ExecutorService executor;
    @Rule
    public TemporaryFolder folder = new TemporaryFolder();
    private File mailmapFile;
    @InjectMocks
    private GitMailmapSupport mailmapSupport;
    @Mock
    private Repository repository;

    @Before
    public void setup() {
        when(agent.getHead(eq(repository))).thenReturn(MASTER);
        when(config.getRepositoryDir(eq(repository))).thenReturn(folder.getRoot());
        when(repository.getScmId()).thenReturn(GitScm.ID);
        mailmapFile = new File(folder.getRoot(), ".mailmap");
    }

    @Test
    public void testDefaultBranchChangeUpdatesMailmap() throws IOException {
        mockRawFile("test", null);

        Files.write("old contents", mailmapFile, Charsets.UTF_8);

        mailmapSupport.onDefaultBranchChanged(
                new RepositoryDefaultBranchModifiedEvent(this, repository, "refs/heads/stable"));

        assertEquals("test", Files.toString(mailmapFile, Charsets.UTF_8));
    }

    @Test
    public void testMissingDefaultBranchDeletesMailmapFile() throws IOException {
        // mock the agent throwing a NoDefaultBranchException
        reset(agent);
        NoDefaultBranchException ex = new NoDefaultBranchException(new KeyedMessage("", "", ""), "repo", null);
        when(agent.getHead(eq(repository))).thenThrow(ex);

        // create an empty mailmap file
        FileUtils.touch(mailmapFile);

        mailmapSupport.onRefsChanged(pushEvent(REF_CHANGE_OTHER));

        //verify that the mailmap file has been deleted
        assertFalse(mailmapFile.exists());

        verifyZeroInteractions(commandFactory);
    }

    @Test
    public void testPushDefaultBranchContainsMailmap() throws IOException {
        mockRawFile("test", null);

        mailmapSupport.onRefsChanged(pushEvent(REF_CHANGE_MASTER));

        assertEquals("test", Files.toString(mailmapFile, Charsets.UTF_8));
    }

    @Test
    public void testPushDefaultBranchContainsMailmapOverwritesExisting() throws IOException {
        mockRawFile("test", null);
        Files.write("old contents", mailmapFile, Charsets.UTF_8);

        mailmapSupport.onRefsChanged(pushEvent(REF_CHANGE_MASTER));

        assertEquals("test", Files.toString(mailmapFile, Charsets.UTF_8));
    }

    @Test
    public void testPushDefaultBranchWithoutMailmapDeletesExisting() throws IOException {
        mockRawFile(null, new NoSuchPathException(new KeyedMessage("", "", ""), ".mailmap", "refs/heads/master"));
        Files.write("old contents", mailmapFile, Charsets.UTF_8);

        mailmapSupport.onRefsChanged(pushEvent(REF_CHANGE_MASTER));

        assertFalse(mailmapFile.exists());
    }

    @Test
    public void testPushDefaultBranchGitFails() throws IOException {
        mockRawFile("new contents", new ServerException(new KeyedMessage("", "", "")));
        Files.write("old contents", mailmapFile, Charsets.UTF_8);

        mailmapSupport.onRefsChanged(pushEvent(REF_CHANGE_MASTER, REF_CHANGE_OTHER));

        // verify that the file has not been changed or deleted
        assertEquals("old contents", Files.toString(mailmapFile, Charsets.UTF_8));
    }

    @Test
    public void testPushOtherBranch() {
        mailmapSupport.onRefsChanged(pushEvent(REF_CHANGE_OTHER));

        verifyZeroInteractions(commandFactory);
    }

    private void mockRawFile(final String mailmapContents, final RuntimeException ex) {
        when(commandFactory.rawFile(
                eq(repository), any(RawFileCommandParameters.class), any(TypeAwareOutputSupplier.class)))
                .thenAnswer(new Answer<GitCommand<Void>>() {
                    @Override
                    public GitCommand<Void> answer(final InvocationOnMock invocationOnMock) throws Throwable {
                        return new SimpleGitCommand<Void>(executor) {
                            @Nullable
                            @Override
                            public Void call() {
                                if (ex != null) {
                                    throw ex;
                                }

                                TypeAwareOutputSupplier supplier = (TypeAwareOutputSupplier) invocationOnMock.getArguments()[2];
                                try {
                                    supplier.getStream("text/plain").write(mailmapContents.getBytes(Charsets.UTF_8));
                                } catch (IOException e) {
                                    // ignore
                                }
                                return null;
                            }
                        };
                    }
                });

    }

    private RepositoryPushEvent pushEvent(RefChange... changes) {
        return new RepositoryPushEvent(this, repository, Arrays.asList(changes));
    }

}
