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

import com.atlassian.bitbucket.mesh.git.commit.Commit;
import com.atlassian.bitbucket.mesh.process.handler.AbstractLineStdioHandler;
import com.atlassian.bitbucket.mesh.util.Person;
import com.google.common.io.Resources;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;

import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.*;

@SuppressWarnings("ConstantConditions")
public class CommitPumpTest {

    private final CommitPumpSettings defaultPumpSettings = new CommitPumpSettings.Builder().build();

    @Test
    public void testReadCommits() throws Exception {
        try (InputStream stdout = Resources.getResource(getClass(), "git-rev-list-commits.out").openStream()) {
            List<Commit> commits = pumpOutput(stdout, defaultPumpSettings);

            Commit commit = commits.get(0);
            assertEquals("fef569b2640eef9d249e8269ea2a8775977e3e3f", commit.getId());
            assertEquals("NONE: add unit test for deleting a repository on disk\n", commit.getMessage());
            assertEquals("Geoff Crain", commit.getAuthor().getName());
            assertEquals("gcrain@atlassian.com", commit.getAuthor().getEmail());
            assertEquals(1333431906000L, commit.getAuthorTimestamp());
            assertEquals("Bryan Turner", commit.getCommitter().getName());
            assertEquals("bturner@atlassian.com", commit.getCommitter().getEmail());
            assertEquals(1490316481000L, commit.getCommitterTimestamp());

            commit = commits.get(1);
            assertEquals("6513a414cc8e448acd969f2be16e6d23027026ba", commit.getId());
            assertEquals("STASH-1880: Adding avatars to the user listing\n", commit.getMessage());
            assertEquals("Jason Hinch", commit.getAuthor().getName());
            assertEquals("jhinch@atlassian.com", commit.getAuthor().getEmail());
            assertEquals(1333431893000L, commit.getAuthorTimestamp());
            assertEquals("Bryan Turner", commit.getCommitter().getName());
            assertEquals("bturner@atlassian.com", commit.getCommitter().getEmail());
            assertEquals(1490318419000L, commit.getCommitterTimestamp());

            commit = commits.get(2);
            assertEquals("c7091688131181886020204cd1e8a4c8a4815928", commit.getId());
            assertEquals("Merge branch 'master' of https://stash-dev.atlassian.com/git/STASH/stash\n", commit.getMessage());
            assertEquals("Jason Hinch", commit.getAuthor().getName());
            assertEquals("jhinch@atlassian.com", commit.getAuthor().getEmail());
            assertEquals(1333430608000L, commit.getAuthorTimestamp());
            assertEquals("Bryan Turner", commit.getCommitter().getName());
            assertEquals("bturner@atlassian.com", commit.getCommitter().getEmail());
            assertEquals(1490318566000L, commit.getCommitterTimestamp());
        }
    }

    @Test(expected = IllegalStateException.class)
    public void testDoesNotAcceptRandomStringsOnceCommitIsDetected() throws Exception {
        InputStream stdout = new ByteArrayInputStream(("commit 792c33efefdfc42c2b004c7a260111b68929d9f2\n" +
                "792c33efefdfc42c2b004c7a260111b68929d9f2f34ada17ae69b9a4aafdd41b8de759a2b66b9e07Jonathan Pohjpoh@atlassian.com1328657785Jonathan Pohjpoh@atlassian.com1328657785\n" +
                "commit with two files of the same name (in folders of the same name)\n" +
                "END\n" +
                "Hello darkness my old friend...").getBytes(StandardCharsets.UTF_8));

        pumpOutput(stdout, defaultPumpSettings);
    }

    @Test
    public void testCommitThatHasMessageStartingWithCommitWord() throws Exception {
        String stdout = "commit 792c33efefdfc42c2b004c7a260111b68929d9f2\n" +
                "792c33efefdfc42c2b004c7a260111b68929d9f2f34ada17ae69b9a4aafdd41b8de759a2b66b9e07Jonathan Pohjpoh@atlassian.com1328657785Jonathan Pohjpoh@atlassian.com1328657785\n" +
                "commit with two files of the same name (in folders of the same name)\n" +
                "END";

        Commit commit = pumpSingle(stdout, defaultPumpSettings);

        assertThat(commit.getMessage(), is("commit with two files of the same name (in folders of the same name)"));
    }

    @Test
    public void testRequestsTruncationOfTheFirstLineOfMessagePart() throws Exception {
        ByteArrayInputStream stdout = new ByteArrayInputStream((
                "commit fef569b2640eef9d249e8269ea2a8775977e3e3f\n" +
                        "fef569b2640eef9d249e8269ea2a8775977e3e3f6513a414cc8e448acd969f2be16e6d23027026baGeoff Craingcrain@atlassian.com1333431906Bryan Turnerbturner@atlassian.com1490316481\n" +
                        "NONE: add unit test for deleting a repository on disk\n" +
                        "\n" +
                        "END").getBytes(StandardCharsets.UTF_8));

        CommitPumpSettings settings = new CommitPumpSettings.Builder()
                .withMessages(true)
                .maxMessageLength(6) // it important for this number to be bigger then OBJECT_END length, so we can distinguish between two.
                .build();

        CommitStdioHandler handler = new CommitStdioHandler(settings);
        List<Commit> commits = pumpOutput(stdout, handler);

        Commit commit = commits.get(0);

        // For given commit and settings we expect CommitPump request message lengths:
        // - first, we don't expect a message but a commit object, so to disable truncation request - Integer.MAX_VALUE
        // - second, as soon as we've read the commit object, we expect the message, request truncation - 6.
        // - third, we're still reading the message but we've already fulfilled the request, so request lines to be truncated to OBJECT_END - 5
        // - fourth, we're done, next should be commit object header, so disable truncation - Integer.MAX_VALUE
        assertThat(handler.getRequestedMessageLengths(), contains(Integer.MAX_VALUE, 6, 5, Integer.MAX_VALUE));
        assertThat(commit.getMessage(), is("NONE: "));
    }

    @Test
    public void testLongCommitMessageIsTruncated() throws Exception {
        String stdout = "commit 1234567890abcdef1234567890abcdef12345678\n" +
                "1234567890abcdef1234567890abcdef12345678\u0002abcdef1234567890abcdef1234567890abcdef12\u0002\u0002\u0002\u0002\u0002\u0002\n" +
                "Commit message\n\u0003END\u0004";

        CommitPumpSettings settings = new CommitPumpSettings.Builder()
                .withMessages(true)
                .maxMessageLength(3)
                .build();

        List<Commit> commits = pumpOutput(stdout, settings);

        assertEquals("Com", commits.get(0).getMessage());
    }

    @Test
    public void testLongMessageAcceptedByDefault() throws Exception {
        String stdout = "commit 1234567890abcdef1234567890abcdef12345678\n" +
                "1234567890abcdef1234567890abcdef12345678\u0002abcdef1234567890abcdef1234567890abcdef12\u0002\u0002\u0002\u0002\u0002\u0002\n" +
                "Commit message that is slightly longer\n and a new line\n\u0003END\u0004";

        CommitPumpSettings settings = new CommitPumpSettings.Builder()
                .withMessages(true)
                .build();

        List<Commit> commits = pumpOutput(stdout, settings);

        assertEquals("Commit message that is slightly longer\n and a new line",
                commits.get(0).getMessage());
    }

    @Test(expected = IllegalStateException.class)
    public void testReadCommitMissingCommit() throws Exception {
        pumpOutput("tag v1.1.0", defaultPumpSettings);
    }

    @Test
    public void testReadCommitWithEmptyTokens() throws Exception {
        String stdout = "commit 1234567890abcdef1234567890abcdef12345678\n" +
                "1234567890abcdef1234567890abcdef12345678\u0002abcdef1234567890abcdef1234567890abcdef12\u0002\u0002\u0002\u0002\u0002\u0002\n" +
                "Commit message\n\u0003END\u0004";

        Commit commit = pumpSingle(stdout, defaultPumpSettings);

        assertNotNull(commit);
        assertEquals("1234567890abcdef1234567890abcdef12345678", commit.getId());
        assertEquals("Commit message", commit.getMessage());

        long authorTimestamp = commit.getAuthorTimestamp();
        assertEquals(0L, authorTimestamp);

        Person author = commit.getAuthor();
        assertNotNull(author);
        assertEquals("Unspecified", author.getName());
        assertNull(author.getEmail());

        Collection<String> parents = commit.getParents();
        assertNotNull(parents);
        assertEquals(1, parents.size());
    }

    @Test
    public void testReadCommitWithoutMessage() throws Exception {
        String stdout = "commit 1234567890abcdef1234567890abcdef12345678\n" +
                "1234567890abcdef1234567890abcdef12345678\u0002abcdef1234567890abcdef1234567890abcdef12\u0002\u0002\u0002\u0002\u0002\u0002";

        List<Commit> commits = pumpOutput(stdout, new CommitPumpSettings.Builder()
                .withMessages(false)
                .build());
        Commit commit = commits.get(0);

        assertNotNull(commit);
        assertEquals("1234567890abcdef1234567890abcdef12345678", commit.getId());
        assertNull("Commit message", commit.getMessage());

        long authorTimestamp = commit.getAuthorTimestamp();
        assertEquals(0L, authorTimestamp);

        Person author = commit.getAuthor();
        assertNotNull(author);
        assertEquals("Unspecified", author.getName());
        assertNull(author.getEmail());

        Collection<String> parents = commit.getParents();
        assertNotNull(parents);
        assertEquals(1, parents.size());
    }

    @Test
    public void testReadCommitWithUsernameBar() throws Exception {
        String stdout = "commit 1234567890abcdef1234567890abcdef12345678\n" +
                "1234567890abcdef1234567890abcdef12345678\u0002abcdef1234567890abcdef1234567890abcdef12\u0002abc|xyz\u0002\u0002\u0002\u0002\u0002\n" +
                "Commit message\n\u0003END\u0004";

        Commit commit = pumpSingle(stdout, defaultPumpSettings);
        Person author = commit.getAuthor();
        assertEquals("abc|xyz", author.getName());
    }

    @Test
    public void testReadCommitWithNoMoreInput() throws Exception {
        Commit commit = pumpSingle("", defaultPumpSettings);
        assertNull(commit);
    }

    @Test(expected = IllegalStateException.class)
    public void testReadCommitWithWrongTokenCount() throws Exception {
        String stdout = "commit 1234567890abcdef1234567890abcdef12345678\n" +
                "1234567890abcdef1234567890abcdef12345678\u00021234567\u0002\u0002";

        pumpSingle(stdout, defaultPumpSettings);
    }

    @Test
    public void testTruncationHandlesMultipleLines() throws Exception {
        String stdout = "commit 1234567890abcdef1234567890abcdef12345678\n" +
                "1234567890abcdef1234567890abcdef12345678\u0002abcdef1234567890abcdef1234567890abcdef12\u0002\u0002\u0002\u0002\u0002\u0002\n" +
                "Commit message\nMore message\n\u0003END\u0004";

        CommitPumpSettings settings = new CommitPumpSettings.Builder()
                .withMessages(true)
                .maxMessageLength(15)
                .build();

        Commit commit = pumpSingle(stdout, settings);

        assertEquals("Commit message", commit.getMessage());
    }

    @Test
    public void testTruncationReadsFullEndMarker() throws Exception {
        String stdout = "commit 1234567890abcdef1234567890abcdef12345678\n" +
                "1234567890abcdef1234567890abcdef12345678\u0002abcdef1234567890abcdef1234567890abcdef12\u0002\u0002\u0002\u0002\u0002\u0002\n" +
                "Commit message\n\u0003END\u0004";

        CommitPumpSettings settings = new CommitPumpSettings.Builder()
                .withMessages(true)
                .maxMessageLength(15)
                .build();

        assertEquals("Commit message", pumpSingle(stdout, settings).getMessage());
    }

    @Test
    public void testHugeCommitMessageIsTruncatedEfficiently() throws Exception {
        String stdout = "commit 1234567890abcdef1234567890abcdef12345678\n" +
                "1234567890abcdef1234567890abcdef12345678\u0002abcdef1234567890abcdef1234567890abcdef12\u0002\u0002\u0002\u0002\u0002\u0002\n" +
                "Commit message\n" + StringUtils.repeat("a\n", 1000000) + "\n\u0003END\u0004";

        CommitPumpSettings settings = new CommitPumpSettings.Builder()
                .withMessages(true)
                .maxMessageLength(3)
                .build();

        List<Commit> commits = pumpOutput(stdout, settings);

        assertEquals("Com", commits.get(0).getMessage());
    }

    private static Commit pumpSingle(String stdout, CommitPumpSettings settings) throws Exception {
        List<Commit> commits = pumpOutput(stdout, settings);

        return commits.isEmpty() ? null : commits.get(0);
    }

    private static List<Commit> pumpOutput(String stdout, CommitPumpSettings settings) throws Exception {
        return pumpOutput(new ByteArrayInputStream(stdout.getBytes(StandardCharsets.UTF_8)), settings);
    }

    private static List<Commit> pumpOutput(InputStream stdout, CommitPumpSettings settings) throws Exception {
        CommitStdioHandler handler = new CommitStdioHandler(settings);
        
        return pumpOutput(stdout, handler);
    }
    
    public static List<Commit> pumpOutput(InputStream stdout, CommitStdioHandler handler) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(128);
        int read = 0;
        while (read != -1) {
            read = stdout.read(buffer.array(), buffer.arrayOffset(), buffer.remaining());

            if (read > 0) {
                buffer.limit(read);
                handler.onStdout(buffer, false);
            } else if (read < 0) {
                buffer.flip();
                handler.onStdout(buffer, true);
            }

            buffer.clear();
        }

        return handler.getResult();
    }

    private static class CommitStdioHandler extends AbstractLineStdioHandler<List<Commit>> implements CommitSink {

        private final List<Commit> commits;
        private final CommitPump pump;
        private final List<Integer> requestedMessageLengths;

        CommitStdioHandler(CommitPumpSettings settings) {
            super(StandardCharsets.UTF_8);

            commits = new ArrayList<>();
            pump = new CommitPump(settings, this);
            requestedMessageLengths = new ArrayList<>();
            //Add the line length AbstractLineStdioHandler was initialized with. We use this in
            //testRequestTruncationOfTheFirstLineMessagePart()
            requestedMessageLengths.add(getMaxLineLength());
        }

        public List<Integer> getRequestedMessageLengths() {
            if (requestedMessageLengths == null) {
                return Collections.emptyList();
            }

            return Collections.unmodifiableList(requestedMessageLengths);
        }

        @Override
        public List<Commit> getResult() {
            return commits;
        }
        
        @Override
        public void onCommit(@Nonnull Commit commit) {
            commits.add(commit);
        }

        @Override
        public void onPartialMessage(int maxLength) {
            setMaxLineLength(maxLength);
        }

        @Override
        protected void setMaxLineLength(int maxLineLength) {
            super.setMaxLineLength(maxLineLength);
            requestedMessageLengths.add(maxLineLength);
        }

        @Override
        protected boolean onStdout(@Nonnull String line, boolean truncated) {
            pump.onStdout(line);

            return true;
        }
    }
}
