/*
 * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.IdentityHashMap;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.stream.IntStream;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import static java.util.Map.entry;
import static org.testng.Assert.*;

/*
 * @test
 * @bug 8285295 8178355
 * @summary Basic tests for IdentityHashMap
 * @run testng Basic
 */

// NOTE: avoid using TestNG's assertEquals/assertNotEquals directly on two IDHM instances,
// as its logic for testing collections equality is suspect. Use checkEntries() to assert
// that a map's entrySet contains exactly the expected mappings. There are no guarantees about
// the identities of Map.Entry instances obtained from the entrySet; however, the keys and
// values they contain are guaranteed to have the right identity.

// TODO add tests using null keys and values
// TODO deeper testing of view collections including iterators, equals, contains, etc.
// TODO Map.Entry::setValue

public class Basic {
    /*
     * Helpers
     */

    record Box(int i) {
        Box(Box other) {
            this(other.i());
        }
    }

    // Checks that a collection contains exactly the given elements and no others, using the
    // provided predicate for equivalence. Checking is performed both using contains() on the
    // collection and by simple array searching. The latter is O(N^2) so is suitable only for
    // small arrays. No two of the given elements can be equivalent according to the predicate.

    // TODO: read out the elements using iterator and stream and check them too

    @SafeVarargs
    private <E> void checkContents(Collection<E> c, BiPredicate<E,E> p, E... given) {
        @SuppressWarnings("unchecked")
        E[] contents = (E[]) c.toArray();

        assertEquals(c.size(), given.length);
        assertEquals(contents.length, given.length);
        final int LEN = given.length;

        for (E e : given) {
            assertTrue(c.contains(e));
        }

        // Fill indexes array with position of a given element in the contents array,
        // or -1 if the given element cannot be found.

        int[] indexes = new int[LEN];

        outer:
        for (int i = 0; i < LEN; i++) {
            for (int j = 0; j < LEN; j++) {
                if (p.test(given[i], contents[j])) {
                    indexes[i] = j;
                    continue outer;
                }
            }
            indexes[i] = -1;
        }

        // If every given element matches a distinct element in the contents array,
        // the sorted indexes array will be the sequence [0..LEN-1].

        Arrays.sort(indexes);
        assertEquals(indexes, IntStream.range(0, LEN).toArray());
    }

    // Checks that the collection contains the given boxes, by identity.
    private void checkElements(Collection<Box> c, Box... given) {
        checkContents(c, (b1, b2) -> b1 == b2, given);
    }

    // Checks that the collection contains entries that have identical keys and values.
    // The entries themselves are not checked for identity.
    @SafeVarargs
    private void checkEntries(Collection<Map.Entry<Box, Box>> c, Map.Entry<Box, Box>... given) {
        checkContents(c, (e1, e2) -> e1.getKey() == e2.getKey() && e1.getValue() == e2.getValue(), given);
    }

    /*
     * Setup
     */

    final Box k1a = new Box(17);
    final Box k1b = new Box(17); // equals but != k1a
    final Box k2  = new Box(42);

    final Box v1a = new Box(30);
    final Box v1b = new Box(30); // equals but != v1a
    final Box v2  = new Box(99);

    IdentityHashMap<Box, Box> map;
    IdentityHashMap<Box, Box> map2;

    @BeforeMethod
    public void setup() {
        map = new IdentityHashMap<>();
        map.put(k1a, v1a);
        map.put(k1b, v1b);
        map.put(k2,  v2);

        map2 = new IdentityHashMap<>();
        map2.put(k1a, v1a);
        map2.put(k1b, v1b);
        map2.put(k2,  v2);
    }

    /*
     * Tests
     */

    // containsKey
    // containsValue
    // size
    @Test
    public void testSizeContainsKeyValue() {
        assertEquals(map.size(), 3);

        assertTrue(map.containsKey(k1a));
        assertTrue(map.containsKey(k1b));
        assertTrue(map.containsKey(k2));
        assertFalse(map.containsKey(new Box(k1a)));

        assertTrue(map.containsValue(v1a));
        assertTrue(map.containsValue(v1b));
        assertFalse(map.containsValue(new Box(v1a)));
        assertTrue(map.containsValue(v2));
    }

    // get
    @Test
    public void testGet() {
        assertSame(map.get(k1a), v1a);
        assertSame(map.get(k1b), v1b);
        assertSame(map.get(k2), v2);
        assertNull(map.get(new Box(k1a)));
    }

    // getOrDefault
    @Test
    public void testGetOrDefault() {
        Box other = new Box(22);

        assertSame(map.getOrDefault(k1a, other), v1a);
        assertSame(map.getOrDefault(k1b, other), v1b);
        assertSame(map.getOrDefault(new Box(k1a), other), other);
        assertSame(map.getOrDefault(k2, other), v2);
    }

    // clear
    // isEmpty
    @Test
    public void testClearEmpty() {
        assertFalse(map.isEmpty());
        map.clear();
        assertTrue(map.isEmpty());
    }

    // hashCode
    @Test
    public void testHashCode() {
        int expected = (System.identityHashCode(k1a) ^ System.identityHashCode(v1a)) +
                       (System.identityHashCode(k1b) ^ System.identityHashCode(v1b)) +
                       (System.identityHashCode(k2)  ^ System.identityHashCode(v2));
        assertEquals(map.hashCode(), expected);
        assertEquals(map.entrySet().hashCode(), expected);
    }

    // equals
    @Test
    public void testEquals() {
        assertTrue(map.equals(map));
        assertTrue(map.equals(map2));
        assertTrue(map2.equals(map));

        assertTrue(map.keySet().equals(map.keySet()));
        assertTrue(map.keySet().equals(map2.keySet()));
        assertTrue(map2.keySet().equals(map.keySet()));

        assertTrue(map.entrySet().equals(map.entrySet()));
        assertTrue(map.entrySet().equals(map2.entrySet()));
        assertTrue(map2.entrySet().equals(map.entrySet()));
    }

    // equals
    @Test
    public void testEqualsDifferentKey() {
        map2.remove(k1a);
        map2.put(new Box(k1a), v1a);

        assertFalse(map.equals(map2));
        assertFalse(map2.equals(map));

        assertFalse(map.keySet().equals(map2.keySet()));
        assertFalse(map2.keySet().equals(map.keySet()));

        assertFalse(map.entrySet().equals(map2.entrySet()));
        assertFalse(map2.entrySet().equals(map.entrySet()));
    }

    // equals
    @Test
    public void testEqualsDifferentValue() {
        map2.put(k1a, new Box(v1a));

        assertFalse(map.equals(map2));
        assertFalse(map2.equals(map));

        assertTrue(map.keySet().equals(map2.keySet()));
        assertTrue(map2.keySet().equals(map.keySet()));

        assertFalse(map.entrySet().equals(map2.entrySet()));
        assertFalse(map2.entrySet().equals(map.entrySet()));
    }

    // equals
    @Test
    public void testEqualsNewMapping() {
        map.put(new Box(k1a), new Box(v1a));

        assertFalse(map.equals(map2));
        assertFalse(map2.equals(map));

        assertFalse(map.keySet().equals(map2.keySet()));
        assertFalse(map2.keySet().equals(map.keySet()));

        assertFalse(map.entrySet().equals(map2.entrySet()));
        assertFalse(map2.entrySet().equals(map.entrySet()));
    }

    // equals
    @Test
    public void testEqualsMissingMapping() {
        var tmp = new IdentityHashMap<Box, Box>();
        tmp.put(k1a, v1a);
        tmp.put(k1b, v1b);

        assertFalse(map.equals(tmp));
        assertFalse(tmp.equals(map));

        assertFalse(map.keySet().equals(tmp.keySet()));
        assertFalse(tmp.keySet().equals(map.keySet()));

        assertFalse(map.entrySet().equals(tmp.entrySet()));
        assertFalse(tmp.entrySet().equals(map.entrySet()));
    }

    // keySet equals, contains
    @Test
    public void testKeySet() {
        Set<Box> keySet = map.keySet();

        checkElements(keySet, k1a, k1b, k2);
        assertFalse(keySet.contains(new Box(k1a)));
        assertTrue(map.keySet().equals(map2.keySet()));
        assertTrue(map2.keySet().equals(map.keySet()));
    }

    // keySet remove
    @Test
    public void testKeySetNoRemove() {
        Set<Box> keySet = map.keySet();
        boolean r = keySet.remove(new Box(k1a));

        assertFalse(r);
        checkElements(keySet, k1a, k1b, k2);
        checkEntries(map.entrySet(), entry(k1a, v1a),
                                     entry(k1b, v1b),
                                     entry(k2, v2));
        assertTrue(map.keySet().equals(map2.keySet()));
        assertTrue(map2.keySet().equals(map.keySet()));
    }

    // keySet remove
    @Test
    public void testKeySetRemove() {
        Set<Box> keySet = map.keySet();
        boolean r = keySet.remove(k1a);

        assertTrue(r);
        checkElements(keySet, k1b, k2);
        checkEntries(map.entrySet(), entry(k1b, v1b),
                                     entry(k2, v2));
        assertFalse(map.keySet().equals(map2.keySet()));
        assertFalse(map2.keySet().equals(map.keySet()));
    }

    // values
    @Test
    public void testValues() {
        Collection<Box> values = map.values();
        checkElements(values, v1a, v1b, v2);
        assertFalse(values.contains(new Box(v1a)));
    }

    // values remove
    @Test
    public void testValuesNoRemove() {
        Collection<Box> values = map.values();
        boolean r = values.remove(new Box(v1a));

        assertFalse(r);
        checkElements(values, v1a, v1b, v2);
        checkEntries(map.entrySet(), entry(k1a, v1a),
                                     entry(k1b, v1b),
                                     entry(k2, v2));
    }

    // values remove
    @Test
    public void testValuesRemove() {
        Collection<Box> values = map.values();
        boolean r = values.remove(v1a);

        assertTrue(r);
        checkElements(values, v1b, v2);
        checkEntries(map.entrySet(), entry(k1b, v1b),
                                     entry(k2, v2));
    }

    // entrySet equals, contains
    @Test
    public void testEntrySet() {
        Set<Map.Entry<Box,Box>> entrySet = map.entrySet();

        assertFalse(entrySet.contains(entry(new Box(k1a), v1a)));
        assertFalse(entrySet.contains(entry(k1b, new Box(v1b))));
        assertFalse(entrySet.contains(entry(new Box(k2), new Box(v2))));
        assertTrue(map.entrySet().equals(map2.entrySet()));
        checkEntries(entrySet, entry(k1a, v1a),
                               entry(k1b, v1b),
                               entry(k2, v2));
    }

    // entrySet remove
    @Test
    public void testEntrySetNoRemove() {
        Set<Map.Entry<Box, Box>> entrySet = map.entrySet();
        boolean r1 = entrySet.remove(entry(new Box(k1a), v1a));
        boolean r2 = entrySet.remove(entry(k1a, new Box(v1a)));

        assertFalse(r1);
        assertFalse(r2);
        assertTrue(entrySet.equals(map2.entrySet()));
        checkEntries(entrySet, entry(k1a, v1a),
                               entry(k1b, v1b),
                               entry(k2, v2));
    }

    // entrySet remove
    @Test
    public void testEntrySetRemove() {
        Set<Map.Entry<Box, Box>> entrySet = map.entrySet();
        boolean r = entrySet.remove(Map.entry(k1a, v1a));

        assertTrue(r);
        assertFalse(entrySet.equals(map2.entrySet()));
        assertFalse(map.entrySet().equals(map2.entrySet()));
        checkEntries(entrySet, entry(k1b, v1b),
                               entry(k2, v2));
        checkEntries(map.entrySet(), entry(k1b, v1b),
                                     entry(k2, v2));
    }

    // put
    @Test
    public void testPutNew() {
        Box newKey = new Box(k1a);
        Box newVal = new Box(v1a);
        Box r = map.put(newKey, newVal);

        assertNull(r);
        checkEntries(map.entrySet(), entry(k1a, v1a),
                                     entry(k1b, v1b),
                                     entry(k2, v2),
                                     entry(newKey, newVal));
    }

    // put
    @Test
    public void testPutOverwrite() {
        Box newVal = new Box(v1a);
        Box r = map.put(k1a, newVal);

        assertSame(r, v1a);
        checkEntries(map.entrySet(), entry(k1a, newVal),
                                     entry(k1b, v1b),
                                     entry(k2, v2));
    }

    // putAll
    @Test
    public void testPutAll() {
        Box newKey  = new Box(k1a);
        Box newVal  = new Box(v1a);
        Box newValB = new Box(v1b);
        var argMap = new IdentityHashMap<Box, Box>();
        argMap.put(newKey, newVal); // new entry
        argMap.put(k1b, newValB);   // will overwrite value
        map.putAll(argMap);

        checkEntries(map.entrySet(), entry(k1a, v1a),
                                     entry(k1b, newValB),
                                     entry(k2, v2),
                                     entry(newKey, newVal));
    }

    // putIfAbsent
    @Test
    public void testPutIfAbsentNoop() {
        Box r = map.putIfAbsent(k1a, new Box(v1a)); // no-op

        assertSame(r, v1a);
        checkEntries(map.entrySet(), entry(k1a, v1a),
                                     entry(k1b, v1b),
                                     entry(k2, v2));
    }

    // putIfAbsent
    @Test
    public void testPutIfAbsentAddsNew() {
        Box newKey = new Box(k1a);
        Box newVal = new Box(v1a);
        Box r = map.putIfAbsent(newKey, newVal); // adds new entry

        assertNull(r);
        checkEntries(map.entrySet(), entry(k1a, v1a),
                                     entry(k1b, v1b),
                                     entry(k2, v2),
                                     entry(newKey, newVal));
    }

    // remove(Object)
    @Test
    public void testRemoveKey() {
        Box r = map.remove(k1b);

        assertSame(r, v1b);
        checkEntries(map.entrySet(), entry(k1a, v1a),
                                     entry(k2, v2));
    }

    // remove(Object, Object) absent key, absent value
    @Test
    public void testRemoveAA() {
        Box k1c = new Box(k1a);
        Box v1c = new Box(v1a);
        assertFalse(map.remove(k1c, v1c));
        checkEntries(map.entrySet(),
                entry(k1a, v1a),
                entry(k1b, v1b),
                entry(k2, v2));
    }

    // remove(Object, Object) absent key, present value
    @Test
    public void testRemoveAV() {
        Box k1c = new Box(k1a);
        assertFalse(map.remove(k1c, v1a));
        checkEntries(map.entrySet(),
                     entry(k1a, v1a),
                     entry(k1b, v1b),
                     entry(k2, v2));
    }

    // remove(Object, Object) present key, absent value
    @Test
    public void testRemoveKA() {
        Box v1c = new Box(v1a);
        assertFalse(map.remove(k1a, v1c));
        checkEntries(map.entrySet(),
                entry(k1a, v1a),
                entry(k1b, v1b),
                entry(k2, v2));
    }

    // remove(Object, Object) present key, present value
    @Test
    public void testRemoveKV() {
        assertTrue(map.remove(k1a, v1a));
        checkEntries(map.entrySet(),
                entry(k1b, v1b),
                entry(k2, v2));
    }

    // replace(K, V, V) absent key, absent oldValue
    @Test
    public void testReplaceAA() {
        Box k1c = new Box(k1a);
        Box v1c = new Box(v1a);
        Box newVal = new Box(v2);
        assertFalse(map.replace(k1c, v1c, newVal));
        checkEntries(map.entrySet(),
                entry(k1a, v1a),
                entry(k1b, v1b),
                entry(k2, v2));
    }

    // replace(K, V, V) absent key, present oldValue
    @Test
    public void testReplaceAV() {
        Box k1c = new Box(k1a);
        Box newVal = new Box(v2);
        assertFalse(map.replace(k1c, v1a, newVal));
        checkEntries(map.entrySet(),
                entry(k1a, v1a),
                entry(k1b, v1b),
                entry(k2, v2));
    }

    // replace(K, V, V) present key, absent oldValue
    @Test
    public void testReplaceKA() {
        Box v1c = new Box(v1a);
        Box newVal = new Box(v2);
        assertFalse(map.replace(k1a, v1c, newVal));
        checkEntries(map.entrySet(),
                entry(k1a, v1a),
                entry(k1b, v1b),
                entry(k2, v2));
    }

    // replace(K, V, V) present key, present oldValue
    @Test
    public void testReplaceKV() {
        Box newVal = new Box(v2);
        assertTrue(map.replace(k1a, v1a, newVal));
        checkEntries(map.entrySet(),
                entry(k1a, newVal),
                entry(k1b, v1b),
                entry(k2, v2));
    }

    // AN: key absent, remappingFunction returns null
    @Test
    public void testComputeAN() {
        Box newKey = new Box(k1a);
        Box r = map.compute(newKey, (k, v) -> null);

        assertNull(r);
        checkEntries(map.entrySet(), entry(k1a, v1a),
                                     entry(k1b, v1b),
                                     entry(k2, v2));
    }

    // AV: key absent, remappingFunction returns non-null value
    @Test
    public void testComputeAV() {
        Box newKey = new Box(k1a);
        Box newVal = new Box(v1a);
        Box r = map.compute(newKey, (k, v) -> newVal);

        assertSame(r, newVal);
        checkEntries(map.entrySet(), entry(k1a, v1a),
                                     entry(k1b, v1b),
                                     entry(k2, v2),
                                     entry(newKey, newVal));
    }

    // PN: key present, remappingFunction returns null
    @Test
    public void testComputePN() {
        Box r = map.compute(k1a, (k, v) -> null);

        assertNull(r);
        checkEntries(map.entrySet(), entry(k1b, v1b),
                                     entry(k2, v2));
    }

    // PV: key present, remappingFunction returns non-null value
    @Test
    public void testComputePV() {
        Box newVal = new Box(v1a);
        Box r = map.compute(k1a, (k, v) -> newVal);

        assertSame(r, newVal);
        checkEntries(map.entrySet(), entry(k1a, newVal),
                                     entry(k1b, v1b),
                                     entry(k2, v2));
    }

    // computeIfAbsent
    @Test
    public void testComputeIfAbsentIsCalled() {
        boolean[] called = new boolean[1];
        Box newKey = new Box(k1a);
        Box newVal = new Box(v1a);
        Box r = map.computeIfAbsent(newKey, k -> { called[0] = true; return newVal; });

        assertSame(r, newVal);
        assertTrue(called[0]);
        checkEntries(map.entrySet(), entry(k1a, v1a),
                                     entry(k1b, v1b),
                                     entry(k2, v2),
                                     entry(newKey, newVal));
    }

    // computeIfAbsent
    @Test
    public void testComputeIfAbsentNotCalled() {
        boolean[] called = new boolean[1];
        Box r = map.computeIfAbsent(k1a, k -> { called[0] = true; return null; });

        assertSame(r, v1a);
        assertFalse(called[0]);
        checkEntries(map.entrySet(), entry(k1a, v1a),
                                     entry(k1b, v1b),
                                     entry(k2, v2));
    }

    // computeIfAbsent
    @Test
    public void testComputeIfAbsentNullReturn() {
        boolean[] called = new boolean[1];
        Box newKey = new Box(k1a);
        Box r = map.computeIfAbsent(newKey, k -> { called[0] = true; return null; });

        assertNull(r);
        assertTrue(called[0]);
        checkEntries(map.entrySet(), entry(k1a, v1a),
                                     entry(k1b, v1b),
                                     entry(k2, v2));
    }

    // computeIfPresent
    @Test
    public void testComputeIfPresentIsCalled() {
        boolean[] called = new boolean[1];
        Box newVal = new Box(v1a);
        Box r = map.computeIfPresent(k1a, (k, v) -> { called[0] = true; return newVal; });

        assertSame(r, newVal);
        assertTrue(called[0]);
        checkEntries(map.entrySet(), entry(k1a, newVal),
                                     entry(k1b, v1b),
                                     entry(k2, v2));
    }

    // computeIfPresent
    @Test
    public void testComputeIfPresentNotCalled() {
        boolean[] called = new boolean[1];
        Box r = map.computeIfPresent(new Box(k1a), (k, v) -> { called[0] = true; return null; });

        assertNull(r);
        assertFalse(called[0]);
        checkEntries(map.entrySet(), entry(k1a, v1a),
                                     entry(k1b, v1b),
                                     entry(k2, v2));
    }

    // computeIfPresent
    @Test
    public void testComputeIfPresentNullReturn() {
        boolean[] called = new boolean[1];
        Box r = map.computeIfPresent(k1a, (k, v) -> { called[0] = true; return null; });

        assertNull(r);
        assertTrue(called[0]);
        checkEntries(map.entrySet(), entry(k1b, v1b),
                                     entry(k2, v2));
    }

    // merge
    @Test
    public void testMergeAbsent() {
        boolean[] called = new boolean[1];
        Box newKey = new Box(k1a);
        Box newVal = new Box(v1a);
        Box r = map.merge(newKey, newVal, (v1, v2) -> { called[0] = true; return newVal; });

        assertSame(r, newVal);
        assertFalse(called[0]);
        checkEntries(map.entrySet(), entry(k1a, v1a),
                                     entry(k1b, v1b),
                                     entry(k2, v2),
                                     entry(newKey, newVal));
    }

    // merge
    @Test
    public void testMergePresent() {
        boolean[] called = new boolean[1];
        Box val2 = new Box(47);
        Box[] mergedVal = new Box[1];
        Box r = map.merge(k1a, val2, (v1, v2) -> {
            called[0] = true;
            mergedVal[0] = new Box(v1.i + v2.i);
            return mergedVal[0];
        });

        assertSame(r, mergedVal[0]);
        assertTrue(called[0]);
        checkEntries(map.entrySet(), entry(k1a, mergedVal[0]),
                                     entry(k1b, v1b),
                                     entry(k2, v2));
    }

    // forEach
    @Test
    public void testForEach() {
        @SuppressWarnings("unchecked")
        List<Map.Entry<Box, Box>> entries = new ArrayList<>();
        map.forEach((k, v) -> entries.add(entry(k, v)));
        checkEntries(entries, entry(k1a, v1a),
                              entry(k1b, v1b),
                              entry(k2, v2));
    }

    // replaceAll
    @Test
    public void testReplaceAll() {
        List<Map.Entry<Box, Box>> replacements = new ArrayList<>();

        map.replaceAll((k, v) -> {
            Box v1 = new Box(v);
            replacements.add(entry(k, v1));
            return v1;
        });

        @SuppressWarnings("unchecked")
        var replacementArray = (Map.Entry<Box, Box>[]) replacements.toArray(Map.Entry[]::new);
        checkEntries(map.entrySet(), replacementArray);
    }
}