/*
 * 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
 *
 *      https://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.commons.collections4.map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.InvalidObjectException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections4.MapIterator;
import org.apache.commons.collections4.OrderedMap;
import org.apache.commons.collections4.ResettableIterator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

/**
 * JUnit tests.
 *
 * @param <K> The key type.
 * @param <V> The value type.
 */
public class LRUMapTest<K, V> extends AbstractOrderedMapTest<K, V> {

    static class MockLRUMapSubclass<K, V> extends LRUMap<K, V> {

        /**
         * Generated serial version ID.
         */
        private static final long serialVersionUID = -2126883654452042477L;
        LinkEntry<K, V> entry;
        K key;
        V value;

        MockLRUMapSubclass(final int size) {
            super(size);
        }

        @Override
        protected boolean removeLRU(final LinkEntry<K, V> entry) {
            this.entry = entry;
            this.key = entry.getKey();
            this.value = entry.getValue();
            return true;
        }

    }
    static class MockLRUMapSubclassBlocksRemove<K, V> extends LRUMap<K, V> {

        /**
         * Generated serial version ID.
         */
        private static final long serialVersionUID = 6278917461128992945L;

        MockLRUMapSubclassBlocksRemove(final int size, final boolean scanUntilRemove) {
            super(size, scanUntilRemove);
        }

        @Override
        protected boolean removeLRU(final LinkEntry<K, V> entry) {
            return false;
        }

    }

    static class MockLRUMapSubclassFirstBlocksRemove<K, V> extends LRUMap<K, V> {

        /**
         * Generated serial version ID.
         */
        private static final long serialVersionUID = -6939790801702973428L;

        MockLRUMapSubclassFirstBlocksRemove(final int size) {
            super(size, true);
        }

        @Override
        protected boolean removeLRU(final LinkEntry<K, V> entry) {
            if ("a".equals(entry.getValue())) {
                return false;
            }
            return true;
        }

    }

    static class SingleHashCode {

        private final String code;

        SingleHashCode(final String code) {
            this.code = code;
        }

        @Override
        public boolean equals(final Object o) {
            // Pairs with equals()
            return super.equals(o);
        }

        @Override
        public int hashCode() {
            // always return the same hash code
            // that way, it will end up in the same bucket
            return 12;
        }

        @Override
        public String toString() {
            return "SingleHashCode:" + code;
        }

    }

    @Override
    public String getCompatibilityVersion() {
        return "4";
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public LRUMap<K, V> getMap() {
        return (LRUMap<K, V>) super.getMap();
    }

    @Override
    public boolean isGetStructuralModify() {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public LRUMap<K, V> makeFullMap() {
        return (LRUMap<K, V>) super.makeFullMap();
    }

    @Override
    public LRUMap<K, V> makeObject() {
        return new LRUMap<>();
    }

    @Test
    void testAccessOrder() {
        if (!isPutAddSupported() || !isPutChangeSupported()) {
            return;
        }
        final K[] keys = getSampleKeys();
        final V[] values = getSampleValues();
        Iterator<K> kit;
        Iterator<V> vit;

        resetEmpty();
        map.put(keys[0], values[0]);
        map.put(keys[1], values[1]);
        kit = map.keySet().iterator();
        assertSame(keys[0], kit.next());
        assertSame(keys[1], kit.next());
        vit = map.values().iterator();
        assertSame(values[0], vit.next());
        assertSame(values[1], vit.next());

        // no change to order
        map.put(keys[1], values[1]);
        kit = map.keySet().iterator();
        assertSame(keys[0], kit.next());
        assertSame(keys[1], kit.next());
        vit = map.values().iterator();
        assertSame(values[0], vit.next());
        assertSame(values[1], vit.next());

        // no change to order
        map.put(keys[1], values[2]);
        kit = map.keySet().iterator();
        assertSame(keys[0], kit.next());
        assertSame(keys[1], kit.next());
        vit = map.values().iterator();
        assertSame(values[0], vit.next());
        assertSame(values[2], vit.next());

        // change to order
        map.put(keys[0], values[3]);
        kit = map.keySet().iterator();
        assertSame(keys[1], kit.next());
        assertSame(keys[0], kit.next());
        vit = map.values().iterator();
        assertSame(values[2], vit.next());
        assertSame(values[3], vit.next());

        // change to order
        map.get(keys[1]);
        kit = map.keySet().iterator();
        assertSame(keys[0], kit.next());
        assertSame(keys[1], kit.next());
        vit = map.values().iterator();
        assertSame(values[3], vit.next());
        assertSame(values[2], vit.next());

        // change to order
        map.get(keys[0]);
        kit = map.keySet().iterator();
        assertSame(keys[1], kit.next());
        assertSame(keys[0], kit.next());
        vit = map.values().iterator();
        assertSame(values[2], vit.next());
        assertSame(values[3], vit.next());

        // no change to order
        map.get(keys[0]);
        kit = map.keySet().iterator();
        assertSame(keys[1], kit.next());
        assertSame(keys[0], kit.next());
        vit = map.values().iterator();
        assertSame(values[2], vit.next());
        assertSame(values[3], vit.next());
    }

    @Test
    void testAccessOrder2() {
        if (!isPutAddSupported() || !isPutChangeSupported()) {
            return;
        }
        final K[] keys = getSampleKeys();
        final V[] values = getSampleValues();
        Iterator<K> kit;
        Iterator<V> vit;

        resetEmpty();
        final LRUMap<K, V> lruMap = (LRUMap<K, V>) map;

        lruMap.put(keys[0], values[0]);
        lruMap.put(keys[1], values[1]);
        kit = lruMap.keySet().iterator();
        assertSame(keys[0], kit.next());
        assertSame(keys[1], kit.next());
        vit = lruMap.values().iterator();
        assertSame(values[0], vit.next());
        assertSame(values[1], vit.next());

        // no change to order
        lruMap.put(keys[1], values[1]);
        kit = lruMap.keySet().iterator();
        assertSame(keys[0], kit.next());
        assertSame(keys[1], kit.next());
        vit = lruMap.values().iterator();
        assertSame(values[0], vit.next());
        assertSame(values[1], vit.next());

        // no change to order
        lruMap.get(keys[1], false);
        kit = lruMap.keySet().iterator();
        assertSame(keys[0], kit.next());
        assertSame(keys[1], kit.next());
        vit = lruMap.values().iterator();
        assertSame(values[0], vit.next());
        assertSame(values[1], vit.next());

        // change to order
        lruMap.get(keys[0], true);
        kit = lruMap.keySet().iterator();
        assertSame(keys[1], kit.next());
        assertSame(keys[0], kit.next());
        vit = lruMap.values().iterator();
        assertSame(values[1], vit.next());
        assertSame(values[0], vit.next());
    }

    @Test
    @SuppressWarnings("unchecked")
    void testClone() {
        final LRUMap<K, V> map = new LRUMap<>(10);
        map.put((K) "1", (V) "1");
        final Map<K, V> cloned = map.clone();
        assertEquals(map.size(), cloned.size());
        assertSame(map.get("1"), cloned.get("1"));
    }

    @Test
    void testCtors() {
        assertThrows(IllegalArgumentException.class, () -> new LRUMap<K, V>(0), "maxSize must be positive");
        assertThrows(IllegalArgumentException.class, () -> new LRUMap<K, V>(-1, 12, 0.75f, false), "maxSize must be positive");
        assertThrows(IllegalArgumentException.class, () -> new LRUMap<K, V>(10, -1), "initialSize must not be negative");
        assertThrows(IllegalArgumentException.class, () -> new LRUMap<K, V>(10, 12), "initialSize must not be larger than maxSize");
        assertThrows(IllegalArgumentException.class, () -> new LRUMap<K, V>(10, -1, 0.75f, false), "initialSize must not be negative");
        assertThrows(IllegalArgumentException.class, () -> new LRUMap<K, V>(10, 12, 0.75f, false), "initialSize must not be larger than maxSize");
    }

    /**
     * A crafted stream can carry a load factor the constructor rejects. AbstractHashedMap.doReadObject
     * must reapply that contract on read.
     */
    @ParameterizedTest
    @ValueSource(floats = {0.0f, -1.0f, Float.NaN})
    void testDeserializeRejectsInvalidLoadFactor(final float badLoadFactor) {
        final LRUMap<K, V> map = new LRUMap<>();
        map.loadFactor = badLoadFactor;
        assertThrows(InvalidObjectException.class, () -> serializeDeserialize(map));
    }

    @Test
    void testDeserializeRejectsNonPositiveMaxSize() throws Exception {
        final LRUMap<String, String> map = new LRUMap<>(3);
        map.put("a", "1");
        final Field maxSizeField = LRUMap.class.getDeclaredField("maxSize");
        maxSizeField.setAccessible(true);
        for (final int maxSize : new int[] {0, -7}) {
            maxSizeField.setInt(map, maxSize);
            assertThrows(InvalidObjectException.class, () -> serializeDeserialize(map));
        }
    }

    @Test
    @SuppressWarnings("unchecked")
    void testInternalState_Buckets() {
        if (!isPutAddSupported() || !isPutChangeSupported()) {
            return;
        }
        final SingleHashCode one = new SingleHashCode("1");
        final SingleHashCode two = new SingleHashCode("2");
        final SingleHashCode three = new SingleHashCode("3");
        final SingleHashCode four = new SingleHashCode("4");
        final SingleHashCode five = new SingleHashCode("5");
        final SingleHashCode six = new SingleHashCode("6");

        final LRUMap<K, V> map = new LRUMap<>(3, 1.0f);
        final int hashIndex = map.hashIndex(map.hash(one), 4);
        map.put((K) one, (V) "A");
        map.put((K) two, (V) "B");
        map.put((K) three, (V) "C");

        assertEquals(4, map.data.length);
        assertEquals(3, map.size);
        assertNull(map.header.next);
        assertEquals(one, map.header.after.key);  // LRU
        assertEquals(two, map.header.after.after.key);
        assertEquals(three, map.header.after.after.after.key);  // MRU
        assertEquals(three, map.data[hashIndex].key);
        assertEquals(two, map.data[hashIndex].next.key);
        assertEquals(one, map.data[hashIndex].next.next.key);

        map.put((K) four, (V) "D");  // reuses last in next list

        assertEquals(4, map.data.length);
        assertEquals(3, map.size);
        assertNull(map.header.next);
        assertEquals(two, map.header.after.key);  // LRU
        assertEquals(three, map.header.after.after.key);
        assertEquals(four, map.header.after.after.after.key);  // MRU
        assertEquals(four, map.data[hashIndex].key);
        assertEquals(three, map.data[hashIndex].next.key);
        assertEquals(two, map.data[hashIndex].next.next.key);

        map.get(three);

        assertEquals(4, map.data.length);
        assertEquals(3, map.size);
        assertNull(map.header.next);
        assertEquals(two, map.header.after.key);  // LRU
        assertEquals(four, map.header.after.after.key);
        assertEquals(three, map.header.after.after.after.key);  // MRU
        assertEquals(four, map.data[hashIndex].key);
        assertEquals(three, map.data[hashIndex].next.key);
        assertEquals(two, map.data[hashIndex].next.next.key);

        map.put((K) five, (V) "E");  // reuses last in next list

        assertEquals(4, map.data.length);
        assertEquals(3, map.size);
        assertNull(map.header.next);
        assertEquals(four, map.header.after.key);  // LRU
        assertEquals(three, map.header.after.after.key);
        assertEquals(five, map.header.after.after.after.key);  // MRU
        assertEquals(five, map.data[hashIndex].key);
        assertEquals(four, map.data[hashIndex].next.key);
        assertEquals(three, map.data[hashIndex].next.next.key);

        map.get(three);
        map.get(five);

        assertEquals(4, map.data.length);
        assertEquals(3, map.size);
        assertNull(map.header.next);
        assertEquals(four, map.header.after.key);  // LRU
        assertEquals(three, map.header.after.after.key);
        assertEquals(five, map.header.after.after.after.key);  // MRU
        assertEquals(five, map.data[hashIndex].key);
        assertEquals(four, map.data[hashIndex].next.key);
        assertEquals(three, map.data[hashIndex].next.next.key);

        map.put((K) six, (V) "F");  // reuses middle in next list

        assertEquals(4, map.data.length);
        assertEquals(3, map.size);
        assertNull(map.header.next);
        assertEquals(three, map.header.after.key);  // LRU
        assertEquals(five, map.header.after.after.key);
        assertEquals(six, map.header.after.after.after.key);  // MRU
        assertEquals(six, map.data[hashIndex].key);
        assertEquals(five, map.data[hashIndex].next.key);
        assertEquals(three, map.data[hashIndex].next.next.key);
    }

    @Test
    @SuppressWarnings("unchecked")
    void testInternalState_getEntry_int() {
        if (!isPutAddSupported() || !isPutChangeSupported()) {
            return;
        }
        final SingleHashCode one = new SingleHashCode("1");
        final SingleHashCode two = new SingleHashCode("2");
        final SingleHashCode three = new SingleHashCode("3");
        final LRUMap<K, V> map = new LRUMap<>(3, 1.0f);
        map.put((K) one, (V) "A");
        map.put((K) two, (V) "B");
        map.put((K) three, (V) "C");
        assertEquals(one, map.getEntry(0).key);
        assertEquals(two, map.getEntry(1).key);
        assertEquals(three, map.getEntry(2).key);
        assertThrows(IndexOutOfBoundsException.class, () -> map.getEntry(-1));
        assertThrows(IndexOutOfBoundsException.class, () -> map.getEntry(3));
    }

    @Test
    void testLRU() {
        if (!isPutAddSupported() || !isPutChangeSupported()) {
            return;
        }
        final K[] keys = getSampleKeys();
        final V[] values = getSampleValues();
        Iterator<K> kit;
        Iterator<V> vit;

        final LRUMap<K, V> map = new LRUMap<>(2);
        assertEquals(0, map.size());
        assertFalse(map.isFull());
        assertEquals(2, map.maxSize());

        map.put(keys[0], values[0]);
        assertEquals(1, map.size());
        assertFalse(map.isFull());
        assertEquals(2, map.maxSize());

        map.put(keys[1], values[1]);
        assertEquals(2, map.size());
        assertTrue(map.isFull());
        assertEquals(2, map.maxSize());
        kit = map.keySet().iterator();
        assertSame(keys[0], kit.next());
        assertSame(keys[1], kit.next());
        vit = map.values().iterator();
        assertSame(values[0], vit.next());
        assertSame(values[1], vit.next());

        map.put(keys[2], values[2]);
        assertEquals(2, map.size());
        assertTrue(map.isFull());
        assertEquals(2, map.maxSize());
        kit = map.keySet().iterator();
        assertSame(keys[1], kit.next());
        assertSame(keys[2], kit.next());
        vit = map.values().iterator();
        assertSame(values[1], vit.next());
        assertSame(values[2], vit.next());

        map.put(keys[2], values[0]);
        assertEquals(2, map.size());
        assertTrue(map.isFull());
        assertEquals(2, map.maxSize());
        kit = map.keySet().iterator();
        assertSame(keys[1], kit.next());
        assertSame(keys[2], kit.next());
        vit = map.values().iterator();
        assertSame(values[1], vit.next());
        assertSame(values[0], vit.next());

        map.put(keys[1], values[3]);
        assertEquals(2, map.size());
        assertTrue(map.isFull());
        assertEquals(2, map.maxSize());
        kit = map.keySet().iterator();
        assertSame(keys[2], kit.next());
        assertSame(keys[1], kit.next());
        vit = map.values().iterator();
        assertSame(values[0], vit.next());
        assertSame(values[3], vit.next());
    }

    @Test
    @SuppressWarnings("unchecked")
    void testRemoveLRU() {
        final MockLRUMapSubclass<K, String> map = new MockLRUMapSubclass<>(2);
        assertNull(map.entry);
        map.put((K) "A", "a");
        assertNull(map.entry);
        map.put((K) "B", "b");
        assertNull(map.entry);
        map.put((K) "C", "c");  // removes oldest, which is A=a
        assertNotNull(map.entry);
        assertEquals("A", map.key);
        assertEquals("a", map.value);
        assertEquals("C", map.entry.getKey());  // entry is reused
        assertEquals("c", map.entry.getValue());  // entry is reused
        assertFalse(map.containsKey("A"));
        assertTrue(map.containsKey("B"));
        assertTrue(map.containsKey("C"));
    }

    @Test
    @SuppressWarnings("unchecked")
    void testRemoveLRUBlocksRemove() {
        final MockLRUMapSubclassBlocksRemove<K, V> map = new MockLRUMapSubclassBlocksRemove<>(2, false);
        assertEquals(0, map.size());
        map.put((K) "A", (V) "a");
        assertEquals(1, map.size());
        map.put((K) "B", (V) "b");
        assertEquals(2, map.size());
        map.put((K) "C", (V) "c");  // should remove oldest, which is A=a, but this is blocked
        assertEquals(3, map.size());
        assertEquals(2, map.maxSize());
        assertTrue(map.containsKey("A"));
        assertTrue(map.containsKey("B"));
        assertTrue(map.containsKey("C"));
    }

    @Test
    @SuppressWarnings("unchecked")
    void testRemoveLRUBlocksRemoveScan() {
        final MockLRUMapSubclassBlocksRemove<K, V> map = new MockLRUMapSubclassBlocksRemove<>(2, true);
        assertEquals(0, map.size());
        map.put((K) "A", (V) "a");
        assertEquals(1, map.size());
        map.put((K) "B", (V) "b");
        assertEquals(2, map.size());
        map.put((K) "C", (V) "c");  // should remove oldest, which is A=a, but this is blocked
        assertEquals(3, map.size());
        assertEquals(2, map.maxSize());
        assertTrue(map.containsKey("A"));
        assertTrue(map.containsKey("B"));
        assertTrue(map.containsKey("C"));
    }

    @Test
    @SuppressWarnings("unchecked")
    void testRemoveLRUFirstBlocksRemove() {
        final MockLRUMapSubclassFirstBlocksRemove<K, V> map = new MockLRUMapSubclassFirstBlocksRemove<>(2);
        assertEquals(0, map.size());
        map.put((K) "A", (V) "a");
        assertEquals(1, map.size());
        map.put((K) "B", (V) "b");
        assertEquals(2, map.size());
        map.put((K) "C", (V) "c");  // should remove oldest, which is A=a  but this is blocked - so advance to B=b
        assertEquals(2, map.size());
        assertEquals(2, map.maxSize());
        assertTrue(map.containsKey("A"));
        assertFalse(map.containsKey("B"));
        assertTrue(map.containsKey("C"));
    }

    @Test
    @SuppressWarnings("unchecked")
    void testReset() {
        resetEmpty();
        OrderedMap<K, V> ordered = getMap();
        ((ResettableIterator<K>) ordered.mapIterator()).reset();

        resetFull();
        ordered = getMap();
        final List<K> list = new ArrayList<>(ordered.keySet());
        final ResettableIterator<K> it = (ResettableIterator<K>) ordered.mapIterator();
        assertSame(list.get(0), it.next());
        assertSame(list.get(1), it.next());
        it.reset();
        assertSame(list.get(0), it.next());
    }

    @Test
    void testSynchronizedRemoveFromEntrySet() throws InterruptedException {

        final Map<Object, Thread> map = new LRUMap<>(10000);

        final Map<Throwable, String> exceptions = new HashMap<>();
        final ThreadGroup tg = new ThreadGroup(getName()) {
            @Override
            public void uncaughtException(final Thread t, final Throwable e) {
                exceptions.put(e, t.getName());
                super.uncaughtException(t, e);
            }
        };

        final int[] counter = new int[1];
        counter[0] = 0;
        final Thread[] threads = new Thread[50];
        for (int i = 0; i < threads.length; ++i) {
            threads[i] = new Thread(tg, "JUnit Thread " + i) {

                @Override
                public void run() {
                    int i = 0;
                    try {
                        synchronized (this) {
                            notifyAll();
                            wait();
                        }
                        final Thread thread = Thread.currentThread();
                        while (i < 1000  && !interrupted()) {
                            synchronized (map) {
                                map.put(thread.getName() + "[" + ++i + "]", thread);
                            }
                        }
                        synchronized (map) {
                            map.entrySet().removeIf(entry -> entry.getValue() == this);
                        }
                    } catch (final InterruptedException e) {
                        interrupt();
                        fail("Unexpected InterruptedException");
                    }
                    if (i > 0) {
                        synchronized (counter) {
                            counter[0]++;
                        }
                    }
                }

            };
        }

        for (final Thread thread : threads) {
            synchronized (thread) {
                thread.start();
                thread.wait();
            }
        }

        for (final Thread thread : threads) {
            synchronized (thread) {
                thread.notifyAll();
            }
        }

        Thread.sleep(1000);

        for (final Thread thread : threads) {
            thread.interrupt();
        }
        for (final Thread thread : threads) {
            synchronized (thread) {
                thread.join();
            }
        }

        assertEquals(0, exceptions.size(), "Exceptions have been thrown: " + exceptions);
        assertTrue(counter[0] >= threads.length,
                "Each thread should have put at least 1 element into the map, but only " + counter[0] + " did succeed");
    }

    @Test
    void testSynchronizedRemoveFromKeySet() throws InterruptedException {

        final Map<Object, Thread> map = new LRUMap<>(10000);

        final Map<Throwable, String> exceptions = new HashMap<>();
        final ThreadGroup tg = new ThreadGroup(getName()) {
            @Override
            public void uncaughtException(final Thread t, final Throwable e) {
                exceptions.put(e, t.getName());
                super.uncaughtException(t, e);
            }
        };

        final int[] counter = new int[1];
        counter[0] = 0;
        final Thread[] threads = new Thread[50];
        for (int i = 0; i < threads.length; ++i) {
            threads[i] = new Thread(tg, "JUnit Thread " + i) {

                @Override
                public void run() {
                    int i = 0;
                    try {
                        synchronized (this) {
                            notifyAll();
                            wait();
                        }
                        final Thread thread = Thread.currentThread();
                        while (i < 1000  && !interrupted()) {
                            synchronized (map) {
                                map.put(thread.getName() + "[" + ++i + "]", thread);
                            }
                        }
                        synchronized (map) {
                            for (final Iterator<Object> iter = map.keySet().iterator(); iter.hasNext();) {
                                final String name = (String) iter.next();
                                if (name.substring(0, name.indexOf('[')).equals(getName())) {
                                    iter.remove();
                                }
                            }
                        }
                    } catch (final InterruptedException e) {
                        interrupt();
                        fail("Unexpected InterruptedException");
                    }
                    if (i > 0) {
                        synchronized (counter) {
                            counter[0]++;
                        }
                    }
                }

            };
        }

        for (final Thread thread : threads) {
            synchronized (thread) {
                thread.start();
                thread.wait();
            }
        }

        for (final Thread thread : threads) {
            synchronized (thread) {
                thread.notifyAll();
            }
        }

        Thread.sleep(1000);

        for (final Thread thread : threads) {
            thread.interrupt();
        }
        for (final Thread thread : threads) {
            synchronized (thread) {
                thread.join();
            }
        }

        assertEquals(0, exceptions.size(), "Exceptions have been thrown: " + exceptions);
        assertTrue(counter[0] >= threads.length,
                "Each thread should have put at least 1 element into the map, but only " + counter[0] + " did succeed");
    }

    @Test
    void testSynchronizedRemoveFromMapIterator() throws InterruptedException {

        final LRUMap<Object, Thread> map = new LRUMap<>(10000);

        final Map<Throwable, String> exceptions = new HashMap<>();
        final ThreadGroup tg = new ThreadGroup(getName()) {
            @Override
            public void uncaughtException(final Thread t, final Throwable e) {
                exceptions.put(e, t.getName());
                super.uncaughtException(t, e);
            }
        };

        final int[] counter = new int[1];
        counter[0] = 0;
        final Thread[] threads = new Thread[50];
        for (int i = 0; i < threads.length; ++i) {
            threads[i] = new Thread(tg, "JUnit Thread " + i) {

                @Override
                public void run() {
                    int i = 0;
                    try {
                        synchronized (this) {
                            notifyAll();
                            wait();
                        }
                        final Thread thread = Thread.currentThread();
                        while (i < 1000  && !interrupted()) {
                            synchronized (map) {
                                map.put(thread.getName() + "[" + ++i + "]", thread);
                            }
                        }
                        synchronized (map) {
                            for (final MapIterator<Object, Thread> iter = map.mapIterator(); iter.hasNext();) {
                                iter.next();
                                if (iter.getValue() == this) {
                                    iter.remove();
                                }
                            }
                        }
                    } catch (final InterruptedException e) {
                        interrupt();
                        fail("Unexpected InterruptedException");
                    }
                    if (i > 0) {
                        synchronized (counter) {
                            counter[0]++;
                        }
                    }
                }

            };
        }

        for (final Thread thread : threads) {
            synchronized (thread) {
                thread.start();
                thread.wait();
            }
        }

        for (final Thread thread : threads) {
            synchronized (thread) {
                thread.notifyAll();
            }
        }

        Thread.sleep(1000);

        for (final Thread thread : threads) {
            thread.interrupt();
        }
        for (final Thread thread : threads) {
            synchronized (thread) {
                thread.join();
            }
        }

        assertEquals(0, exceptions.size(), "Exceptions have been thrown: " + exceptions);
        assertTrue(counter[0] >= threads.length,
                "Each thread should have put at least 1 element into the map, but only " + counter[0] + " did succeed");
    }

    @Test
    void testSynchronizedRemoveFromValues() throws InterruptedException {

        final Map<Object, Thread> map = new LRUMap<>(10000);

        final Map<Throwable, String> exceptions = new HashMap<>();
        final ThreadGroup tg = new ThreadGroup(getName()) {
            @Override
            public void uncaughtException(final Thread t, final Throwable e) {
                exceptions.put(e, t.getName());
                super.uncaughtException(t, e);
            }
        };

        final int[] counter = new int[1];
        counter[0] = 0;
        final Thread[] threads = new Thread[50];
        for (int i = 0; i < threads.length; ++i) {
            threads[i] = new Thread(tg, "JUnit Thread " + i) {

                @Override
                public void run() {
                    int i = 0;
                    try {
                        synchronized (this) {
                            notifyAll();
                            wait();
                        }
                        final Thread thread = Thread.currentThread();
                        while (i < 1000  && !interrupted()) {
                            synchronized (map) {
                                map.put(thread.getName() + "[" + ++i + "]", thread);
                            }
                        }
                        synchronized (map) {
                            map.values().removeIf(thread1 -> thread1 == this);
                        }
                    } catch (final InterruptedException e) {
                        interrupt();
                        fail("Unexpected InterruptedException");
                    }
                    if (i > 0) {
                        synchronized (counter) {
                            counter[0]++;
                        }
                    }
                }

            };
        }

        for (final Thread thread : threads) {
            synchronized (thread) {
                thread.start();
                thread.wait();
            }
        }

        for (final Thread thread : threads) {
            synchronized (thread) {
                thread.notifyAll();
            }
        }

        Thread.sleep(1000);

        for (final Thread thread : threads) {
            thread.interrupt();
        }
        for (final Thread thread : threads) {
            synchronized (thread) {
                thread.join();
            }
        }

        assertEquals(0, exceptions.size(), "Exceptions have been thrown: " + exceptions);
        assertTrue(counter[0] >= threads.length,
                "Each thread should have put at least 1 element into the map, but only " + counter[0] + " did succeed");
    }

//    void testCreate() throws Exception {
//        resetEmpty();
//        writeExternalFormToDisk((java.io.Serializable) map, "src/test/resources/data/test/LRUMap.emptyCollection.version4.obj");
//        resetFull();
//        writeExternalFormToDisk((java.io.Serializable) map, "src/test/resources/data/test/LRUMap.fullCollection.version4.obj");
//    }

}
