package com.atlassian.stash.internal.plugin;

import com.atlassian.bitbucket.transaction.SimpleSpringTransactionTemplate;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.commons.lang3.StringUtils;
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 org.springframework.transaction.support.TransactionTemplate;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultPluginSettingsTest {

    private final String pluginKey = "plugin.key";

    @Mock
    private PluginSettingDao dao;
    private final TransactionTemplate readOnlyTransaction = new SimpleSpringTransactionTemplate();
    private final TransactionTemplate readWriteTransaction = new SimpleSpringTransactionTemplate();
    private DefaultPluginSettings pluginSettings;

    @Before
    public void setUp() throws Exception {
        pluginSettings = new DefaultPluginSettings(dao, pluginKey, readOnlyTransaction, readWriteTransaction);
    }

    @Test
    public void testGet() throws Exception {
        when(dao.get(pluginKey, "key")).thenReturn("value");
        assertEquals("value", pluginSettings.get("key"));
    }

    @Test
    public void testGetWithLongKeyShouldSucceed() {
        pluginSettings.get(StringUtils.repeat("a", 100));
        pluginSettings.get(StringUtils.repeat("a", 101));
        pluginSettings.get(StringUtils.repeat("a", 255));
        pluginSettings.get(StringUtils.repeat("a", 256));
    }

    @Test
    public void testLocalModificationOfListDoesNotAffectCache() {
        // mock the backing DAO as a map to let the serialization and deserialization logic do its thing
        mockDaoBackedByMap();

        // add some data
        List<String> list = Lists.newArrayList("one", "two", "three");
        pluginSettings.put("test-list", list);

        // modify the original values and verify that the cached values are not affected
        list.add("another-value");

        //noinspection unchecked
        List<String> retrievedList = (List<String>) pluginSettings.get("test-list");
        assertEquals(Lists.newArrayList("one", "two", "three"), retrievedList);

        // now modify the retrieved list and verify again
        retrievedList.remove(0);
        assertEquals(Lists.newArrayList("one", "two", "three"), pluginSettings.get("test-list"));
    }

    @Test
    public void testLocalModificationOfMapDoesNotAffectCache() {
        // mock the backing DAO as a map to let the serialization and deserialization logic do its thing
        mockDaoBackedByMap();

        Map<String, String> expected = Maps.newHashMap();
        expected.put("one", "value1");
        expected.put("two", "value2");

        Map<String, String> map = Maps.newHashMap(expected);
        pluginSettings.put("test-map", map);

        // modify the map
        map.put("one", "changed!");
        map.put("three", "value3");

        //noinspection unchecked
        Map<String, String> retrieved = (Map<String, String>) pluginSettings.get("test-map");
        assertEquals(expected, retrieved);

        // now modify retrieved and verify again
        retrieved.put("one", "changed again");
        retrieved.remove("two");

        assertEquals(expected, pluginSettings.get("test-map"));
    }

    @Test
    public void testLocalModificationOfPropertiesDoesNotAffectCache() {
        // mock the backing DAO as a map to let the serialization and deserialization logic do its thing
        mockDaoBackedByMap();

        Properties expected = new Properties();
        expected.setProperty("one", "value1");
        expected.setProperty("two", "value2");

        Properties properties = new Properties();
        properties.putAll(expected);

        pluginSettings.put("test-properties", properties);

        // modify the stored value and verify that the cached value is unaffected
        properties.setProperty("one", "changed as well");
        properties.setProperty("three", "value3");

        Properties retrieved = (Properties) pluginSettings.get("test-properties");
        assertEquals(expected, retrieved);

        // modify the retrieved value and verify again
        retrieved.remove("one");
        retrieved.setProperty("new property", "will be lost");

        assertEquals(expected, pluginSettings.get("test-properties"));
    }

    @Test
    public void testPut() throws Exception {
        pluginSettings.put("key", "value");
        verify(dao).put(pluginKey, "key", "value");
    }

    @Test
    public void testPutInvalidatesCache() throws Exception {
        mockDaoBackedByMap();
        pluginSettings.put("key", "first");

        assertEquals("first", pluginSettings.get("key"));

        assertEquals("first", pluginSettings.put("key", "new-value"));

        // verify that the modified value really is there
        assertEquals("new-value", pluginSettings.get("key"));
    }

    @Test
    public void testPutWithBlankValueRemoves() throws Exception {
        pluginSettings.put("key", "");
        // This intentionally removes the key, see BSERVDEV-3029 and BSERVDEV-11124.
        verify(dao).remove(pluginKey, "key");
    }

    @Test
    public void testRemove() throws Exception {
        when(dao.get(pluginKey, "key")).thenReturn("oldValue");

        assertEquals("oldValue", pluginSettings.remove("key"));
        verify(dao).remove(pluginKey, "key");
    }

    @Test
    public void testRemoveInvalidatesCache() throws Exception {
        mockDaoBackedByMap();
        pluginSettings.put("key", "first");

        assertEquals("first", pluginSettings.get("key"));

        assertEquals("first", pluginSettings.remove("key"));

        // verify that the removed value really is gone
        assertNull(pluginSettings.get("key"));
    }

    @Test
    public void testRemoveWithLongKeyShouldSucceed() {
        pluginSettings.remove(StringUtils.repeat("a", 100));
        pluginSettings.remove(StringUtils.repeat("a", 101));
        pluginSettings.remove(StringUtils.repeat("a", 255));
        pluginSettings.remove(StringUtils.repeat("a", 256));
    }

    private void mockDaoBackedByMap() {
        final Map<PluginSettingKey, String> persistedSettings = new HashMap<>();
        when(dao.get(anyString(), anyString())).thenAnswer(new Answer<String>() {
            @Override
            public String answer(InvocationOnMock invocation) throws Throwable {
                String namespace = (String) invocation.getArguments()[0];
                String key = (String) invocation.getArguments()[1];
                return persistedSettings.get(new PluginSettingKey(namespace, key));
            }
        });
        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                String namespace = (String) invocation.getArguments()[0];
                String key = (String) invocation.getArguments()[1];
                String value = (String) invocation.getArguments()[2];
                persistedSettings.put(new PluginSettingKey(namespace, key), value);
                return null;
            }
        }).when(dao).put(anyString(), anyString(), anyString());
        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                String namespace = (String) invocation.getArguments()[0];
                String key = (String) invocation.getArguments()[1];
                persistedSettings.remove(new PluginSettingKey(namespace, key));
                return null;
            }
        }).when(dao).remove(anyString(), anyString());
    }
}
