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

import com.atlassian.bitbucket.Product;
import com.atlassian.bitbucket.hook.HookResponse;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.repository.MinimalRef;
import com.atlassian.bitbucket.repository.RefChange;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.util.Chainable;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.CharStreams;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;

import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.List;
import java.util.Set;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class PullRequestRefGuardHookTest {

    @InjectMocks
    private PullRequestRefGuardHook hook;
    @Spy
    private I18nService i18nService = new SimpleI18nService();
    @Mock
    private Repository repository;
    @Mock
    private HookResponse response;

    @Test
    public void testOnReceiveAllowsOtherUpdates() {
        List<RefChange> refChanges = Lists.newArrayList(
                refChange("refs/heads/master"),
                refChange("refs/tags/v1.0.0")
        );
        assertTrue("The push should have been allowed", hook.onReceive(repository, refChanges, response));

        verifyZeroInteractions(i18nService);
        verifyZeroInteractions(repository);
        verifyZeroInteractions(response);
    }

    @Test
    public void testOnReceiveRejectsUpdatesToPullRequestRefs() throws Exception {
        List<RefChange> refChanges = Lists.newArrayList(
                refChange("refs/heads/master"),                      //Allowed, just a normal branch
                refChange("refs/notes/commit"),                      //Allowed, not related to pull requests
                refChange("refs/notes/pull-requests"),               //Rejected, required to be a directory
                refChange("refs/notes/pull-requests/1"),             //Rejected, required to be a directory
                refChange("refs/notes/pull-requests/1/description"), //Allowed, not a reserved name
                refChange("refs/pull-requests"),                     //Rejected, required to be a directory
                refChange("refs/pull-requests/1"),                   //Rejected, required to be a directory
                refChange("refs/pull-requests/1/proposal"),          //Allowed, not a reserved name
                refChange("refs/tags/v1.0.0")                        //Allowed, just a normal tag
        );
        for (String reservedName : PullRequestRefGuardHook.RESERVED_NAMES) {
            refChanges.add(refChange("refs/notes/pull-requests/1/" + reservedName)); //Rejected, reserved name
            refChanges.add(refChange("refs/pull-requests/1/" + reservedName));       //Rejected, reserved name
        }

        StringWriter writer = new StringWriter();

        when(response.err()).thenReturn(new PrintWriter(writer));

        assertFalse("The push should have been rejected", hook.onReceive(repository, refChanges, response));

        verify(i18nService).getMessage(eq("bitbucket.scm.git.pull.refreserved"), eq(Product.NAME));
        verify(response).err();
        verify(response, never()).out();
        verifyZeroInteractions(repository);

        //After verifying the interactions, verify that the message contains the expected refs
        Set<String> refs = Sets.newHashSet(Chainable.chain(CharStreams.readLines(new StringReader(writer.toString())))
                .filter(line -> line.startsWith("\t"))
                .transform(line -> line.trim())
                .toSet());
        assertTrue("The top-level note directory was not rejected", refs.remove("refs/notes/pull-requests"));
        assertTrue("The pull request note directory ws not rejected", refs.remove("refs/notes/pull-requests/1"));
        assertTrue("The top-level pull-requests directory was not rejected", refs.remove("refs/pull-requests"));
        assertTrue("The pull request directory was not rejected", refs.remove("refs/pull-requests/1"));
        for (String reservedName : PullRequestRefGuardHook.RESERVED_NAMES) {
            assertTrue("A reserved note was not rejected", refs.remove("refs/notes/pull-requests/1/" + reservedName));
            assertTrue("A reserved ref was not rejected", refs.remove("refs/pull-requests/1/" + reservedName));
        }
        assertTrue("Unexpected refs were rejected", refs.isEmpty());
    }

    private static RefChange refChange(String refId) {
        RefChange refChange = mock(RefChange.class);
        MinimalRef ref = mock(MinimalRef.class);
        when(refChange.getRef()).thenReturn(ref);
        when(ref.getId()).thenReturn(refId);

        return refChange;
    }
}
