package com.atlassian.stash.internal.pull.comment.drift;

import com.atlassian.bitbucket.commit.NoSuchCommitException;
import com.atlassian.bitbucket.scm.CommandFailedException;
import com.atlassian.stash.internal.pull.InternalPullRequest;
import com.atlassian.stash.internal.repository.InternalRepository;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.core.Ordered;

import java.util.Arrays;

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

@RunWith(MockitoJUnitRunner.class)
public class CommentDriftStrategyChainTest {

    private CommentDriftStrategyChain chain;
    @Mock
    private DriftContext context;
    @Mock(name = "fallbackStrategy")
    private CommentDriftStrategy fallbackStrategy;
    @Mock(name = "strategyA", extraInterfaces = Ordered.class)
    private CommentDriftStrategy strategyA;
    @Mock(name = "strategyB", extraInterfaces = Ordered.class)
    private CommentDriftStrategy strategyB;

    @Before
    public void setup() {
        InternalRepository repository = mock(InternalRepository.class);
        when(repository.getId()).thenReturn(1);

        InternalPullRequest pullRequest = mock(InternalPullRequest.class);
        when(pullRequest.getGlobalId()).thenReturn(42L);
        when(pullRequest.getScopeRepository()).thenReturn(repository);
        when(pullRequest.getVersion()).thenReturn(2);

        prepareStrategy(strategyA, "A", Ordered.HIGHEST_PRECEDENCE);
        prepareStrategy(strategyB, "B", Ordered.LOWEST_PRECEDENCE);
        when(context.getPullRequest()).thenReturn(pullRequest);
        when(fallbackStrategy.getName()).thenReturn("C");

        //Using Arrays.asList here verifies that the chain uses its own mutable collection to sort strategies.
        //Passing the strategies out of order verifies that the chain sorts them
        chain = new CommentDriftStrategyChain(Arrays.asList(strategyB, strategyA), fallbackStrategy);
    }

    @Test
    public void testApplyTo() {
        chain.applyTo(context);

        InOrder ordered = inOrder(fallbackStrategy, strategyA, strategyB);
        ordered.verify(strategyA).apply(same(context));
        ordered.verify(strategyB).apply(same(context));
        ordered.verify(fallbackStrategy).apply(same(context));

        verify(context, times(2)).isEmpty(); //isEmpty() should be checked after each main strategy
    }

    @Test
    public void testApplyToHandlesNoSuchCommitExceptions() {
        doThrow(NoSuchCommitException.class).when(strategyA).apply(same(context));

        chain.applyTo(context);

        InOrder ordered = inOrder(fallbackStrategy, strategyA);
        ordered.verify(strategyA).apply(same(context));
        ordered.verify(fallbackStrategy).apply(same(context));

        //strategyB should never be run, and isEmpty() should never be checked (since strategyA threw)
        verify(strategyB, never()).apply(any(DriftContext.class));
        verify(context, never()).isEmpty();
    }

    @Test(expected = CommandFailedException.class)
    public void testApplyToPropagatesOtherExceptions() {
        doThrow(CommandFailedException.class).when(strategyB).apply(same(context));

        try {
            chain.applyTo(context);
        } finally {
            InOrder ordered = inOrder(strategyA, strategyB);
            ordered.verify(strategyA).apply(same(context));
            ordered.verify(strategyB).apply(same(context));

            //The exception should be propagated, so the fallback strategy should not be invoked
            verify(fallbackStrategy, never()).apply(any(DriftContext.class));
        }
    }

    @Test
    public void testApplyToShortCircuitsOnEmpty() {
        when(context.isEmpty()).thenReturn(true);

        chain.applyTo(context);

        InOrder ordered = inOrder(context, strategyA);
        ordered.verify(strategyA).apply(same(context));
        ordered.verify(context).isEmpty();

        //Processing should short-circuit before running strategyB or the fallbackStrategy
        verify(strategyB, never()).apply(any(DriftContext.class));
        verify(fallbackStrategy, never()).apply(any(DriftContext.class));
    }

    @Test
    public void testToString() {
        assertEquals("Chain ([A -> B], or C)", chain.toString());
    }

    private static void prepareStrategy(CommentDriftStrategy strategy, String name, int order) {
        when(strategy.getName()).thenReturn(name);
        when(((Ordered) strategy).getOrder()).thenReturn(order);
    }
}