/*
 * 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.ignite.lang.utils;

import java.lang.reflect.Field;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import org.apache.ignite.internal.util.GridLeanMap;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.apache.ignite.testframework.junits.common.GridCommonTest;
import org.jetbrains.annotations.Nullable;
import org.junit.Test;

/**
 * Tests for {@link GridLeanMap}.
 */
@GridCommonTest(group = "Lang")
public class GridLeanMapSelfTest extends GridCommonAbstractTest {
    /**
     * JUnit.
     *
     * @throws Exception If failed.
     */
    @Test
    public void testDefaultMap() throws Exception {
        Map<String, String> map = new GridLeanMap<>();

        checkEmptyMap(map);
        checkImpl(map, "Map3");

        try {
            map.keySet().iterator().next();

            fail("NoSuchElementException must have been thrown.");
        }
        catch (NoSuchElementException e) {
            info("Caught expected exception: " + e);
        }

        try {
            map.entrySet().iterator().next();

            fail("NoSuchElementException must have been thrown.");
        }
        catch (NoSuchElementException e) {
            info("Caught expected exception: " + e);
        }

        try {
            map.values().iterator().next();

            fail("NoSuchElementException must have been thrown.");
        }
        catch (NoSuchElementException e) {
            info("Caught expected exception: " + e);
        }

        try {
            map.keySet().iterator().remove();

            fail("IllegalStateException must have been thrown.");
        }
        catch (IllegalStateException e) {
            info("Caught expected exception: " + e);
        }

        try {
            map.entrySet().iterator().remove();

            fail("IllegalStateException must have been thrown.");
        }
        catch (IllegalStateException e) {
            info("Caught expected exception: " + e);
        }

        try {
            map.values().iterator().remove();

            fail("IllegalStateException must have been thrown.");
        }
        catch (IllegalStateException e) {
            info("Caught expected exception: " + e);
        }
    }

    /**
     * JUnit.
     *
     * @throws Exception If failed.
     */
    @Test
    public void testEmptyMap() throws Exception {
        Map<String, String> map = new GridLeanMap<>(0);

        checkEmptyMap(map);
        checkImplBySize(map);

        try {
            map.keySet().iterator().next();

            fail("NoSuchElementException must have been thrown.");
        }
        catch (NoSuchElementException e) {
            info("Caught expected exception: " + e);
        }

        try {
            map.entrySet().iterator().next();

            fail("NoSuchElementException must have been thrown.");
        }
        catch (NoSuchElementException e) {
            info("Caught expected exception: " + e);
        }

        try {
            map.values().iterator().next();

            fail("NoSuchElementException must have been thrown.");
        }
        catch (NoSuchElementException e) {
            info("Caught expected exception: " + e);
        }

        try {
            map.keySet().iterator().remove();

            fail("IllegalStateException must have been thrown.");
        }
        catch (IllegalStateException e) {
            info("Caught expected exception: " + e);
        }

        try {
            map.entrySet().iterator().remove();

            fail("IllegalStateException must have been thrown.");
        }
        catch (IllegalStateException e) {
            info("Caught expected exception: " + e);
        }

        try {
            map.values().iterator().remove();

            fail("IllegalStateException must have been thrown.");
        }
        catch (IllegalStateException e) {
            info("Caught expected exception: " + e);
        }
    }

    /**
     * JUnit.
     *
     * @throws Exception If failed.
     */
    @Test
    public void testOneEntryMap() throws Exception {
        Map<String, String> map = new GridLeanMap<>(0);

        map.put("k1", "v1");

        checkImplBySize(map);

        assert !map.isEmpty();

        assertEquals(1, map.size());

        assert map.containsKey("k1");

        assert map.containsValue("v1");

        assert !map.keySet().isEmpty();

        assert !map.entrySet().isEmpty();

        assert !map.values().isEmpty();

        assertEquals("v1", map.get("k1"));

        assert map.get("k2") == null;

        Iterator<String> kIter = map.keySet().iterator();

        assertEquals("k1", kIter.next());

        assert !kIter.hasNext();

        Iterator<Map.Entry<String, String>> eIter = map.entrySet().iterator();

        Map.Entry<String, String> entry = eIter.next();

        assert entry != null;

        assertEquals("k1", entry.getKey());

        assertEquals("v1", entry.getValue());

        assert !eIter.hasNext();

        Iterator<String> vIter = map.values().iterator();

        assertEquals("v1", vIter.next());

        assert !vIter.hasNext();
    }

    /**
     * JUnit.
     *
     * @throws Exception If failed.
     */
    @Test
    public void testMapPutSameKey() throws Exception {
        Map<String, String> map = new GridLeanMap<>(0);

        map.put("k1", "v1");
        map.put("k1", "v2");

        checkImplBySize(map);

        assertEquals(1, map.size());

        assertEquals("v2", map.get("k1"));
    }

    /**
     * JUnit.
     *
     * @throws Exception If failed.
     */
    @Test
    public void testMultipleEntriesMap() throws Exception {
        Map<String, String> map = new GridLeanMap<>(0);

        map.put("k1", "v1");

        checkImplBySize(map);

        map.put("k2", "v2");

        checkImplBySize(map);

        map.put("k3", "v3");

        checkImplBySize(map);

        map.put("k4", "v4");

        checkImplBySize(map);

        map.put("k5", "v5");

        checkImplBySize(map);

        map.put("k6", "v6");

        assert !map.isEmpty();

        assertEquals(6, map.size());

        assert map.containsKey("k1");
        assert map.containsKey("k2");
        assert map.containsKey("k3");
        assert map.containsKey("k4");
        assert map.containsKey("k5");
        assert map.containsKey("k6");

        assert map.containsValue("v1");
        assert map.containsValue("v2");
        assert map.containsValue("v3");
        assert map.containsValue("v4");
        assert map.containsValue("v5");
        assert map.containsValue("v6");

        assert !map.keySet().isEmpty();

        assert !map.entrySet().isEmpty();

        assert !map.values().isEmpty();

        assertEquals("v1", map.get("k1"));
        assertEquals("v2", map.get("k2"));
        assertEquals("v3", map.get("k3"));
        assertEquals("v4", map.get("k4"));
        assertEquals("v5", map.get("k5"));
        assertEquals("v6", map.get("k6"));

        assert map.get("k7") == null;

        Iterator<String> kIter = map.keySet().iterator();

        for (int i = 0; i < 6; i++)
            assert kIter.next().contains("k");

        assert !kIter.hasNext();

        Iterator<Map.Entry<String, String>> eIter = map.entrySet().iterator();

        for (int i = 0; i < 6; i++) {
            Map.Entry<String, String> entry = eIter.next();

            assert entry != null : "Empty entry number: " + i;

            assert entry.getKey().contains("k") : "Invalid entry number: " + i;
            assert entry.getValue().contains("v") : "Invalid entry number: " + i;
        }

        assert !eIter.hasNext();

        Iterator<String> vIter = map.values().iterator();

        for (int i = 0; i < 6; i++)
            assert vIter.next().contains("v");

        assert !vIter.hasNext();
    }

    /**
     * JUnit.
     *
     * @throws Exception If failed.
     */
    @Test
    public void testMapRemove() throws Exception {
        Map<String, String> map = new GridLeanMap<>(0);

        // Put 1 entry.
        map.put("k1", "v1");

        checkImpl(map, "Map1");

        assertEquals("v1", map.remove("k1"));

        checkEmptyMap(map);
        checkImpl(map, null);

        // Put 2 entries.
        map.put("k1", "v1");
        map.put("k2", "v2");

        checkImpl(map, "Map2");

        assertEquals("v1", map.remove("k1"));
        assertEquals("v2", map.remove("k2"));

        checkEmptyMap(map);
        checkImpl(map, null);

        // Put more than 5 entries.
        map.put("k1", "v1");
        map.put("k2", "v2");
        map.put("k3", "v3");
        map.put("k4", "v4");
        map.put("k5", "v5");
        map.put("k6", "v6");
        map.put("k7", "v7");

        checkImpl(map, "LeanHashMap");
        checkImplBySize(map);

        assertEquals("v1", map.remove("k1"));
        assertEquals("v2", map.remove("k2"));
        assertEquals("v3", map.remove("k3"));
        assertEquals("v4", map.remove("k4"));
        assertEquals("v5", map.remove("k5"));
        assertEquals("v6", map.remove("k6"));
        assertEquals("v7", map.remove("k7"));

        checkEmptyMap(map);
        checkImpl(map, null);
    }

    /**
     * JUnit.
     *
     * @throws Exception If failed.
     */
    @Test
    public void testMapClear() throws Exception {
        Map<String, String> map = new GridLeanMap<>();

        map.put("k1", "v1");
        map.put("k2", "v2");
        map.put("k3", "v3");
        map.put("k4", "v4");
        map.put("k5", "v5");
        map.put("k6", "v6");

        map.clear();

        checkEmptyMap(map);
        checkImplBySize(map);
    }

    /**
     * JUnit.
     *
     * @throws Exception If failed.
     */
    @Test
    public void testEntrySet() throws Exception {
        Map<String, String> map = new GridLeanMap<>();

        map.put("k1", "v1");
        map.put("k2", "v2");
        map.put("k3", "v3");
        map.put("k4", "v4");
        map.put("k5", "v5");
        map.put("k6", "v6");

        checkImpl(map, "LeanHashMap");

        Iterator<Map.Entry<String, String>> iter = map.entrySet().iterator();

        assert iter.hasNext();

        Map.Entry<String, String> e = iter.next();

        assert e.getKey().contains("k");
        assert e.getValue().contains("v");

        iter.next();

        iter.remove();

        checkImpl(map, "Map5");

        assertEquals(5, map.size());

        assert iter.next() != null;
        assert iter.next() != null;

        iter.remove();

        checkImpl(map, "Map4");

        assertEquals(4, map.size());

        assert iter.next() != null;

        assert iter.hasNext();

        assert iter.next() != null;

        assert !iter.hasNext();
    }

    /**
     * JUnit.
     *
     * @throws Exception If failed.
     */
    @Test
    public void testWithInitSize1() throws Exception {
        // Batch mode.
        Map<String, String> map = new GridLeanMap<>(4);

        checkEmptyMap(map);
        checkImpl(map, "Map4");

        map.put("k1", "v1");

        assertEquals(1, map.size());
        checkImpl(map, "Map4");

        map.put("k2", "v2");

        assertEquals(2, map.size());
        checkImpl(map, "Map4");

        // The same key does not lead to implementation change.
        map.put("k2", "v2");

        assertEquals(2, map.size());
        checkImpl(map, "Map4");

        map.put("k3", "v3");

        assertEquals(3, map.size());
        checkImpl(map, "Map4");

        map.put("k4", "v4");

        assertEquals(4, map.size());
        checkImpl(map, "Map4");

        map.put("k5", "v5");

        assertEquals(5, map.size());
        checkImpl(map, "Map5");

        map.put("k6", "v6");

        assertEquals(6, map.size());
        checkImpl(map, "LeanHashMap");

        map = new GridLeanMap<>(4);

        checkEmptyMap(map);
        checkImpl(map, "Map4");

        map.put("k1", "v1");

        assertEquals(1, map.size());
        checkImpl(map, "Map4");

        map.put("k2", "v2");

        assertEquals(2, map.size());
        checkImpl(map, "Map4");

        // Any remove operation leads to map size optimization.
        assertEquals("v1", map.remove("k1"));
        checkImpl(map, "Map1");
    }

    /**
     * JUnit.
     *
     * @throws Exception If failed.
     */
    @Test
    public void testWithInitSize2() throws Exception {
        // Batch mode.
        Map<String, String> map = new GridLeanMap<>(10);

        checkEmptyMap(map);
        checkImpl(map, "LeanHashMap");

        map.put("k1", "v1");

        assertEquals(1, map.size());
        checkImpl(map, "LeanHashMap");

        map.put("k2", "v2");

        assertEquals(2, map.size());
        checkImpl(map, "LeanHashMap");

        map.put("k3", "v3");

        assertEquals(3, map.size());
        checkImpl(map, "LeanHashMap");

        map.put("k4", "v4");

        assertEquals(4, map.size());
        checkImpl(map, "LeanHashMap");

        map.put("k5", "v5");

        assertEquals(5, map.size());
        checkImpl(map, "LeanHashMap");

        map.put("k6", "v6");

        assertEquals(6, map.size());
        checkImpl(map, "LeanHashMap");

        map.put("k7", "v7");

        assertEquals(7, map.size());
        checkImpl(map, "LeanHashMap");

        map.put("k8", "v8");

        assertEquals(8, map.size());
        checkImpl(map, "LeanHashMap");

        map.put("k9", "v9");

        assertEquals(9, map.size());
        checkImpl(map, "LeanHashMap");

        map.put("k10", "v10");

        assertEquals(10, map.size());
        checkImpl(map, "LeanHashMap");
    }

    /**
     * Checks map emptiness.
     *
     * @param map Map to check.
     * @throws Exception If failed.
     */
    private void checkEmptyMap(Map<?, ?> map) throws Exception {
        assert map.isEmpty();

        assert !map.containsKey("key");

        assert !map.containsValue("value");

        assertEquals(0, map.size());

        assert map.keySet().isEmpty();

        assert map.entrySet().isEmpty();

        assert map.values().isEmpty();

        assert !map.keySet().iterator().hasNext();

        assert !map.entrySet().iterator().hasNext();

        assert !map.values().iterator().hasNext();
    }

    /**
     * @param map Lean map.
     * @param implName Internal implementation simple class name.
     *      {@code null} for empty implementation.
     * @throws Exception If failed.
     */
    private void checkImpl(Map map, @Nullable String implName) throws Exception {
        assert map != null;

        Field f = GridLeanMap.class.getDeclaredField("map");

        f.setAccessible(true);

        Map impl = (Map)f.get(map);

        if (implName != null)
            assertEquals(implName, impl.getClass().getSimpleName());
        else
            assert impl == null;
    }

    /**
     * @param map Lean map.
     * @throws Exception If failed.
     */
    private void checkImplBySize(Map map) throws Exception {
        assert map != null;

        if (map.isEmpty())
            checkImpl(map, null);
        else if (map.size() == 1)
            checkImpl(map, "Map1");
        else if (map.size() == 2)
            checkImpl(map, "Map2");
        else if (map.size() == 3)
            checkImpl(map, "Map3");
        else if (map.size() == 4)
            checkImpl(map, "Map4");
        else if (map.size() == 5)
            checkImpl(map, "Map5");
        else
            checkImpl(map, "LeanHashMap");
    }
}
