package org.infinispan.util;

import org.infinispan.commons.equivalence.ByteArrayEquivalence;
import org.infinispan.commons.equivalence.Equivalence;
import org.infinispan.commons.equivalence.EquivalentHashMap;
import org.testng.annotations.Test;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import static org.testng.AssertJUnit.*;

/**
 * Tests that {@link EquivalentHashMap} is functionally correct.
 *
 * @author Galder Zamarreño
 * @since 5.3
 */
@Test(groups = "functional", testName = "util.EquivalentHashMapTest")
public class EquivalentHashMapTest {

   protected static final Equivalence<byte[]> EQUIVALENCE =
         new DebugByteArrayEquivalence();

   public void testJdkMapExpectations() {
      byteArrayGet(createStandardConcurrentMap(), false);
      byteArrayContainsKey(createStandardConcurrentMap(), false);
      byteArrayRemove(createStandardConcurrentMap(), false);
      byteArrayPutSameValueTwice(createStandardConcurrentMap(), false);
      byteArrayPutAll(createStandardConcurrentMap(), 3);
      byteArrayContainsValue(createStandardConcurrentMap(), false);
      byteArrayEquals(createStandardConcurrentMap(), createStandardConcurrentMap(), false);
      byteArrayEntryEquality(createStandardConcurrentMap(), createStandardConcurrentMap(), false);
      byteArrayValuesContains(createStandardConcurrentMap(), false);
      byteArrayValuesRemove(createStandardConcurrentMap(), false);
      byteArrayKeySetContains(createStandardConcurrentMap(), false);
      byteArrayKeySetRemove(createStandardConcurrentMap(), false);
   }

   public void testByteArrayGet() {
      byteArrayGet(createComparingConcurrentMap(), true);
   }

   public void testByteArrayContainsKey() {
      byteArrayContainsKey(createComparingConcurrentMap(), true);
   }

   public void testByteArrayRemove() {
      byteArrayRemove(createComparingConcurrentMap(), true);
   }

   public void testByteArrayPutSameValueTwice() {
      byteArrayPutSameValueTwice(createComparingConcurrentMap(), true);
   }

   public void testByteArrayPutAll() {
      byteArrayPutAll(createComparingConcurrentMap(), 2);
   }

   public void testByteArrayContainsValue() {
      byteArrayContainsValue(createComparingConcurrentMap(), true);
   }

   public void testByteArrayEquals() {
      byteArrayEquals(createComparingConcurrentMap(), createComparingConcurrentMap(), true);
   }

   public void testByteArrayEntryEquality() {
      byteArrayEntryEquality(createComparingConcurrentMap(), createComparingConcurrentMap(), true);
   }

   public void testByteArrayValuesContains() {
      byteArrayValuesContains(createComparingConcurrentMap(), true);
   }

   public void testByteArrayValuesRemove() {
      byteArrayValuesRemove(createComparingConcurrentMap(), true);
   }

   public void testByteArrayEntrySetContains() {
      byteArrayEntrySetContains(createComparingConcurrentMap(), createComparingConcurrentMap(), true);
   }

   public void testByteArrayEntrySetRemove() {
      byteArrayEntrySetRemove(createComparingConcurrentMap(), createComparingConcurrentMap(), true);
   }

   public void testByteArrayKeySetContains() {
      byteArrayKeySetContains(createComparingConcurrentMap(), true);
   }

   public void testByteArrayKeySetRemove() {
      byteArrayKeySetRemove(createComparingConcurrentMap(), true);
   }

   protected void byteArrayGet(
         Map<byte[], byte[]> map, boolean expectFound) {
      byte[] key = {1, 2, 3};
      byte[] value = {4, 5, 6};
      map.put(key, value);
      byte[] lookupKey = {1, 2, 3}; // on purpose, different instance required
      if (expectFound)
         assertTrue(String.format(
               "Expected key=%s to return value=%s", str(lookupKey), str(value)),
               Arrays.equals(value, map.get(lookupKey)));
      else
         assertNull(map.get(lookupKey));
   }

   protected void byteArrayContainsKey(
         Map<byte[], byte[]> map, boolean expectFound) {
      byte[] key = {1, 2, 3};
      byte[] value = {4, 5, 6};
      map.put(key, value);
      byte[] lookupKey = {1, 2, 3}; // on purpose, different instance required
      if (expectFound)
         assertTrue(String.format(
               "Expected key=%s to be in collection", str(lookupKey)),
               map.containsKey(lookupKey));
      else
         assertNull(map.get(lookupKey));
   }

   protected void byteArrayRemove(
         Map<byte[], byte[]> map, boolean expectRemove) {
      byte[] key = {1, 2, 3};
      byte[] value = {4, 5, 6};
      map.put(key, value);
      byte[] removeKey = {1, 2, 3}; // on purpose, different instance required
      if (expectRemove)
         assertTrue(String.format(
               "Expected key=%s to be removed", str(removeKey)),
               Arrays.equals(value, map.remove(removeKey)));
      else
         assertNull(map.get(removeKey));
   }

   protected void byteArrayPutSameValueTwice(
         Map<byte[], byte[]> map, boolean expectFound) {
      byte[] key = {1, 2, 3};
      byte[] value = {4, 5, 6};
      map.put(key, value);
      byte[] putKey = {1, 2, 3}; // on purpose, different instance required
      byte[] sameValue = {4, 5, 6}; // on purpose, different instance required
      if (expectFound)
         assertTrue(String.format(
               "Expected putting %s again on key=%s to return value=%s",
               str(sameValue), str(putKey), str(value)),
               Arrays.equals(value, map.put(putKey, sameValue)));
      else
         assertNull(map.put(putKey, sameValue));
   }

   protected void byteArrayPutAll(
         Map<byte[], byte[]> map, int expectCount) {
      byte[] key = {1, 2, 3};
      byte[] value = {4, 5, 6};
      map.put(key, value);

      Map<byte[], byte[]> data = new HashMap<byte[], byte[]>();
      data.put(new byte[]{1, 2, 3}, new byte[]{7, 8, 9});
      data.put(new byte[]{11, 22, 33}, new byte[]{44, 55, 66});

      map.putAll(data);
      assertEquals(expectCount, map.size());
   }

   protected void byteArrayContainsValue(
         Map<byte[], byte[]> map, boolean expectFound) {
      byte[] key = {1, 2, 3};
      byte[] value = {4, 5, 6};
      map.put(key, value);
      byte[] lookupValue = {4, 5, 6}; // on purpose, different instance required
      if (expectFound)
         assertTrue(String.format(
               "Expected value=%s lookup to return value=%s", str(lookupValue), str(value)),
               map.containsValue(lookupValue));
      else
         assertFalse(map.containsValue(lookupValue));
   }

   protected void byteArrayEquals(Map<byte[], byte[]> map1,
         Map<byte[], byte[]> map2, boolean expectEquals) {
      byte[] key = {11, 22, 33};
      map1.put(new byte[]{1, 2, 3}, new byte[]{4, 5, 6});
      map1.put(key, new byte[]{7, 8, 9});
      map2.put(new byte[]{1, 2, 3}, new byte[]{4, 5, 6});
      map2.put(key, new byte[]{7, 8, 9});

      if (expectEquals) {
         assertEquals(map1, map2);
         assertEquals(map1.hashCode(), map2.hashCode());
      } else
         assertFalse(String.format(
               "Expected map1=%s to be distinct to map2=%s", map1.toString(), map2.toString()),
               map1.equals(map2));
   }

   protected void byteArrayEntryEquality(Map<byte[], byte[]> map1,
         Map<byte[], byte[]> map2, boolean expectEquals) {
      map1.put(new byte[]{1, 2, 3}, new byte[]{4, 5, 6});
      map2.put(new byte[]{1, 2, 3}, new byte[]{4, 5, 6});

      Map.Entry<byte[], byte[]> entry1 = map1.entrySet().iterator().next();
      Map.Entry<byte[], byte[]> entry2 = map2.entrySet().iterator().next();
      if (expectEquals) {
         assertEquals(entry1, entry2);
         assertEquals(entry1.hashCode(), entry2.hashCode());
      } else {
         assertFalse(String.format(
               "Expected entry1=%s to be distinct to entry2=%s", entry1, entry2),
               entry1.equals(entry2));
      }
   }

   protected void byteArrayValuesContains(
         Map<byte[], byte[]> map, boolean expectContains) {
      byte[] key = {1, 2, 3};
      byte[] value = {4, 5, 6};
      map.put(key, value);
      byte[] containsValue = {4, 5, 6}; // on purpose, different instance required
      Collection<byte[]> values = map.values();
      if (expectContains)
         assertTrue(String.format(
               "Expected value=%s to be contained in values=%s", str(containsValue), values),
               values.contains(containsValue));
      else
         assertFalse(values.contains(containsValue));
   }

   protected void byteArrayValuesRemove(
         Map<byte[], byte[]> map, boolean expectRemove) {
      byte[] key = {1, 2, 3};
      byte[] value = {4, 5, 6};
      map.put(key, value);
      byte[] removeValue = {4, 5, 6}; // on purpose, different instance required
      Collection<byte[]> values = map.values();
      if (expectRemove)
         assertTrue(String.format(
               "Expected value=%s to be removed from values=%s", str(removeValue), values),
               values.remove(removeValue));
      else
         assertFalse(values.remove(removeValue));
   }

   protected void byteArrayEntrySetContains(Map<byte[], byte[]> map1,
         Map<byte[], byte[]> map2, boolean expectContains) {
      map1.put(new byte[]{1, 2, 3}, new byte[]{4, 5, 6});
      map2.put(new byte[]{1, 2, 3}, new byte[]{4, 5, 6});
      Set<Map.Entry<byte[],byte[]>> entries1 = map1.entrySet();
      Set<Map.Entry<byte[],byte[]>> entries2 = map2.entrySet();
      Map.Entry<byte[], byte[]> entry1 = entries1.iterator().next();
      if (expectContains)
         assertTrue(String.format(
               "Expected entry=%s to be contained in entries=%s", entry1, entries2),
               entries2.contains(entry1));
      else
         assertFalse(entries2.contains(entry1));
   }

   protected void byteArrayEntrySetRemove(Map<byte[], byte[]> map1,
         Map<byte[], byte[]> map2, boolean expectRemove) {
      map1.put(new byte[]{1, 2, 3}, new byte[]{4, 5, 6});
      map2.put(new byte[]{1, 2, 3}, new byte[]{4, 5, 6});
      Set<Map.Entry<byte[],byte[]>> entries1 = map1.entrySet();
      Set<Map.Entry<byte[],byte[]>> entries2 = map2.entrySet();
      Map.Entry<byte[], byte[]> entry1 = entries1.iterator().next();
      if (expectRemove)
         assertTrue(String.format(
               "Expected entry=%s to be removed from entries=%s", entry1, entries2),
               entries2.remove(entry1));
      else
         assertFalse(entries2.remove(entry1));
   }

   protected void byteArrayKeySetContains(
         Map<byte[], byte[]> map, boolean expectContains) {
      byte[] key = {1, 2, 3};
      byte[] value = {4, 5, 6};
      map.put(key, value);
      byte[] containsKey = {1, 2, 3}; // on purpose, different instance required
      Set<byte[]> keys = map.keySet();
      if (expectContains)
         assertTrue(String.format(
               "Expected key=%s to be contained in keys=%s", str(containsKey), keys),
               keys.contains(containsKey));
      else
         assertFalse(keys.contains(containsKey));
   }

   protected void byteArrayKeySetRemove(
         Map<byte[], byte[]> map, boolean expectRemove) {
      byte[] key = {1, 2, 3};
      byte[] value = {4, 5, 6};
      map.put(key, value);
      byte[] removeKey = {1, 2, 3}; // on purpose, different instance required
      Set<byte[]> keys = map.keySet();
      if (expectRemove)
         assertTrue(String.format(
               "Expected value=%s to be removed from keys=%s", str(removeKey), keys),
               keys.remove(removeKey));
      else
         assertFalse(keys.remove(removeKey));
   }

   protected Map<byte[], byte[]> createStandardConcurrentMap() {
      return new HashMap<byte[], byte[]>();
   }

   protected Map<byte[], byte[]> createComparingConcurrentMap() {
      return new EquivalentHashMap<byte[], byte[]>(EQUIVALENCE, EQUIVALENCE);
   }

   protected String str(byte[] array) {
      // Ignore IDE warning about hashCode() call!!
      return array != null
            ? Arrays.toString(array) + "@" + Integer.toHexString(array.hashCode())
            : "null";
   }

   private static class DebugByteArrayEquivalence implements Equivalence<byte[]> {

      final Equivalence<byte[]> delegate = ByteArrayEquivalence.INSTANCE;

      @Override
      public int hashCode(Object obj) {
         return delegate.hashCode(obj);
      }

      @Override
      public boolean equals(byte[] obj, Object otherObj) {
         return delegate.equals(obj, otherObj);
      }

      @Override
      public String toString(Object obj) {
         return delegate.toString(obj) + "@" + Integer.toHexString(obj.hashCode());
      }

      @Override
      public boolean isComparable(Object obj) {
         return delegate.isComparable(obj);
      }

      @Override
      public int compare(byte[] obj, byte[] otherObj) {
         return delegate.compare(obj, otherObj);
      }

   }

}
