/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.brooklyn.core.internal.storage.impl;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;

import java.util.AbstractMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.brooklyn.core.internal.storage.BrooklynStorage;
import org.apache.brooklyn.core.internal.storage.DataGrid;
import org.apache.brooklyn.core.internal.storage.Reference;
import org.apache.brooklyn.core.internal.storage.impl.BrooklynStorageImpl;
import org.apache.brooklyn.core.internal.storage.impl.inmemory.InmemoryDatagrid;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;

public class BrooklynStorageImplTest {
    
    private DataGrid datagrid;
    private BrooklynStorage storage;

    @BeforeMethod(alwaysRun=true)
    public void setUp() throws Exception {
        // TODO Note that InmemoryDatagrid's ConcurrentMap currently returns snapshot for entrySet() and values()
        // so the tests here aren't particularly good for confirming it'll work against a real datagrid...
        datagrid = new InmemoryDatagrid();
        storage = new BrooklynStorageImpl(datagrid);
    }

    @Test
    public void testReferenceGetAndSet() throws Exception {
        Reference<Object> ref = storage.getReference("mykey");
        assertNull(ref.get());
        assertTrue(ref.isNull());
        assertFalse(ref.contains("different"));
        assertTrue(ref.contains(null));
        
        ref.set("myval");
        assertEqualsCommutative(ref.get(), storage.getReference("mykey").get(), "myval");
        assertFalse(ref.isNull());
        assertFalse(ref.contains("different"));
        assertTrue(ref.contains("myval"));
        
        ref.clear();
        assertNull(ref.get());
    }
    
    @Test
    public void testReferenceAcceptsNullValue() throws Exception {
        Reference<Object> ref = storage.getReference("mykey");
        ref.set(null);
        assertNull(ref.get());
        assertTrue(ref.isNull());
        assertTrue(ref.contains(null));
    }
    
    @Test
    public void testCreateMapReturnsSameEachTime() throws Exception {
        storage.getMap("mykey").put("k1", "v1");
        assertEqualsCommutative(storage.getMap("mykey"), ImmutableMap.of("k1", "v1"));
    }
    
    @Test
    public void testMapOperations() throws Exception {
        Map<Object, Object> map = storage.getMap("mykey");
        
        map.put("k1", "v1");
        assertEqualsCommutative(map, storage.getMap("mykey"), ImmutableMap.of("k1", "v1"));
        assertEqualsCommutative(map.keySet(), storage.getMap("mykey").keySet(), ImmutableSet.of("k1"));
        assertEqualsCommutative(ImmutableList.copyOf(map.values()), ImmutableList.copyOf(storage.getMap("mykey").values()), ImmutableList.of("v1"));
        
        assertEquals(map.size(), 1);
        assertEquals(map.get("k1"), "v1");
        assertTrue(map.containsKey("k1"));
        assertTrue(map.containsValue("v1"));
        assertFalse(map.containsKey("notthere"));
        assertFalse(map.containsValue("notthere"));
        assertFalse(map.isEmpty());
        
        assertEqualsCommutative(map, storage.getMap("mykey"), ImmutableMap.of("k1", "v1"));
        
        map.put("k1", "v2");
        assertEquals(map.get("k1"), "v2");
        assertEquals(map.size(), 1);
        assertEqualsCommutative(map, storage.getMap("mykey"), ImmutableMap.of("k1", "v2"));
        
        map.remove("k1");
        assertTrue(map.isEmpty());
        assertEquals(map.size(), 0);
        assertEquals(map.get("k1"), null);
        assertEqualsCommutative(map, storage.getMap("mykey"), ImmutableMap.of());
        
        map.putAll(ImmutableMap.of("k1", "v3", "k2", "v4"));
        assertEqualsCommutative(map, storage.getMap("mykey"), ImmutableMap.of("k1", "v3", "k2", "v4"));
        
        map.clear();
        assertEquals(map.size(), 0);
        assertEqualsCommutative(map, storage.getMap("mykey"), ImmutableMap.of());
    }
    
    // TODO InmemoryDatagrid's map.entrySet() returns an immutable snapshot
    // Want to test against a real datagrid instead.
    @Test(enabled=false)
    public void testMapEntrySetIterator() throws Exception {
        Map<Object,Object> map = storage.getMap("mykey");
        map.put("k1", "v1");
        assertEquals(iteratorToList(map.entrySet().iterator()), ImmutableList.of(newMapEntry("k1", "v1")));
        
        // Remove entry while iterating; will use snapshot so still contain k1
        Iterator<Map.Entry<Object, Object>> iter1 = map.entrySet().iterator();
        assertTrue(iter1.hasNext());
        map.remove("k1");
        assertEquals(iteratorToList(iter1), ImmutableList.of(newMapEntry("k1", "v1")));
        
        // iter.remove removes value
        map.clear();
        map.put("k1", "v1"); map.put("k2", "v2");
        Iterator<Map.Entry<Object, Object>> iter2 = map.entrySet().iterator();
        assertEquals(newMapEntry("k1", "v1"), iter2.next());
        iter2.remove();
        assertEquals(newMapEntry("k2", "v2"), iter2.next());
        assertEqualsCommutative(map, storage.getMap("mykey"), ImmutableMap.of("k2", "v2"));
        
        // iter.remove when value has already been removed
        map.clear();
        map.put("k1", "v1"); map.put("k2", "v2");
        Iterator<Map.Entry<Object, Object>> iter3 = map.entrySet().iterator();
        assertEquals(newMapEntry("k1", "v1"), iter3.next());
        map.remove("k1");
        iter3.remove();
        assertEquals(newMapEntry("k2", "v2"), iter3.next());
        assertEqualsCommutative(map, storage.getMap("mykey"), ImmutableMap.of("k2", "v2"));
        
        // iter.remove when value has already been removed, but was then re-added!
        // TODO is this really the desired behaviour?
        map.clear();
        map.put("k1", "v1"); map.put("k2", "v2");
        Iterator<Map.Entry<Object, Object>> iter4 = map.entrySet().iterator();
        assertEquals(newMapEntry("k1", "v1"), iter4.next());
        map.remove("k1");
        map.put("k1", "v1b");
        iter4.remove();
        assertEquals(newMapEntry("k2", "v2"), iter4.next());
        assertEqualsCommutative(map, storage.getMap("mykey"), ImmutableMap.of("k2", "v2"));
    }
    
    @Test
    public void testMapKeySetIterator() throws Exception {
        Map<Object,Object> map = storage.getMap("mykey");
        map.put("k1", "v1");
        assertEquals(iteratorToList(map.keySet().iterator()), ImmutableList.of("k1"));
        
        // Remove key while iterating; will use snapshot so still contain k1
        Iterator<Object> iter1 = map.keySet().iterator();
        assertTrue(iter1.hasNext());
        map.remove("k1");
        assertEquals(iteratorToList(iter1), ImmutableList.of("k1"));
        
        // iter.remove removes value
        map.clear();
        map.put("k1", "v1"); map.put("k2", "v2");
        Iterator<Object> iter2 = map.keySet().iterator();
        assertEquals("k1", iter2.next());
        iter2.remove();
        assertEquals("k2", iter2.next());
        assertEqualsCommutative(map, storage.getMap("mykey"), ImmutableMap.of("k2", "v2"));
        
        // iter.remove when value has already been removed
        map.clear();
        map.put("k1", "v1"); map.put("k2", "v2");
        Iterator<Object> iter3 = map.keySet().iterator();
        assertEquals("k1", iter3.next());
        map.remove("k1");
        iter3.remove();
        assertEquals("k2", iter3.next());
        assertEqualsCommutative(map, storage.getMap("mykey"), ImmutableMap.of("k2", "v2"));
        
        // iter.remove when value has already been removed, but was then re-added!
        // TODO is this really the desired behaviour?
        map.clear();
        map.put("k1", "v1"); map.put("k2", "v2");
        Iterator<Object> iter4 = map.keySet().iterator();
        assertEquals("k1", iter4.next());
        map.remove("k1");
        map.put("k1", "v1b");
        iter4.remove();
        assertEquals("k2", iter4.next());
        assertEqualsCommutative(map, storage.getMap("mykey"), ImmutableMap.of("k2", "v2"));
    }
    
    // TODO InmemoryDatagrid.getMap().values() returning snapshot, so iter.remove not supported.
    // Want to test against a real datagrid instead.
    @Test(enabled=false)
    public void testMapValuesIterator() throws Exception {
        Map<Object,Object> map = storage.getMap("mykey");
        map.put("k1", "v1");
        assertEquals(ImmutableList.copyOf(iteratorToList(map.values().iterator())), ImmutableList.of("v1"));
        
        // Remove key while iterating; will use snapshot so still contain k1
        Iterator<Object> iter1 = map.values().iterator();
        assertTrue(iter1.hasNext());
        map.remove("v1");
        assertEquals(ImmutableList.copyOf(iteratorToList(iter1)), ImmutableList.of("v1"));
        
        // iter.remove removes value
        map.clear();
        map.put("k1", "v1"); map.put("k2", "v2");
        Iterator<Object> iter2 = map.values().iterator();
        assertEquals("v1", iter2.next());
        iter2.remove();
        assertEquals("v2", iter2.next());
        assertEqualsCommutative(map, storage.getMap("mykey"), ImmutableMap.of("k2", "v2"));
        
        // iter.remove when value has already been removed
        map.clear();
        map.put("k1", "v1"); map.put("k2", "v2");
        Iterator<Object> iter3 = map.values().iterator();
        assertEquals("v1", iter3.next());
        map.remove("k1");
        iter3.remove();
        assertEquals("v2", iter3.next());
        assertEqualsCommutative(map, storage.getMap("mykey"), ImmutableMap.of("k2", "v2"));
        
        // iter.remove when value has already been removed, but was then re-added!
        // TODO is this really the desired behaviour?
        map.clear();
        map.put("k1", "v1"); map.put("k2", "v2");
        Iterator<Object> iter4 = map.values().iterator();
        assertEquals("v1", iter4.next());
        map.remove("k1");
        map.put("k1", "v1b");
        iter4.remove();
        assertEquals("v2", iter4.next());
        assertEqualsCommutative(map, storage.getMap("mykey"), ImmutableMap.of("k2", "v2"));
    }
    
    private void assertEqualsCommutative(Object o1, Object o2) {
        assertEquals(o1, o2);
        assertEquals(o2, o1);
    }
    
    private void assertEqualsCommutative(Object o1, Object o2, Object o3) {
        assertEquals(o1, o3);
        assertEquals(o3, o1);
        assertEquals(o2, o3);
        assertEquals(o3, o2);
    }
    
    private <T> List<T> iteratorToList(Iterator<T> iter) {
        List<T> result = Lists.newArrayList();
        while (iter.hasNext()) {
            result.add(iter.next());
        }
        return result;
    }
    
    private <K,V> Map.Entry<K,V> newMapEntry(K k, V v) {
        return new AbstractMap.SimpleEntry<K, V>(k, v);
    }
}
