package com.atlassian.stash.internal.comment;

import com.atlassian.plugin.ModuleDescriptor;
import com.atlassian.plugin.PluginAccessor;
import com.atlassian.bitbucket.comment.Comment;
import com.atlassian.bitbucket.comment.CommentPropertyContext;
import com.atlassian.bitbucket.comment.CommentPropertyProvider;
import com.atlassian.bitbucket.comment.FlatteningCommentPropertyProvider;
import com.atlassian.stash.internal.plugin.CommentPropertyProviderDescriptor;
import com.atlassian.stash.internal.pull.InternalPullRequest;
import com.atlassian.bitbucket.property.PropertyMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.hamcrest.Matchers;
import org.junit.Before;
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 javax.annotation.Nonnull;
import java.util.List;

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

/**
 * @see com.atlassian.stash.internal.pull.PluginPullRequestEnricherTest
 */
@RunWith(MockitoJUnitRunner.class)
public class PluginCommentPostProcessorTest {

    @Mock
    private PluginAccessor pluginAccessor;
    @Spy
    private CommentPropertyProvider standardPropertyProvider = new CommentPropertyProvider() {
        @Override
        public void provideProperties(@Nonnull CommentPropertyContext context) {
            for (Comment comment : context) {
                context.setProperty(comment, "standard-property-comment-id", comment.getId());
            }
        }
    };
    @Spy
    private CommentPropertyProvider flatteningPropertyProvider = new FlatteningCommentPropertyProvider() {
        @Override
        protected void doProvideProperties(@Nonnull CommentPropertyContext context) {
            for (Comment comment : context) {
                context.setProperty(comment, "flattened-property-comment-id", comment.getId());
            }
        }
    };

    @Mock
    private ModuleDescriptor<CommentPropertyProvider> standardPropertyProviderDescriptor;
    @Mock
    private ModuleDescriptor<CommentPropertyProvider> flatteningPropertyProviderDescriptor;

    @InjectMocks
    private PluginCommentPostProcessor postProcessor;

    @SuppressWarnings("unchecked")
    @Before
    public void setUp() {
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(CommentPropertyProviderDescriptor.class))
                .thenReturn((List) ImmutableList.of(standardPropertyProviderDescriptor, flatteningPropertyProviderDescriptor));

        when(standardPropertyProviderDescriptor.getModule()).thenReturn(standardPropertyProvider);
        when(flatteningPropertyProviderDescriptor.getModule()).thenReturn(flatteningPropertyProvider);
    }

    @Test
    public void testProcess() {
        InternalPullRequest pullRequest = new InternalPullRequest.Builder().globalId(99L).build();
        InternalComment comment = newComment(1L);
        postProcessor.process(pullRequest, comment);

        assertAllPropertiesProvided(comment);
    }

    @Test
    public void testProcessCommentWithPropertiesAlreadySet() {
        InternalPullRequest pullRequest = new InternalPullRequest.Builder().globalId(99L).build();
        InternalComment comment = newCommentWithProperty(5L, "existing-property", 99);
        postProcessor.process(pullRequest, comment);

        assertPropertiesNotProvided(comment);
    }

    @Test
    public void testProcessAll() {
        InternalPullRequest pullRequest = new InternalPullRequest.Builder().globalId(99L).build();
        InternalComment replyWithProperties = newCommentWithProperty(5L, "existing-property", "value");
        InternalComment comment1 = newCommentWithReplies(1L, replyWithProperties);
        postProcessor.processAll(pullRequest, ImmutableSet.of(comment1));

        // root comments receive all properties
        assertAllPropertiesProvided(comment1);

        // no flattening, replies should not receive from standard provider by default
        assertStandardPropertyNotProvided(replyWithProperties);

        // flattened provider should work for replies, unless they have pre-existing properties
        assertFlattenedPropertyNotProvided(replyWithProperties);
    }

    private static void assertAllPropertiesProvided(Comment comment) {
        assertThat(comment.getProperties().keySet(), hasSize(2));
        assertStandardPropertyProvided(comment);
        assertFlattenedPropertyProvided(comment);
    }

    private static void assertPropertiesNotProvided(Comment comment) {
        assertStandardPropertyNotProvided(comment);
        assertFlattenedPropertyNotProvided(comment);
    }

    private static void assertFlattenedPropertyProvided(Comment comment) {
        assertThat(comment.getProperties(), Matchers.<String, Object>hasEntry("flattened-property-comment-id", comment.getId()));
    }

    private static void assertFlattenedPropertyNotProvided(Comment comment) {
        assertThat(comment.getProperties().keySet(), not(hasItem("flattened-property-comment-id")));
    }

    private static void assertStandardPropertyProvided(Comment comment) {
        assertThat(comment.getProperties(), Matchers.<String, Object>hasEntry("standard-property-comment-id", comment.getId()));
    }

    private static void assertStandardPropertyNotProvided(Comment comment) {
        assertThat(comment.getProperties().keySet(), not(hasItem("standard-property-comment-id")));
    }

    private static InternalComment newComment(long id) {
        InternalComment comment = spy(new InternalComment());
        when(comment.getId()).thenReturn(id);

        return comment;
    }

    private static InternalComment newCommentWithProperty(long id, String key, Object value) {
        InternalComment comment = newComment(id);
        comment.setProperties(new PropertyMap.Builder().property(key, value).build());

        return comment;
    }

    private static InternalComment newCommentWithReplies(long id, Comment... replies) {
        InternalComment comment = newComment(id);
        when(comment.getComments()).thenReturn(ImmutableList.copyOf(replies));

        return comment;
    }
}