package com.atlassian.bitbucket.mesh.git.builder.catfile;

import com.atlassian.bitbucket.mesh.git.GitObjectType;
import com.atlassian.bitbucket.mesh.git.mailmap.MailmapResolver;
import com.atlassian.bitbucket.mesh.git.ref.AnnotatedTag;
import com.atlassian.bitbucket.mesh.git.ref.AnnotatedTagCallback;
import com.atlassian.bitbucket.mesh.process.OsProcess;
import com.atlassian.bitbucket.mesh.test.MockitoUtils;
import com.atlassian.bitbucket.mesh.util.Person;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.time.Instant;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class CallbackAnnotatedTagBatchCatFileStdioHandlerTest {

    @Mock
    private AnnotatedTagCallback callback;
    @Mock
    private MailmapResolver mailmapResolver;

    @Before
    public void setup() throws IOException {
        when(mailmapResolver.resolve(any())).thenAnswer(MockitoUtils.returnFirst());
    }

    @Test
    public void testAnnotatedTag() {
        processObject("tag", "2bce21df7dedc315ebeb25b05601efd5fd426933",
                "object 0cd8d42b98503333be0ce22a56dcfd6f107be9dd\n" +
                        "type commit\n" +
                        "tag stash-parent-4.0.0-m9\n" +
                        "tagger Stash release (mszczepanski) <mszczepanski@atlassian.com> 1438229529 +0000\n" +
                        "\n" +
                        "[maven-release-plugin] copy for tag stash-parent-4.0.0-m9\n");

        ArgumentCaptor<AnnotatedTag> tagCaptor = ArgumentCaptor.forClass(AnnotatedTag.class);
        verify(callback).onTag(tagCaptor.capture());

        AnnotatedTag tag = tagCaptor.getValue();
        assertEquals("refs/tags/stash-parent-4.0.0-m9", tag.getId());
        assertEquals("[maven-release-plugin] copy for tag stash-parent-4.0.0-m9\n", tag.getMessage().orElse(null));

        Person tagger = tag.getTagger();
        assertNotNull(tagger);
        assertEquals("Stash release (mszczepanski)", tagger.getName());
        assertEquals("mszczepanski@atlassian.com", tagger.getEmail());

        assertTaggerTimestamp(tag);
    }

    @Test
    public void testAnnotatedTagTruncates() {
        processObject(5, "tag", "2bce21df7dedc315ebeb25b05601efd5fd426933",
                "object 0cd8d42b98503333be0ce22a56dcfd6f107be9dd\n" +
                        "type commit\n" +
                        "tag stash-parent-4.0.0-m9\n" +
                        "tagger Stash release (mszczepanski) <mszczepanski@atlassian.com> 1438229529 +0000\n" +
                        "\n" +
                        "[maven-release-plugin] copy for tag stash-parent-4.0.0-m9\n" +
                        "line 2\n");

        ArgumentCaptor<AnnotatedTag> tagCaptor = ArgumentCaptor.forClass(AnnotatedTag.class);
        verify(callback).onTag(tagCaptor.capture());

        AnnotatedTag tag = tagCaptor.getValue();
        assertEquals("[mave\n", tag.getMessage().orElse(null));
    }

    @Test
    public void testCallsOnMissingForUnknownObject() {
        processObject("missing", "0cd8d42b98503333be0ce22a56dcfd6f107be9dd", null);

        verify(callback).onMissing("0cd8d42b98503333be0ce22a56dcfd6f107be9dd");
    }

    @Test
    public void testCallsOnOtherTypeForCommit() {
        processObject("commit", "0cd8d42b98503333be0ce22a56dcfd6f107be9dd",
                "tree 8fdf971dce58790101d110e53b225aef2bdc0cc0\n" +
                        "parent 8574b84593963046277bcdda723db8671e7e2d57\n" +
                        "author Stash release (mszczepanski) <mszczepanski@atlassian.com> 1438229525 +0000\n" +
                        "committer Stash release (mszczepanski) <mszczepanski@atlassian.com> 1438229525 +0000\n" +
                        "\n" +
                        "[maven-release-plugin] prepare release stash-parent-4.0.0-m9\n");

        verify(callback).onNonTag(eq("0cd8d42b98503333be0ce22a56dcfd6f107be9dd"), same(GitObjectType.COMMIT));
    }

    @Test
    public void testSignedAnnotatedTag() {
        processObject("tag", "2bce21df7dedc315ebeb25b05601efd5fd426933",
                "object 0cd8d42b98503333be0ce22a56dcfd6f107be9dd\n" +
                        "type commit\n" +
                        "tag stash-parent-4.0.0-m9\n" +
                        "tagger Stash release (mszczepanski) <mszczepanski@atlassian.com> 1438229529 +0000\n" +
                        "\n" +
                        "[maven-release-plugin] copy for tag stash-parent-4.0.0-m9\n" +
                        "-----BEGIN PGP SIGNATURE-----\n" +
                        "Version: GnuPG v1\n" +
                        "\n" +
                        "iQEcBAABCAAGBQJW/OhLAAoJEJDLBFvTmUcBKXAH/0i3O/F+YjD8xMknsMZGSa2/\n" +
                        "/uGNnF5SUCxQjztWCJecHmp88GdyagT9rcgv/q6eniElwp3M3dQXBTdJ+tPH+m7G\n" +
                        "yZdrmuLqrn/NTzZKj3E5xMT9IXJ+jg4RsfhALGqnrG5XFtsB5VVucURbEsrqNM+Y\n" +
                        "k5PJPQD4jroT/jOOWBysQMlJRNVZGYhtCC2DkRPQo8lII8/KW5mGu/GJzpQepW4K\n" +
                        "vnqd6h9vwhTddzQ+EosNGscQvQBM4+CtLznK3iCYEnDe111wCtMm/ukxd7378/tj\n" +
                        "O+mdC0Q+mxTOgIHcgZKBFzVosxiSHVXo7cvmGgk8kuONdaGo2D0k0PqceZPOjRw=\n" +
                        "=4ZAu\n" +
                        "-----END PGP SIGNATURE-----\n");

        ArgumentCaptor<AnnotatedTag> tagCaptor = ArgumentCaptor.forClass(AnnotatedTag.class);
        verify(callback).onTag(tagCaptor.capture());

        AnnotatedTag tag = tagCaptor.getValue();
        assertEquals("refs/tags/stash-parent-4.0.0-m9", tag.getId());
        assertEquals("[maven-release-plugin] copy for tag stash-parent-4.0.0-m9\n", tag.getMessage().orElse(null));

        Person tagger = tag.getTagger();
        assertNotNull(tagger);
        assertEquals("Stash release (mszczepanski)", tagger.getName());
        assertEquals("mszczepanski@atlassian.com", tagger.getEmail());

        assertTaggerTimestamp(tag);
    }

    private static void assertTaggerTimestamp(AnnotatedTag tag) {
        Instant taggerTimestamp = tag.getTaggerTimestamp();
        Instant expectedTimestamp = Instant.ofEpochMilli(TimeUnit.SECONDS.toMillis(1438229529L));
        assertEquals(expectedTimestamp, taggerTimestamp);
    }

    private void processObject(String objectType, String tagHash, String rawObject) {
        processObject(1024, objectType, tagHash, rawObject);
    }

    private void processObject(int maxMessageSize, String objectType, String hash, String rawObject) {
        StringBuilder builder = new StringBuilder()
                .append(hash)
                .append(' ')
                .append(objectType);
        if ("missing".equals(objectType)) {
            //Missing objects don't have any object content
            builder.append('\n');
        } else {
            builder.append(' ')
                    .append(rawObject.length())
                    .append('\n')
                    .append(rawObject)
                    .append('\n');
        }
        CharBuffer output = CharBuffer.wrap(builder.toString());

        CallbackAnnotatedTagBatchCatFileStdioHandler handler = new CallbackAnnotatedTagBatchCatFileStdioHandler(
                Collections.singleton(hash), maxMessageSize, mailmapResolver, callback);
        handler.onPreStart(mock(OsProcess.class));
        handler.onStart(mock(OsProcess.class));

        //Encode the test output in chunks to a 100 byte buffer. This buffer is small enough that some
        //of the test output will require multiple onStdout/onContent calls, but large enough that the
        //BatchHeader always fits within a single buffer (which is required for it to parse correctly)
        ByteBuffer buffer = ByteBuffer.allocate(100);
        CharsetEncoder encoder = UTF_8.newEncoder();
        CoderResult result;
        do {
            result = encoder.encode(output, buffer, true);
            buffer.flip();

            handler.onStdout(buffer, result.isUnderflow());
            buffer.compact();
        } while (result.isOverflow());

        handler.onExit(0);
    }
}