package com.atlassian.stash.internal.repository;

import com.atlassian.plugin.PluginAccessor;
import com.atlassian.stash.internal.plugin.RefMetadataProviderModuleDescriptor;
import com.atlassian.bitbucket.repository.MetadataMap;
import com.atlassian.bitbucket.repository.Ref;
import com.atlassian.bitbucket.repository.RefMetadataContext;
import com.atlassian.bitbucket.repository.RefMetadataProvider;
import com.google.common.base.Functions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.SettableFuture;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;

import javax.annotation.Nonnull;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@SuppressWarnings("unchecked")
@RunWith(MockitoJUnitRunner.class)
public class PluginRefMetadataMapProviderTest {

    @Mock
    private ExecutorService executorService;
    @Mock
    private PluginAccessor pluginAccessor;
    private PluginRefMetadataMapProvider metadataProvider;

    @Before
    public void setUp() throws Exception {
        metadataProvider = new PluginRefMetadataMapProvider(executorService, pluginAccessor);

        when(executorService.invokeAll(anyCollection(), anyLong(), isA(TimeUnit.class))).thenAnswer(new Answer<List<Future>>() {
            @Override
            public List<Future> answer(InvocationOnMock invocation) throws Throwable {
                Collection<Callable> callables = (Collection<Callable>) invocation.getArguments()[0];
                List<Future> futures = Lists.newArrayListWithCapacity(callables.size());
                for (Callable callable : callables) {
                    SettableFuture settableFuture = SettableFuture.create();
                    try {
                        settableFuture.set(callable.call());
                    } catch (Throwable e) {
                        settableFuture.setException(e);
                    }
                    futures.add(settableFuture);
                }

                return futures;
            }
        });
    }

    @Test
    public void testGetPromisedRefMetadataHappyPath() throws Exception {
        mockModuleDescriptors(
                mockModuleDescriptor("blah", new RefMetadataProvider<Ref>() {
                    @Nonnull
                    @Override
                    public Map<Ref, Ref> getMetadata(@Nonnull RefMetadataContext context) {
                        return Maps.uniqueIndex(context.getRefs(), Functions.<Ref>identity());
                    }
                })
        );

        RefMetadataContext context = mock(RefMetadataContext.class);
        Ref ref = mock(Ref.class);
        when(context.getRefs()).thenReturn(Sets.newHashSet(ref));
        Map<Ref, MetadataMap> metadata = metadataProvider.getMetadata(context);
        assertNotNull(metadata);
        assertEquals(1, metadata.size());
        assertEquals(metadata.keySet().iterator().next(), ref);
        assertEquals(Collections.<String, Object>singletonMap("blah", ref), metadata.values().iterator().next().asMap());
    }

    @Test
    public void testGetPromisedRefMetadataWhenSyncProviderReturnsNull() throws Exception {
        mockModuleDescriptors(
                mockModuleDescriptor("blah", new RefMetadataProvider<Object>() {
                    @SuppressWarnings("ConstantConditions")
                    @Nonnull
                    @Override
                    public Map<Ref, Object> getMetadata(@Nonnull RefMetadataContext context) {
                        return null;
                    }
                })
        );

        Map<Ref, ?> metadata = metadataProvider.getMetadata(mock(RefMetadataContext.class));
        assertEquals(Collections.<Ref, Object>emptyMap(), metadata);
    }

    @Test
    public void testGetPromisedRefMetadataWhenProviderReturnsMapWithNulls() throws Exception {
        mockModuleDescriptors(
                mockModuleDescriptor("blah", new RefMetadataProvider<Object>() {
                    @Nonnull
                    @Override
                    public Map<Ref, Object> getMetadata(@Nonnull RefMetadataContext context) {
                        Map<Ref, Object> metadata = Maps.newHashMap();
                        metadata.put(null, "Blah");
                        metadata.put(context.getRefs().iterator().next(), null);
                        return metadata;
                    }
                })
        );

        RefMetadataContext context = mock(RefMetadataContext.class);
        when(context.getRefs()).thenReturn(Collections.singleton(mock(Ref.class)));

        Map<Ref, ?> metadata = metadataProvider.getMetadata(context);
        assertEquals(Collections.<Ref, Object>emptyMap(), metadata);
    }

    @Test
    public void testGetPromisedRefMetadataWhenProviderThrowsException() throws Exception {
        RefMetadataProvider provider = mock(RefMetadataProvider.class);
        when(provider.getMetadata(isA(RefMetadataContext.class))).thenThrow(new RuntimeException());
        mockModuleDescriptors(
                mockModuleDescriptor("blah", provider)
        );

        Map<Ref, ?> metadata = metadataProvider.getMetadata(mock(RefMetadataContext.class));
        assertEquals(Collections.<Ref, Object>emptyMap(), metadata);
    }

    private void mockModuleDescriptors(RefMetadataProviderModuleDescriptor... descriptors) {
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(RefMetadataProviderModuleDescriptor.class))
                .thenReturn(Arrays.asList(descriptors));
    }

    private RefMetadataProviderModuleDescriptor mockModuleDescriptor(String completeModuleKey, RefMetadataProvider provider) {
        RefMetadataProviderModuleDescriptor descriptor = mock(RefMetadataProviderModuleDescriptor.class);
        when(descriptor.getCompleteKey()).thenReturn(completeModuleKey);
        when(descriptor.getModule()).thenReturn(provider);
        return descriptor;
    }

}
