package com.atlassian.stash.internal.web.util.captcha;

import com.google.common.collect.Sets;
import com.hazelcast.core.IMap;
import com.hazelcast.map.EntryProcessor;
import com.octo.captcha.Captcha;
import com.octo.captcha.service.captchastore.CaptchaAndLocale;
import com.octo.captcha.service.captchastore.CaptchaStore;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

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

@SuppressWarnings("deprecation")
public class HazelcastCaptchaStoreTest {

    private CaptchaStore store;

    @Before
    public void setup() {
        store = new HazelcastCaptchaStore(mockIMap(String.class, CaptchaAndLocale.class));
    }

    @Test
    public void testClearAndShutdown() {
        store.storeCaptcha("test", createCaptcha());
        store.cleanAndShutdown();
        assertTrue(store.getKeys().isEmpty());
    }

    @Test
    public void testEmpty() {
        store.storeCaptcha("test", createCaptcha());
        store.empty();
        assertTrue(store.getKeys().isEmpty());
    }

    @Test
    public void testGetCaptcha() {
        Captcha captcha1 = createCaptcha();
        Captcha captcha2 = createCaptcha();
        store.storeCaptcha("test1", captcha1);
        store.storeCaptcha("test2", captcha2);

        assertSame(captcha1, store.getCaptcha("test1"));
        assertSame(captcha2, store.getCaptcha("test2"));
        assertNull(store.getCaptcha("other"));
    }

    @Test
    public void testGetCaptchaNull() {
        assertNull(store.getCaptcha(null));
    }

    @Test
    public void testGetCaptchaUnknown() {
        assertNull(store.getCaptcha("unknown"));
    }

    @Test
    public void testGetKeys() {
        assertTrue(store.getKeys().isEmpty());

        // add 1 item
        store.storeCaptcha("test1", createCaptcha());
        assertEquals(Sets.newHashSet("test1"), Sets.newHashSet(store.getKeys()));

        // another
        store.storeCaptcha("test1", createCaptcha());
        store.storeCaptcha("test2", createCaptcha());

        Collection keys = store.getKeys();
        assertEquals(2, keys.size());
        assertEquals(Sets.newHashSet("test1", "test2"), Sets.newHashSet(keys));

        // remove an item
        store.removeCaptcha("test1");
        assertEquals(Sets.newHashSet("test2"), Sets.newHashSet(store.getKeys()));
    }

    @Test
    public void testGetLocaleUnknown() {
        assertNull(store.getLocale("unknown"));
    }

    @Test
    public void testGetLocaleDefault() {
        store.storeCaptcha("test", createCaptcha());

        assertNull(store.getLocale("test"));
    }

    @Test
    public void testGetLocale() {
        store.storeCaptcha("test", createCaptcha(), Locale.CANADA_FRENCH);
        assertEquals(Locale.CANADA_FRENCH, store.getLocale("test"));
    }

    @Test
    public void testStoreCaptcha() {
        Captcha captcha = createCaptcha();
        store.storeCaptcha("id", captcha);

        assertSame(captcha, store.getCaptcha("id"));
        assertNull(store.getLocale("id"));
    }

    @Test
    public void testStoreCaptchaAndLocale() {
        Captcha captcha = createCaptcha();
        Locale locale = Locale.CHINESE;
        store.storeCaptcha("id", captcha, locale);

        assertSame(captcha, store.getCaptcha("id"));
        assertSame(Locale.CHINESE, store.getLocale("id"));
    }

    protected Captcha createCaptcha() {
        return mock(Captcha.class);
    }

    @SuppressWarnings("unchecked")
    private <K, V> IMap<K, V> mockIMap(Class<K> keyClass, final Class<V> valueClass) {
        final Map<K, V> backingMap = new HashMap<>();
        IMap<K, V> mockMap = mock(IMap.class, new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                //HashMap doesn't support set; translate it to put
                String methodName = invocation.getMethod().getName();
                if ("set".equals(methodName)) {
                    methodName = "put";
                }

                try {
                    return backingMap.getClass().getMethod(methodName, invocation.getMethod().getParameterTypes())
                            .invoke(backingMap, invocation.getArguments());
                } catch (NoSuchMethodException e) {
                    return null;
                }
            }
        });
        when(mockMap.executeOnKey(any(keyClass), any(EntryProcessor.class))).thenAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                final K key = (K) invocation.getArguments()[0];
                EntryProcessor<K, V> processor = (EntryProcessor) invocation.getArguments()[1];

                Map.Entry<K, V> entry = mock(Map.Entry.class);
                when(entry.getKey()).thenReturn(key);
                when(entry.getValue()).thenReturn(backingMap.get(key));
                when(entry.setValue(any(valueClass))).thenAnswer(new Answer<Object>() {
                    @Override
                    public Object answer(InvocationOnMock invocation) throws Throwable {
                        V value = (V) invocation.getArguments()[0];
                        if (value == null) {
                            return backingMap.remove(key);
                        }
                        return backingMap.put(key, value);
                    }
                });
                return processor.process(entry);
            }
        });
        return mockMap;
    }
}