package com.atlassian.stash.internal.repository.sync.command;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import com.atlassian.bitbucket.scm.CommandFailedException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.mockito.MockitoUtils;
import com.atlassian.bitbucket.repository.Branch;
import com.atlassian.bitbucket.repository.RefChange;
import com.atlassian.bitbucket.repository.RefChangeType;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.sync.RejectedRefState;
import com.atlassian.bitbucket.scm.CommandErrorHandler;
import com.atlassian.bitbucket.scm.CommandExitHandler;
import com.atlassian.bitbucket.scm.CommandOutputHandler;
import com.atlassian.bitbucket.scm.git.GitAgent;
import com.atlassian.bitbucket.scm.git.command.GitCommand;
import com.atlassian.bitbucket.scm.git.GitRefPattern;
import com.atlassian.bitbucket.scm.git.GitUtils;
import com.atlassian.bitbucket.scm.git.command.fetch.GitFetchBuilder;
import com.atlassian.utils.process.OutputHandler;
import com.atlassian.utils.process.ProcessException;
import com.atlassian.utils.process.Watchdog;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class FetchCommandHandlerTest {

    @Mock
    private GitAgent agent;
    @Mock
    private GitFetchBuilder builder;
    @Mock
    private FetchCallback callback;
    @Mock
    private GitCommand<Void> command;
    private CommandErrorHandler errorHandler;
    private CommandExitHandler exitHandler;
    @InjectMocks
    private FetchCommandHandler handler;
    @Spy
    private I18nService i18nService = new SimpleI18nService(SimpleI18nService.Mode.RETURN_KEYS);
    private CommandOutputHandler<Void> outputHandler;
    @Mock
    private Repository repository;

    @Before
    @SuppressWarnings("unchecked")
    public void setup() {
        ArgumentCaptor<CommandErrorHandler> errorHandlerCaptor = ArgumentCaptor.forClass(CommandErrorHandler.class);
        ArgumentCaptor<CommandExitHandler> exitHandlerCaptor = ArgumentCaptor.forClass(CommandExitHandler.class);
        ArgumentCaptor<CommandOutputHandler> outputHandlerCaptor = ArgumentCaptor.forClass(CommandOutputHandler.class);
        Watchdog watchdog = mock(Watchdog.class);

        doAnswer(MockitoUtils.returnsSelf()).when(builder).errorHandler(errorHandlerCaptor.capture());
        doAnswer(MockitoUtils.returnsSelf()).when(builder).exitHandler(exitHandlerCaptor.capture());
        doReturn(command).when(builder).build(outputHandlerCaptor.capture());

        doAnswer(new Answer() {

            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                return GitRefPattern.HEADS.qualify((String) invocation.getArguments()[0]);
            }
        }).when(agent).qualifyBranch(anyString());

        assertSame(command, handler.build(builder));
        errorHandler = errorHandlerCaptor.getValue();
        exitHandler = exitHandlerCaptor.getValue();
        outputHandler = outputHandlerCaptor.getValue();

        //The Watchdog must be available or the output handlers fail with NullPointerExceptions
        errorHandler.setWatchdog(watchdog);
        outputHandler.setWatchdog(watchdog);
    }

    @Test
    public void test() throws Exception {
        doAnswer(new Answer() {

            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                Branch branch = mock(Branch.class);
                when(branch.getLatestCommit()).thenReturn((String) invocation.getArguments()[1]);
                return branch;
            }
        }).when(agent).resolveBranch(same(repository), anyString(), eq(true));

        runWith("From ../1\n" +
                " * [new branch]      release-2.9.0-m1 -> release-2.9.0-m1\n" +
                " * [new branch]      release-2.9.0-temp -> release-2.9.0-temp\n" +
                "   53d59ba..787bbae  release/2.5 -> release/2.5\n" +
                "   e3ae4dc..b79fb5c  release/2.6 -> release/2.6\n" +
                "   d74908f..ee2cc82  release/2.7 -> release/2.7\n" +
                "   518f1ca..ac78ba0  release/2.8 -> release/2.8\n" +
                "   7c08e3a..33b4052  release/2.9 -> release/2.9\n");

        ArgumentCaptor<RefChange> changeCaptor = ArgumentCaptor.forClass(RefChange.class);
        verify(callback, times(7)).onChanged(changeCaptor.capture());
        verifyNoMoreInteractions(callback);

        List<RefChange> changes = changeCaptor.getAllValues();
        assertAdd(changes.get(0), "release-2.9.0-m1");
        assertAdd(changes.get(1), "release-2.9.0-temp");
        assertUpdate(changes.get(2), "release/2.5", "53d59ba", "787bbae");
        assertUpdate(changes.get(3), "release/2.6", "e3ae4dc", "b79fb5c");
        assertUpdate(changes.get(4), "release/2.7", "d74908f", "ee2cc82");
        assertUpdate(changes.get(5), "release/2.8", "518f1ca", "ac78ba0");
        assertUpdate(changes.get(6), "release/2.9", "7c08e3a", "33b4052");
    }

    @Test
    public void testLockErrorWithUnixPath() throws Exception {
        failWithLock("fatal: Unable to create '/opt/j2ee/domains/atlassian.com/stash-prod/home/data/repositories/554/refs/heads/master.lock': File exists.\n" +
                "\n" +
                "If no other git process is currently running, this probably means a\n" +
                "git process crashed in this repository earlier. Make sure no other git\n" +
                "process is running and remove the file manually to continue.\n" +
                "fatal: The remote end hung up unexpectedly\n");
    }

    @Test
    public void testLockErrorWithWindowsPath() throws Exception {
        failWithLock("fatal: Unable to create 'C:/Temp/repo/.git/refs/heads/master.lock': File exists.\n" +
                "\n" +
                "If no other git process is currently running, this probably means a\n" +
                "git process crashed in this repository earlier. Make sure no other git\n" +
                "process is running and remove the file manually to continue.\n" +
                "fatal: The remote end hung up unexpectedly\n");
    }

    @Test
    public void testRemoteRefNotFound() throws Exception {
        ILoggingEvent event = failWithLogging("fatal: Couldn't find remote ref refs/heads/ROTP-2308-horde-login-redirect\n" +
                "fatal: The remote end hung up unexpectedly");
        assertSame(Level.WARN, event.getLevel());
        assertEquals("{}: Refs could not be synchronized; {} no longer exists", event.getMessage());
        assertEquals(2, event.getArgumentArray().length);
        assertSame(repository, event.getArgumentArray()[0]);
        assertEquals("refs/heads/ROTP-2308-horde-login-redirect", event.getArgumentArray()[1]);
    }

    @Test
    public void testRejectedRef() throws Exception {
        //No exception should be thrown despite the "failed" command
        failWith("From ..\\1\n" +
                " ! [rejected]        master     -> master  (non-fast-forward)\n");

        verify(callback).onRejected(eq("refs/heads/master"), same(RejectedRefState.DIVERGED));
    }

    @Test(expected = CommandFailedException.class)
    public void testStdErrIsPropagated() throws Exception {
        String output = "fatal: protocol error: impossibly long line\n" +
                "fatal: Could not read from remote repository.\n" +
                "\n" +
                "Please make sure you have the correct access rights";

        try {
            failWith(output);
        } catch (ProcessException e) {
            //The key used is verified as the message. The default message is not verified because we don't care. The
            //vararg is verified because it's how we can tell if the stderr contents are being swallowed or propagated.
            verify(i18nService.createKeyedMessage(anyString(), eq("'git fetch' exited with code 128 saying: " + output)));

            assertEquals("bitbucket.service.externalprocess.exception", e.getMessage());
        }
    }

    @Test(expected = IllegalStateException.class)
    public void testUnexpectedChangeType() throws Exception {
        runWith("From ../1\n" +
                " x [deleted]         (none)     -> STASHDEV-5357-branch-list-help");
    }

    @Test(expected = IllegalStateException.class)
    public void testUnresolvableAddRef() throws Exception {
        try {
            runWith("From ..\\1\n" +
                    " * [new branch]      BUILDENG-4115_release -> BUILDENG-4115_release\n");
        } finally {
            //Verify that the handler attempted to resolve the branch
            verify(agent).resolveBranch(same(repository), eq("refs/heads/BUILDENG-4115_release"), eq(true));
        }
    }

    private static void assertAdd(RefChange change, String refId) {
        assertRefChange(change, refId, GitUtils.NULL_SHA1, GitRefPattern.HEADS.qualify(refId), RefChangeType.ADD);
    }

    private static void assertRefChange(RefChange change, String refId, String from, String to, RefChangeType type) {
        assertEquals(GitRefPattern.HEADS.qualify(refId), change.getRefId());
        assertEquals(from, change.getFromHash());
        assertEquals(to, change.getToHash());
        assertSame(type, change.getType());
    }

    private static void assertUpdate(RefChange change, String refId, String from, String to) {
        assertRefChange(change, refId, from, to, RefChangeType.UPDATE);
    }

    private static void process(OutputHandler handler, String output) throws ProcessException {
        handler.process(new ByteArrayInputStream(output.getBytes()));
    }

    private void failWith(String output) throws ProcessException {
        process(errorHandler, output);
        exitHandler.onExit("git fetch", 128, "", new ProcessException("Non-zero exit code: 128"));
    }

    private void failWithLock(String output) throws ProcessException {
        ILoggingEvent event = failWithLogging(output);
        assertSame(Level.WARN, event.getLevel());
        assertEquals("{}: Refs could not be synchronized; {} is locked by another process", event.getMessage());
        assertEquals(2, event.getArgumentArray().length);
        assertSame(repository, event.getArgumentArray()[0]);
        assertEquals("refs/heads/master", event.getArgumentArray()[1]);
    }

    private ILoggingEvent failWithLogging(String output) throws ProcessException {
        CapturingAppender appender = new CapturingAppender();

        //Add an appender so we can validate that the handler produces the logging we want. Ensure
        //its level is set high enough to actually append the message
        Logger log = (Logger) LoggerFactory.getLogger(FetchCommandHandler.class);
        Level level = log.getLevel();

        log.addAppender(appender);
        log.setLevel(Level.WARN);
        try {
            failWith(output);
        } finally {
            //Restore the previous level and remove our appender
            log.setLevel(level);
            log.detachAppender(appender);
        }

        return appender.event;
    }

    private void runWith(String output) throws ProcessException {
        process(outputHandler, output);
        exitHandler.onExit("git fetch", 0, "", null);
    }

    private static class CapturingAppender extends AppenderBase<ILoggingEvent> {

        private ILoggingEvent event;

        private CapturingAppender() {
            start(); //Appenders only work when they're started
        }

        @Override
        protected void append(ILoggingEvent event) {
            this.event = event;
        }
    }
}
