package com.atlassian.bitbucket.mesh.git.pull;

import com.atlassian.bitbucket.mesh.repository.Repository;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream;

import static com.atlassian.bitbucket.mesh.git.pull.PullRequestRescopeOutcome.*;
import static java.util.Optional.ofNullable;
import static org.junit.Assert.*;

@RunWith(MockitoJUnitRunner.Silent.class)
public class BatchPullRequestRescopeAnalyzerTest {

    private static final List<String> EMPTY = Collections.emptyList();

    @Mock
    private Repository repository;
    @Captor
    private ArgumentCaptor<List<String>> commitsAddedCaptor;
    @Captor
    private ArgumentCaptor<List<String>> commitsRemovedCaptor;
    @Captor
    private ArgumentCaptor<String> mergeHashCaptor;
    @Captor
    private ArgumentCaptor<Integer> totalAddedCaptor;
    @Captor
    private ArgumentCaptor<Integer> totalRemovedCaptor;

    @Test
    public void testNoOutcomeNoCallback() {
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "d", null, "e");

        assertUnknown(analyzer);
    }

    @Test
    public void testAddCommitToFromBranch() {
        //  a --- b --- d   <- to [d]
        //         \
        //           -- c --- e   <- from [c->e]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "d", "e", "d");

        assertFalse(analyzer.onCommit("e", list("c")));

        assertUpdate(analyzer, list("e"), 1, EMPTY, 0);
    }

    @Test
    public void testAddCommitToToBranch() {
        //  a --- b --- d --- e   <- to [d->e]
        //         \
        //           -- c   <- from [c]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "d", "c", "e");

        assertFalse(analyzer.onCommit("e", list("d")));

        assertUpdate(analyzer, EMPTY, 0, EMPTY, 0);
    }

    // verifies that pull requests are rescoped correctly when processed in bulk
    @Test
    public void testBulkRescoping() {
        // a --- b --- e --- f         master [e->f]
        //  \     \
        //   \      -- d --- g --- i   branchA [d -> g -> i]
        //    \         \         /
        //     \          -- h --      branchB [h -> deleted]
        //       c --- j               branchC [c -> j]
        //
        // PR1 : branchA -> master
        // PR2 : branchB -> branchA
        // PR3 : branchC -> master
        //
        // ref-changes:
        // 1. branchA: d -> g
        // 2. master:  e -> f
        // 3. branchC: c -> j
        // 4. branchA: g -> i && branchB deleted
        PullRequestRescope rescope1 = rescope(1L, "d", "e", "g", "e");  // PR1, ref-change 1
        PullRequestRescope rescope2 = rescope(2L, "h", "d", "h", "g");  // PR2, ref-change 1
        PullRequestRescope rescope3 = rescope(1L, "g", "e", "g", "f");  // PR1, ref-change 2
        PullRequestRescope rescope4 = rescope(3L, "c", "e", "c", "f");  // PR3, ref-change 2
        PullRequestRescope rescope5 = rescope(3L, "c", "f", "j", "f");  // PR3, ref-change 3
        PullRequestRescope rescope6 = rescope(1L, "g", "f", "i", "f");  // PR1, ref-change 4
        PullRequestRescope rescope7 = rescope(2L, "h", "g", null, "i"); // PR2, ref-change 4
        BatchPullRequestRescopeAnalyzer
                analyzer = new BatchPullRequestRescopeAnalyzer.Builder(5)
                .rescope(rescope1)
                .rescope(rescope2)
                .rescope(rescope3)
                .rescope(rescope4)
                .rescope(rescope5)
                .rescope(rescope6)
                .rescope(rescope7)
                .build();

        assertTrue(analyzer.onCommit("i", list("g", "h")));
        assertTrue(analyzer.onCommit("j", list("c")));
        assertTrue(analyzer.onCommit("g", list("d")));
        assertTrue(analyzer.onCommit("f", list("e")));
        assertFalse(analyzer.onCommit("h", list("d")));

        analyzer.close();

        List<PullRequestRescopeAnalysisResult> results = analyzer.getResults();
        assertUpdate(find(results, rescope1), list("g"), 1, list(), 0); // rescope1: update [add: g], [remove: -]
        assertUpdate(find(results, rescope2), list(), 0, list(), 0); // rescope2: update [add: -], [remove: -]
        assertUpdate(find(results, rescope3), list(), 0, list(), 0); // rescope3: update [add: -], [remove: -]
        assertUpdate(find(results, rescope4), list(), 0, list(), 0); // rescope4: update [add: -], [remove: -]
        assertUpdate(find(results, rescope5), list("j"), 1, list(), 0); // rescope5: update [add: j], [remove: -]
        assertUpdate(find(results, rescope6), list("i", "h"), 2, list(), 0); // rescope6: update [add: i, h], [remove: -]
        assertMerge(find(results, rescope7), "i"); // rescope7: merge  [hash: i]
    }

    @Test
    public void testCommitsAddedAndRemovedAreLimited() {
        //              -- c01 --- c02 - .... - c10
        //            /
        // a00 --- a01
        //     \
        //      -- b01 --- b02 - .... - b10
        //
        // PR rescope from [b10 -> a1] to [c10 -> a1] (from-branch rebased on top of a1)
        //
        BatchPullRequestRescopeAnalyzer analyzer = analyzer(1L, "b10", "a01", "c10", "a01", 5);

        for (int i = 10; i > 1; --i) {
            String num = StringUtils.leftPad(Integer.toString(i), 2, '0');
            String parentNum = StringUtils.leftPad(Integer.toString(i - 1), 2, '0');
            assertTrue(analyzer.onCommit("b" + num, list("b" + parentNum)));
            assertTrue(analyzer.onCommit("c" + num, list("c" + parentNum)));
        }
        assertTrue(analyzer.onCommit("c01", list("a01")));
        assertTrue(analyzer.onCommit("b01", list("a00")));
        assertFalse(analyzer.onCommit("a01", list("a00")));

        assertUpdate(analyzer, list("c10", "c09", "c08", "c07", "c06"), 10, list("b10", "b09", "b08", "b07", "b06"), 10);
    }

    @Test
    public void testDeleteFromBranchToBranchChanged() {
        //  a --- b --- d --- e   <- to [d->e]
        //         \
        //           -- c   <- from [c->X]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "d", null, "e");

        assertTrue(analyzer.onCommit("e", list("d")));
        assertTrue(analyzer.onCommit("d", list("b")));
        assertFalse(analyzer.onCommit("c", list("b")));

        assertDecline(analyzer);
    }

    @Test
    public void testDeleteFromBranchToBranchUnchanged() {
        //  a --- b --- d   <- to [d]
        //         \
        //           -- c   <- from [c->X]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "d", null, "d");

        assertFalse(analyzer.onCommit("d", list("b")));

        assertDecline(analyzer);
    }

    @Test
    public void testDeleteFromBranchToBranchUnknown() {
        //           -- e
        //         /
        //  a --- b --- d   <- to [d->e]
        //         \
        //           -- c   <- from [c->X]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "d", null, "e");

        assertTrue(analyzer.onCommit("e", list("b")));
        assertTrue(analyzer.onCommit("b", list("a")));

        assertUnknown(analyzer);
    }

    @Test
    public void testDeleteToBranchFromChanged() {
        //  a --- b --- d   <- to [d->X]
        //         \
        //           -- c --- e  <- from [c->e]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "d", "e", null);

        assertFalse(analyzer.onCommit("e", list("c")));

        assertDecline(analyzer);
    }

    @Test
    public void testDeleteToBranchFromUnchanged() {
        //  a --- b --- d   <- to [d->X]
        //         \
        //           -- c   <- from [c]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "d", "c", null);

        assertFalse(analyzer.onCommit("c", list("b")));

        assertDecline(analyzer);
    }

    @Test
    public void testMergeRemotely() {
        //  a --- b --- d --- e   <- to [d->e]
        //         \         /
        //           -- c --   <- from [c]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "d", "c", "e");

        assertFalse(analyzer.onCommit("e", list("d", "c")));

        assertMerge(analyzer, "e");
    }

    @Test
    public void testMergeAndDeleteFromBranch() {
        //  a --- b --- d --- e --- f   <- to [d->f]
        //         \         /
        //           -- c --   <- from [c->X]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "d", null, "f");

        assertTrue(analyzer.onCommit("f", list("e")));
        assertFalse(analyzer.onCommit("e", list("d", "c")));

        // the merge hash detection is minimal at the moment. It cannot deal with complex cases just yet
        assertMerge(analyzer, null);
    }

    @Test
    public void testMergeToBranchIntoFromBranch() {
        //     -- e --- f
        //    /           \
        //  a --- b --- d --- g       <- to [d->g]
        //         \            \
        //           -- c -------- h     <- from [c->h]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "d", "h", "g");

        assertTrue(analyzer.onCommit("h", list("g", "c")));
        assertTrue(analyzer.onCommit("g", list("d", "f")));
        assertTrue(analyzer.onCommit("f", list("e")));
        assertTrue(analyzer.onCommit("e", list("a")));
        assertTrue(analyzer.onCommit("d", list("b")));
        assertTrue(analyzer.onCommit("c", list("b")));
        assertFalse(analyzer.onCommit("b", list("a")));

        assertUpdate(analyzer, list("h"), 1, EMPTY, 0);
    }

    @Test
    public void testRebaseFromBranch() {
        //  a --- b ------------- d    <- to [d]
        //         \               \
        //           -- c --- e      f <- from [e->f]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("e", "d", "f", "d");

        assertTrue(analyzer.onCommit("f", list("d")));
        assertTrue(analyzer.onCommit("e", list("c")));
        assertTrue(analyzer.onCommit("d", list("b")));
        assertFalse(analyzer.onCommit("c", list("b")));

        assertUpdate(analyzer, list("f"), 1, list("e", "c"), 2);
    }

    @Test
    public void testRebaseToBranch() {
        //            -------- f
        //          /
        //  a --- b --- d    <- to [d->f]
        //         \
        //           -- c --- e     <- from [e]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("e", "d", "e", "f");

        assertTrue(analyzer.onCommit("f", list("b")));
        assertTrue(analyzer.onCommit("e", list("c")));
        assertFalse(analyzer.onCommit("d", list("b")));

        assertUpdate(analyzer, EMPTY, 0, EMPTY, 0);
    }

    @Test
    public void testRemoveAllCommitsFromFromBranch() {
        //  a --- b --- d   <- to [d]
        //         \
        //           -- c   <- from [c -> b]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "d", "b", "d");

        assertFalse(analyzer.onCommit("d", list("b")));

        assertDecline(analyzer);
    }

    @Test
    public void testResetFromBranchToToBranch() {
        //  a --- b --- d   <- to [d]
        //         \
        //           -- c   <- from [c -> d]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "d", "d", "d");

        assertTrue(analyzer.onCommit("c", list("b")));
        assertFalse(analyzer.onCommit("d", list("b")));

        assertDecline(analyzer);
    }

    // This scenario is known to be imperfect, but for now we'll accept it because it is unlikely and complex to fix
    // definitely
    @Test
    public void testResetFromBranchToToBranchWithChangeToToBranch() {
        //  a --- b --- d --- e   <- to [d -> e]
        //         \
        //           -- c         <- from [c -> d]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "d", "d", "e");

        assertFalse(analyzer.onCommit("e", list("d")));

        assertMerge(analyzer, null);
    }

    @Test
    public void testResetFromBranchToToBranchEvenSimpler() {
        //  a --- b         <- to [b]
        //         \
        //           -- c   <- from [c -> b]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "b", "b", "b");

        assertFalse(analyzer.onCommit("c", list("b")));

        assertDecline(analyzer);
    }

    @Test
    public void testRemoveCommitFromFromBranch() {
        //  a --- b --- d   <- to [d]
        //         \
        //           -- c --- e   <- from [e->c]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("e", "d", "c", "d");

        assertTrue(analyzer.onCommit("e", list("c")));
        assertTrue(analyzer.onCommit("d", list("b")));
        assertFalse(analyzer.onCommit("c", list("b")));

        assertUpdate(analyzer, EMPTY, 0, list("e"), 1);
    }

    @Test
    public void testRemoveCommitFromToBranch() {
        //  a --- b --- d --- e  <- to [e->d]
        //         \
        //           -- c    <- from [c]
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("c", "e", "c", "d");

        assertFalse(analyzer.onCommit("e", list("d")));

        assertUpdate(analyzer, EMPTY, 0, EMPTY, 0);
    }

    @Test
    public void testComplexExample() {
        //          -- d --- f                    - 'master' starts at a
        //         /     /     \                  - 'feature1' branches off at b
        //    -- c --- e --- g --- h              - 'feature1' is updated with 'j'
        //   /          \            \            - 'feature2' branches off at j
        // a --- b --------- i --------- n        - 'feature1' is updated with 'l'
        //         \                 /            - another branch is merged into 'master' producing 'i'
        //          -- j --- l --- m --- o --- p  - octopus merge of 'feature1' and another branch into 'master', producing 'n'
        //               \                 /      - add 'o' to 'feature1'
        //                -- k ------------       - 'feature2' is merged into 'feature1' producing 'p'
        //
        // Example is for rescope of feature1@l --> master@b to feature1@p --> master@n
        //
        BatchPullRequestRescopeAnalyzer analyzer = analyzer("l", "b", "p", "n");

        assertTrue(analyzer.onCommit("p", list("o", "k")));
        assertTrue(analyzer.onCommit("o", list("m")));
        assertTrue(analyzer.onCommit("n", list("hi-there", "ii", "mmmm"))); // to verify prefix matching of commit ids
        // pollute with some unrelated commits to verify that they're ignored
        assertTrue(analyzer.onCommit("unrelated", list("u2", "u3")));
        assertTrue(analyzer.onCommit("u3", list("m")));
        assertTrue(analyzer.onCommit("u2", list("a")));

        assertTrue(analyzer.onCommit("m", list("l")));
        assertTrue(analyzer.onCommit("l", list("j")));
        assertTrue(analyzer.onCommit("k", list("j")));
        assertTrue(analyzer.onCommit("j", list("b")));
        assertTrue(analyzer.onCommit("i", list("e", "b")));
        assertTrue(analyzer.onCommit("h", list("g", "f")));
        assertTrue(analyzer.onCommit("g", list("e")));
        assertTrue(analyzer.onCommit("f", list("e", "d")));
        assertTrue(analyzer.onCommit("e", list("c")));
        assertTrue(analyzer.onCommit("d", list("c")));
        assertTrue(analyzer.onCommit("c", list("a")));
        assertFalse(analyzer.onCommit("b", list("a")));

        assertUpdate(analyzer, list("p", "o", "k"), 3, list("l", "j"), 2);
    }

    @Test
    public void testRealScenario1() throws IOException, InterruptedException {
        // tests a real scenario in which the from-branch was rewound to a previous version, thereby stripping all
        // commits from the from-branch
        BatchPullRequestRescopeAnalyzer
                analyzer = new BatchPullRequestRescopeAnalyzer.Builder(5)
            .rescope(rescope(1L, "4dd02e3ad101d494354f559116dd8654580cff5e", "d31b1ee822bd0e9f111b7687894e63d208fbdbc9",
                    "1ff63c85562dfcd4b2f06f5f8e714b5d2b98eaa7", "d31b1ee822bd0e9f111b7687894e63d208fbdbc9"))
            .build();

        streamCommits("rescope/analyzer-t1-commits.out", analyzer);

        assertDecline(analyzer);
    }

    @Test
    public void testRealScenario2() throws IOException {
        // tests a real scenario in which a single commit was added to the from-branch
        BatchPullRequestRescopeAnalyzer analyzer = analyzer(
                "69b9d2235f59afcd70c9d84c5da97c3e0555ae33", "905db610c6c21af8a08e7d930b1d96e708a87c18",
                "9424fe3c2b56d530aaa9bc7dc583622abd1084e2", "905db610c6c21af8a08e7d930b1d96e708a87c18");

        streamCommits("rescope/analyzer-t2-commits.out", analyzer);

        assertUpdate(analyzer, list("9424fe3c2b56d530aaa9bc7dc583622abd1084e2"), 1, list(), 0);
    }

    private static PullRequestRescopeAnalysisResult find(List<PullRequestRescopeAnalysisResult> results, 
                                                         PullRequestRescope rescope) {
        return results.stream()
                .filter(result -> rescope.equals(result.getRescope()))
                .findFirst()
                .orElse(null);
    }

    private static void assertDecline(BatchPullRequestRescopeAnalyzer analyzer) {
        assertDecline(analyzer.getResults().iterator().next());
    }

    private static void assertDecline(PullRequestRescopeAnalysisResult result) {
        assertEquals(DECLINE, result.getOutcome());
    }

    private static void assertMerge(BatchPullRequestRescopeAnalyzer analyzer, String mergeHash) {
        assertMerge(analyzer.getResults().iterator().next(), mergeHash);
    }

    private static void assertMerge(PullRequestRescopeAnalysisResult result, String mergeHash) {
        assertEquals(MERGE, result.getOutcome());
        assertEquals(ofNullable(mergeHash), result.getMergeCommit());
    }

    private static void assertUnknown(BatchPullRequestRescopeAnalyzer analyzer) {
        assertUnknown(getResult(analyzer));
    }

    private static void assertUnknown(PullRequestRescopeAnalysisResult result) {
        assertEquals(UNKNOWN, result.getOutcome());
    }

    private static void assertUpdate(BatchPullRequestRescopeAnalyzer analyzer, List<String> added, int totalAdded,
                                     List<String> removed, int totalRemoved) {
        assertUpdate(getResult(analyzer), added, totalAdded, removed, totalRemoved);
    }

    private static void assertUpdate(PullRequestRescopeAnalysisResult result, List<String> added, int totalAdded,
                              List<String> removed, int totalRemoved) {
        assertNotNull(result);
        assertEquals(UPDATE, result.getOutcome());
        assertEquals(totalAdded, result.getAddedCommitsCount());
        assertEquals(totalRemoved, result.getRemovedCommitsCount());
        assertEquals(added, result.getAddedCommits());
        assertEquals(removed, result.getRemovedCommits());
    }

    private static PullRequestRescopeAnalysisResult getResult(BatchPullRequestRescopeAnalyzer analyzer) {
        return analyzer.getResults().get(0);
    }

    private static List<String> list(String... values) {
        return ImmutableList.<String>builder().add(values).build();
    }

    private BatchPullRequestRescopeAnalyzer analyzer(String oldFrom, String oldTo, String newFrom, String newTo) {
        return analyzer(1L, oldFrom, oldTo, newFrom, newTo, 5);
    }

    private BatchPullRequestRescopeAnalyzer analyzer(long pullRequestId, String oldFrom, String oldTo,
                                                     String newFrom, String newTo) {
        return analyzer(pullRequestId, oldFrom, oldTo, newFrom, newTo, 5);
    }

    private BatchPullRequestRescopeAnalyzer analyzer(long pullRequestId, String oldFrom, String oldTo,
                                                     String newFrom, String newTo, int maxCommits) {
        return new BatchPullRequestRescopeAnalyzer.Builder(maxCommits)
                .rescope(rescope(pullRequestId, oldFrom, oldTo, newFrom, newTo))
                .build();
    }

    private Stream<String> getLines(String commitsTestOutput) {
        return new BufferedReader(new InputStreamReader(getClass().getClassLoader().getResourceAsStream(commitsTestOutput)))
                .lines();
    }

    private PullRequestRescope rescope(long pullRequestId, String oldFrom, String oldTo, String newFrom, String newTo) {
        return new PullRequestRescope.Builder(pullRequestId, repository, repository)
                .oldHashes(oldFrom, oldTo)
                .newHashes(newFrom, newTo)
                .build();
    }
    
    private int streamCommits(String commitsTestOutput, BatchPullRequestRescopeAnalyzer... allAnalyzers) {
        List<BatchPullRequestRescopeAnalyzer> analyzers = Lists.newArrayList(allAnalyzers);
        int count = 0;
        try (Stream<String> lines = getLines(commitsTestOutput)) {
            Iterator<String> line = lines.iterator();
            while (line.hasNext()) {
                String[] parts = line.next().split(" ");
                String[] parents = new String[parts.length - 1];
                System.arraycopy(parts, 1, parents, 0, parents.length);
                ++count;
                analyzers.removeIf(analyzer -> !analyzer.onCommit(parts[0], list(parents)));
                if (analyzers.isEmpty()) {
                    break;
                }
            }
        }
        return count;
    }
}