package com.atlassian.bitbucket.util;

import com.atlassian.bitbucket.request.RequestContext;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import org.apache.commons.lang3.mutable.MutableInt;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.Map;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class RequestLocalCacheTest {

    @Mock
    private RequestContext requestContext;

    @SuppressWarnings("ConstantConditions")
    @Test(expected = NullPointerException.class)
    public void testNullCacheKey() {
        new RequestLocalCache<String, String>(requestContext).get(null, wrap(Suppliers.ofInstance("test")));
    }

    @SuppressWarnings("ConstantConditions")
    @Test(expected = NullPointerException.class)
    public void testNullSupplier() {
        new RequestLocalCache<String, String>(requestContext).get("test", null);
    }

    @Test
    public void testActiveRequestContext() {
        when(requestContext.isActive()).thenReturn(true);

        RequestLocalCache<String, String> cache = new RequestLocalCache<>(requestContext);
        MutableInt callCount = new MutableInt();

        assertEquals("foo", cache.get("foo", new CallCountingSupplier("foo", callCount))); // cache miss
        assertEquals("bar", cache.get("bar", new CallCountingSupplier("bar", callCount))); // cache miss
        assertEquals("foo", cache.get("foo", new CallCountingSupplier("baz", callCount))); // cache hit
        assertEquals("bar", cache.get("bar", new CallCountingSupplier("qux", callCount))); // cache hit

        assertEquals(2, callCount.intValue()); // == number of cache misses
        verify(requestContext).addCleanupCallback(any(Runnable.class));
    }

    @Test
    public void testNoActiveRequestContext() {
        when(requestContext.isActive()).thenReturn(false);

        RequestLocalCache<String, String> cache = new RequestLocalCache<>(requestContext);
        MutableInt callCount = new MutableInt();

        // no request context - all calls should go through the loader
        assertEquals("foo", cache.get("foo", new CallCountingSupplier("foo", callCount)));
        assertEquals("bar", cache.get("bar", new CallCountingSupplier("bar", callCount)));
        assertEquals("baz", cache.get("foo", new CallCountingSupplier("baz", callCount)));
        assertEquals("qux", cache.get("bar", new CallCountingSupplier("qux", callCount)));

        assertEquals(4, callCount.intValue()); // == number of cache misses
        verify(requestContext, never()).addCleanupCallback(any(Runnable.class));
    }

    @Test
    public void testAsMapActiveRequestContext() {
        when(requestContext.isActive()).thenReturn(true);

        RequestLocalCache<String, String> cache = new RequestLocalCache<>(requestContext);
        MutableInt callCount = new MutableInt();

        Map<String, String> rawCache = cache.asMap();
        assertThat(rawCache, not(nullValue()));
        rawCache.put("foo", "foo");

        assertEquals("foo", cache.get("foo", new CallCountingSupplier("foo", callCount)));
        assertEquals("bar", cache.get("bar", new CallCountingSupplier("bar", callCount)));
        assertEquals("foo", cache.get("foo", new CallCountingSupplier("foo", callCount)));
        assertEquals("bar", cache.get("bar", new CallCountingSupplier("bar", callCount)));

        rawCache = cache.asMap();
        assertThat(rawCache, not(nullValue()));
        assertEquals("foo", rawCache.get("foo"));
        assertEquals("bar", rawCache.get("bar"));

        assertEquals(1, callCount.intValue()); // == number of cache misses
        verify(requestContext).addCleanupCallback(any(Runnable.class));
    }

    @Test
    public void testAsMapNoActiveRequestContext() {
        when(requestContext.isActive()).thenReturn(false);

        RequestLocalCache<String, String> cache = new RequestLocalCache<>(requestContext);
        MutableInt callCount = new MutableInt();

        Map<String, String> rawCache = cache.asMap();
        assertThat(rawCache, is(nullValue()));

        assertEquals("foo", cache.get("foo", new CallCountingSupplier("foo", callCount)));
        assertEquals("bar", cache.get("bar", new CallCountingSupplier("bar", callCount)));
        assertEquals("baz", cache.get("foo", new CallCountingSupplier("baz", callCount)));
        assertEquals("qux", cache.get("bar", new CallCountingSupplier("qux", callCount)));

        rawCache = cache.asMap();
        assertThat(rawCache, is(nullValue()));

        assertEquals(4, callCount.intValue()); // == number of cache misses
        verify(requestContext, never()).addCleanupCallback(any(Runnable.class));
    }

    @Test
    public void testAsMapUseAfterRequest() {
        when(requestContext.isActive()).thenReturn(true);

        RequestLocalCache<String, String> cache = new RequestLocalCache<>(requestContext);
        MutableInt callCount = new MutableInt();

        assertEquals("foo", cache.get("foo", new CallCountingSupplier("foo", callCount)));

        Map<String, String> rawCache = cache.asMap();
        assertThat(rawCache, hasEntry("foo", "foo"));

        when(requestContext.isActive()).thenReturn(false);

        try {
            rawCache.get("foo");
            fail("Exception expected");
        } catch (IllegalStateException ignored) {
            // not using @Test(expected = IllegalStateException) because multiple lines in here could throw that exception.
        }
    }

    private static <T> com.atlassian.cache.Supplier<T> wrap(final Supplier<T> guavaSupplier) {
        return new com.atlassian.cache.Supplier<T>() {
            @Override
            public T get() {
                return guavaSupplier.get();
            }
        };
    }

    private static class CallCountingSupplier implements com.atlassian.cache.Supplier<String> {

        private final String value;
        private final MutableInt callCount;

        private CallCountingSupplier(String value, MutableInt callCount) {
            this.value = value;
            this.callCount = callCount;
        }

        @Override
        public String get() {
            callCount.increment();
            return value;
        }
    }
}
