/*
 * Copyright (C) 2011 The Guava Authors
 *
 * Licensed 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 com.google.common.collect;

import static com.google.common.collect.MapMakerInternalMap.DRAIN_THRESHOLD;
import static com.google.common.truth.Truth.assertThat;

import com.google.common.base.Equivalence;
import com.google.common.collect.MapMakerInternalMap.InternalEntry;
import com.google.common.collect.MapMakerInternalMap.Segment;
import com.google.common.collect.MapMakerInternalMap.Strength;
import com.google.common.collect.MapMakerInternalMap.WeakValueEntry;
import com.google.common.collect.MapMakerInternalMap.WeakValueReference;
import com.google.common.testing.NullPointerTester;
import java.lang.ref.Reference;
import java.util.concurrent.atomic.AtomicReferenceArray;
import junit.framework.TestCase;

/**
 * @author Charles Fry
 */
@SuppressWarnings("deprecation") // many tests of deprecated methods
public class MapMakerInternalMapTest extends TestCase {

  static final int SMALL_MAX_SIZE = DRAIN_THRESHOLD * 5;

  private static <K, V>
      MapMakerInternalMap<K, V, ? extends InternalEntry<K, V, ?>, ? extends Segment<K, V, ?, ?>>
          makeMap(MapMaker maker) {
    return MapMakerInternalMap.create(maker);
  }

  private static MapMaker createMapMaker() {
    MapMaker maker = new MapMaker();
    maker.useCustomMap = true;
    return maker;
  }

  // constructor tests

  public void testDefaults() {
    MapMakerInternalMap<Object, Object, ?, ?> map = makeMap(createMapMaker());

    assertSame(Strength.STRONG, map.keyStrength());
    assertSame(Strength.STRONG, map.valueStrength());
    assertSame(map.keyStrength().defaultEquivalence(), map.keyEquivalence);
    assertSame(map.valueStrength().defaultEquivalence(), map.valueEquivalence());

    assertThat(map.entryHelper)
        .isInstanceOf(MapMakerInternalMap.StrongKeyStrongValueEntry.Helper.class);

    assertEquals(4, map.concurrencyLevel);

    // concurrency level
    assertThat(map.segments).hasLength(4);
    // initial capacity / concurrency level
    assertEquals(16 / map.segments.length, map.segments[0].table.length());
  }

  public void testSetKeyEquivalence() {
    Equivalence<Object> testEquivalence = new Equivalence<Object>() {
      @Override
      protected boolean doEquivalent(Object a, Object b) {
        return false;
      }

      @Override
      protected int doHash(Object t) {
        return 0;
      }
    };

    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(createMapMaker().keyEquivalence(testEquivalence));
    assertSame(testEquivalence, map.keyEquivalence);
    assertSame(map.valueStrength().defaultEquivalence(), map.valueEquivalence());
  }

  public void testSetConcurrencyLevel() {
    // round up to nearest power of two

    checkConcurrencyLevel(1, 1);
    checkConcurrencyLevel(2, 2);
    checkConcurrencyLevel(3, 4);
    checkConcurrencyLevel(4, 4);
    checkConcurrencyLevel(5, 8);
    checkConcurrencyLevel(6, 8);
    checkConcurrencyLevel(7, 8);
    checkConcurrencyLevel(8, 8);
  }

  private static void checkConcurrencyLevel(int concurrencyLevel, int segmentCount) {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(createMapMaker().concurrencyLevel(concurrencyLevel));
    assertThat(map.segments).hasLength(segmentCount);
  }

  public void testSetInitialCapacity() {
    // share capacity over each segment, then round up to nearest power of two

    checkInitialCapacity(1, 0, 1);
    checkInitialCapacity(1, 1, 1);
    checkInitialCapacity(1, 2, 2);
    checkInitialCapacity(1, 3, 4);
    checkInitialCapacity(1, 4, 4);
    checkInitialCapacity(1, 5, 8);
    checkInitialCapacity(1, 6, 8);
    checkInitialCapacity(1, 7, 8);
    checkInitialCapacity(1, 8, 8);

    checkInitialCapacity(2, 0, 1);
    checkInitialCapacity(2, 1, 1);
    checkInitialCapacity(2, 2, 1);
    checkInitialCapacity(2, 3, 2);
    checkInitialCapacity(2, 4, 2);
    checkInitialCapacity(2, 5, 4);
    checkInitialCapacity(2, 6, 4);
    checkInitialCapacity(2, 7, 4);
    checkInitialCapacity(2, 8, 4);

    checkInitialCapacity(4, 0, 1);
    checkInitialCapacity(4, 1, 1);
    checkInitialCapacity(4, 2, 1);
    checkInitialCapacity(4, 3, 1);
    checkInitialCapacity(4, 4, 1);
    checkInitialCapacity(4, 5, 2);
    checkInitialCapacity(4, 6, 2);
    checkInitialCapacity(4, 7, 2);
    checkInitialCapacity(4, 8, 2);
  }

  private static void checkInitialCapacity(
      int concurrencyLevel, int initialCapacity, int segmentSize) {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(
            createMapMaker().concurrencyLevel(concurrencyLevel).initialCapacity(initialCapacity));
    for (int i = 0; i < map.segments.length; i++) {
      assertEquals(segmentSize, map.segments[i].table.length());
    }
  }

  public void testSetMaximumSize() {
    // vary maximumSize wrt concurrencyLevel

    for (int maxSize = 1; maxSize < 8; maxSize++) {
      checkMaximumSize(1, 8, maxSize);
      checkMaximumSize(2, 8, maxSize);
      checkMaximumSize(4, 8, maxSize);
      checkMaximumSize(8, 8, maxSize);
    }

    checkMaximumSize(1, 8, Integer.MAX_VALUE);
    checkMaximumSize(2, 8, Integer.MAX_VALUE);
    checkMaximumSize(4, 8, Integer.MAX_VALUE);
    checkMaximumSize(8, 8, Integer.MAX_VALUE);

    // vary initial capacity wrt maximumSize

    for (int capacity = 0; capacity < 8; capacity++) {
      checkMaximumSize(1, capacity, 4);
      checkMaximumSize(2, capacity, 4);
      checkMaximumSize(4, capacity, 4);
      checkMaximumSize(8, capacity, 4);
    }
  }

  private static void checkMaximumSize(int concurrencyLevel, int initialCapacity, int maxSize) {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(
            createMapMaker().concurrencyLevel(concurrencyLevel).initialCapacity(initialCapacity));
    int totalCapacity = 0;
    for (int i = 0; i < map.segments.length; i++) {
      totalCapacity += map.segments[i].maxSegmentSize;
    }
    assertTrue("totalCapcity=" + totalCapacity + ", maxSize=" + maxSize, totalCapacity <= maxSize);
  }

  public void testSetWeakKeys() {
    MapMakerInternalMap<Object, Object, ?, ?> map = makeMap(createMapMaker().weakKeys());
    checkStrength(map, Strength.WEAK, Strength.STRONG);
    assertThat(map.entryHelper)
        .isInstanceOf(MapMakerInternalMap.WeakKeyStrongValueEntry.Helper.class);
  }

  public void testSetWeakValues() {
    MapMakerInternalMap<Object, Object, ?, ?> map = makeMap(createMapMaker().weakValues());
    checkStrength(map, Strength.STRONG, Strength.WEAK);
    assertThat(map.entryHelper)
        .isInstanceOf(MapMakerInternalMap.StrongKeyWeakValueEntry.Helper.class);
  }

  private static void checkStrength(
      MapMakerInternalMap<Object, Object, ?, ?> map, Strength keyStrength, Strength valueStrength) {
    assertSame(keyStrength, map.keyStrength());
    assertSame(valueStrength, map.valueStrength());
    assertSame(keyStrength.defaultEquivalence(), map.keyEquivalence);
    assertSame(valueStrength.defaultEquivalence(), map.valueEquivalence());
  }

  // Segment core tests

  public void testNewEntry() {
    for (MapMaker maker : allWeakValueStrengthMakers()) {
      MapMakerInternalMap<Object, Object, ?, ?> map = makeMap(maker);
      Segment<Object, Object, ?, ?> segment = map.segments[0];

      Object keyOne = new Object();
      Object valueOne = new Object();
      int hashOne = map.hash(keyOne);
      InternalEntry<Object, Object, ?> entryOne = segment.newEntryForTesting(keyOne, hashOne, null);
      WeakValueReference<Object, Object, ?> valueRefOne =
          segment.newWeakValueReferenceForTesting(entryOne, valueOne);
      assertSame(valueOne, valueRefOne.get());
      segment.setWeakValueReferenceForTesting(entryOne, valueRefOne);

      assertSame(keyOne, entryOne.getKey());
      assertEquals(hashOne, entryOne.getHash());
      assertNull(entryOne.getNext());
      assertSame(valueRefOne, segment.getWeakValueReferenceForTesting(entryOne));

      Object keyTwo = new Object();
      Object valueTwo = new Object();
      int hashTwo = map.hash(keyTwo);

      InternalEntry<Object, Object, ?> entryTwo =
          segment.newEntryForTesting(keyTwo, hashTwo, entryOne);
      WeakValueReference<Object, Object, ?> valueRefTwo =
          segment.newWeakValueReferenceForTesting(entryTwo, valueTwo);
      assertSame(valueTwo, valueRefTwo.get());
      segment.setWeakValueReferenceForTesting(entryTwo, valueRefTwo);

      assertSame(keyTwo, entryTwo.getKey());
      assertEquals(hashTwo, entryTwo.getHash());
      assertSame(entryOne, entryTwo.getNext());
      assertSame(valueRefTwo, segment.getWeakValueReferenceForTesting(entryTwo));
    }
  }

  public void testCopyEntry() {
    for (MapMaker maker : allWeakValueStrengthMakers()) {
      MapMakerInternalMap<Object, Object, ?, ?> map = makeMap(maker);
      Segment<Object, Object, ?, ?> segment = map.segments[0];

      Object keyOne = new Object();
      Object valueOne = new Object();
      int hashOne = map.hash(keyOne);
      InternalEntry<Object, Object, ?> entryOne = segment.newEntryForTesting(keyOne, hashOne, null);
      segment.setValueForTesting(entryOne, valueOne);

      Object keyTwo = new Object();
      Object valueTwo = new Object();
      int hashTwo = map.hash(keyTwo);
      InternalEntry<Object, Object, ?> entryTwo = segment.newEntryForTesting(keyTwo, hashTwo, null);
      segment.setValueForTesting(entryTwo, valueTwo);

      InternalEntry<Object, Object, ?> copyOne = segment.copyForTesting(entryOne, null);
      assertSame(keyOne, entryOne.getKey());
      assertEquals(hashOne, entryOne.getHash());
      assertNull(entryOne.getNext());
      assertSame(valueOne, copyOne.getValue());

      InternalEntry<Object, Object, ?> copyTwo = segment.copyForTesting(entryTwo, copyOne);
      assertSame(keyTwo, copyTwo.getKey());
      assertEquals(hashTwo, copyTwo.getHash());
      assertSame(copyOne, copyTwo.getNext());
      assertSame(valueTwo, copyTwo.getValue());
    }
  }

  public void testSegmentGetAndContains() {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(createMapMaker().concurrencyLevel(1).weakValues());
    Segment<Object, Object, ?, ?> segment = map.segments[0];
    // TODO(fry): check recency ordering

    Object key = new Object();
    int hash = map.hash(key);
    Object value = new Object();
    AtomicReferenceArray<? extends InternalEntry<Object, Object, ?>> table = segment.table;
    int index = hash & (table.length() - 1);

    InternalEntry<Object, Object, ?> entry = segment.newEntryForTesting(key, hash, null);
    segment.setValueForTesting(entry, value);

    assertNull(segment.get(key, hash));

    // count == 0
    segment.setTableEntryForTesting(index, entry);
    assertNull(segment.get(key, hash));
    assertFalse(segment.containsKey(key, hash));
    assertFalse(segment.containsValue(value));

    // count == 1
    segment.count++;
    assertSame(value, segment.get(key, hash));
    assertTrue(segment.containsKey(key, hash));
    assertTrue(segment.containsValue(value));
    // don't see absent values now that count > 0
    assertNull(segment.get(new Object(), hash));

    // null key
    InternalEntry<Object, Object, ?> nullEntry = segment.newEntryForTesting(null, hash, entry);
    Object nullValue = new Object();
    WeakValueReference<Object, Object, ?> nullValueRef =
        segment.newWeakValueReferenceForTesting(nullEntry, nullValue);
    segment.setWeakValueReferenceForTesting(nullEntry, nullValueRef);
    segment.setTableEntryForTesting(index, nullEntry);
    // skip the null key
    assertSame(value, segment.get(key, hash));
    assertTrue(segment.containsKey(key, hash));
    assertTrue(segment.containsValue(value));
    assertFalse(segment.containsValue(nullValue));

    // hash collision
    InternalEntry<Object, Object, ?> dummyEntry =
        segment.newEntryForTesting(new Object(), hash, entry);
    Object dummyValue = new Object();
    WeakValueReference<Object, Object, ?> dummyValueRef =
        segment.newWeakValueReferenceForTesting(dummyEntry, dummyValue);
    segment.setWeakValueReferenceForTesting(dummyEntry, dummyValueRef);
    segment.setTableEntryForTesting(index, dummyEntry);
    assertSame(value, segment.get(key, hash));
    assertTrue(segment.containsKey(key, hash));
    assertTrue(segment.containsValue(value));
    assertTrue(segment.containsValue(dummyValue));

    // key collision
    dummyEntry = segment.newEntryForTesting(key, hash, entry);
    dummyValue = new Object();
    dummyValueRef = segment.newWeakValueReferenceForTesting(dummyEntry, dummyValue);
    segment.setWeakValueReferenceForTesting(dummyEntry, dummyValueRef);
    segment.setTableEntryForTesting(index, dummyEntry);
    // returns the most recent entry
    assertSame(dummyValue, segment.get(key, hash));
    assertTrue(segment.containsKey(key, hash));
    assertTrue(segment.containsValue(value));
    assertTrue(segment.containsValue(dummyValue));
  }

  public void testSegmentReplaceValue() {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(createMapMaker().concurrencyLevel(1).weakValues());
    Segment<Object, Object, ?, ?> segment = map.segments[0];
    // TODO(fry): check recency ordering

    Object key = new Object();
    int hash = map.hash(key);
    Object oldValue = new Object();
    Object newValue = new Object();
    AtomicReferenceArray<? extends InternalEntry<Object, Object, ?>> table = segment.table;
    int index = hash & (table.length() - 1);

    InternalEntry<Object, Object, ?> entry = segment.newEntryForTesting(key, hash, null);
    WeakValueReference<Object, Object, ?> oldValueRef =
        segment.newWeakValueReferenceForTesting(entry, oldValue);
    segment.setWeakValueReferenceForTesting(entry, oldValueRef);

    // no entry
    assertFalse(segment.replace(key, hash, oldValue, newValue));
    assertEquals(0, segment.count);

    // same value
    segment.setTableEntryForTesting(index, entry);
    segment.count++;
    assertEquals(1, segment.count);
    assertSame(oldValue, segment.get(key, hash));
    assertTrue(segment.replace(key, hash, oldValue, newValue));
    assertEquals(1, segment.count);
    assertSame(newValue, segment.get(key, hash));

    // different value
    assertFalse(segment.replace(key, hash, oldValue, newValue));
    assertEquals(1, segment.count);
    assertSame(newValue, segment.get(key, hash));

    // cleared
    segment.setWeakValueReferenceForTesting(entry, oldValueRef);
    oldValueRef.clear();
    assertFalse(segment.replace(key, hash, oldValue, newValue));
    assertEquals(0, segment.count);
    assertNull(segment.get(key, hash));
  }

  public void testSegmentReplace() {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(createMapMaker().concurrencyLevel(1).weakValues());
    Segment<Object, Object, ?, ?> segment = map.segments[0];
    // TODO(fry): check recency ordering

    Object key = new Object();
    int hash = map.hash(key);
    Object oldValue = new Object();
    Object newValue = new Object();
    AtomicReferenceArray<? extends InternalEntry<Object, Object, ?>> table = segment.table;
    int index = hash & (table.length() - 1);

    InternalEntry<Object, Object, ?> entry = segment.newEntryForTesting(key, hash, null);
    WeakValueReference<Object, Object, ?> oldValueRef =
        segment.newWeakValueReferenceForTesting(entry, oldValue);
    segment.setWeakValueReferenceForTesting(entry, oldValueRef);

    // no entry
    assertNull(segment.replace(key, hash, newValue));
    assertEquals(0, segment.count);

    // same key
    segment.setTableEntryForTesting(index, entry);
    segment.count++;
    assertEquals(1, segment.count);
    assertSame(oldValue, segment.get(key, hash));
    assertSame(oldValue, segment.replace(key, hash, newValue));
    assertEquals(1, segment.count);
    assertSame(newValue, segment.get(key, hash));

    // cleared
    segment.setWeakValueReferenceForTesting(entry, oldValueRef);
    oldValueRef.clear();
    assertNull(segment.replace(key, hash, newValue));
    assertEquals(0, segment.count);
    assertNull(segment.get(key, hash));
  }

  public void testSegmentPut() {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(createMapMaker().concurrencyLevel(1).weakValues());
    Segment<Object, Object, ?, ?> segment = map.segments[0];
    // TODO(fry): check recency ordering

    Object key = new Object();
    int hash = map.hash(key);
    Object oldValue = new Object();
    Object newValue = new Object();

    // no entry
    assertEquals(0, segment.count);
    assertNull(segment.put(key, hash, oldValue, false));
    assertEquals(1, segment.count);

    // same key
    assertSame(oldValue, segment.put(key, hash, newValue, false));
    assertEquals(1, segment.count);
    assertSame(newValue, segment.get(key, hash));

    // cleared
    InternalEntry<Object, Object, ?> entry = segment.getEntry(key, hash);
    WeakValueReference<Object, Object, ?> oldValueRef =
        segment.newWeakValueReferenceForTesting(entry, oldValue);
    segment.setWeakValueReferenceForTesting(entry, oldValueRef);
    assertSame(oldValue, segment.get(key, hash));
    oldValueRef.clear();
    assertNull(segment.put(key, hash, newValue, false));
    assertEquals(1, segment.count);
    assertSame(newValue, segment.get(key, hash));
  }

  public void testSegmentPutIfAbsent() {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(createMapMaker().concurrencyLevel(1).weakValues());
    Segment<Object, Object, ?, ?> segment = map.segments[0];
    // TODO(fry): check recency ordering

    Object key = new Object();
    int hash = map.hash(key);
    Object oldValue = new Object();
    Object newValue = new Object();

    // no entry
    assertEquals(0, segment.count);
    assertNull(segment.put(key, hash, oldValue, true));
    assertEquals(1, segment.count);

    // same key
    assertSame(oldValue, segment.put(key, hash, newValue, true));
    assertEquals(1, segment.count);
    assertSame(oldValue, segment.get(key, hash));

    // cleared
    InternalEntry<Object, Object, ?> entry = segment.getEntry(key, hash);
    WeakValueReference<Object, Object, ?> oldValueRef =
        segment.newWeakValueReferenceForTesting(entry, oldValue);
    segment.setWeakValueReferenceForTesting(entry, oldValueRef);
    assertSame(oldValue, segment.get(key, hash));
    oldValueRef.clear();
    assertNull(segment.put(key, hash, newValue, true));
    assertEquals(1, segment.count);
    assertSame(newValue, segment.get(key, hash));
  }

  public void testSegmentPut_expand() {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(createMapMaker().concurrencyLevel(1).initialCapacity(1));
    Segment<Object, Object, ?, ?> segment = map.segments[0];
    assertEquals(1, segment.table.length());

    int count = 1024;
    for (int i = 0; i < count; i++) {
      Object key = new Object();
      Object value = new Object();
      int hash = map.hash(key);
      assertNull(segment.put(key, hash, value, false));
      assertTrue(segment.table.length() > i);
    }
  }

  public void testSegmentRemove() {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(createMapMaker().concurrencyLevel(1).weakValues());
    Segment<Object, Object, ?, ?> segment = map.segments[0];

    Object key = new Object();
    int hash = map.hash(key);
    Object oldValue = new Object();
    AtomicReferenceArray<? extends InternalEntry<Object, Object, ?>> table = segment.table;
    int index = hash & (table.length() - 1);

    InternalEntry<Object, Object, ?> entry = segment.newEntryForTesting(key, hash, null);
    WeakValueReference<Object, Object, ?> oldValueRef =
        segment.newWeakValueReferenceForTesting(entry, oldValue);
    segment.setWeakValueReferenceForTesting(entry, oldValueRef);

    // no entry
    assertEquals(0, segment.count);
    assertNull(segment.remove(key, hash));
    assertEquals(0, segment.count);

    // same key
    segment.setTableEntryForTesting(index, entry);
    segment.count++;
    assertEquals(1, segment.count);
    assertSame(oldValue, segment.get(key, hash));
    assertSame(oldValue, segment.remove(key, hash));
    assertEquals(0, segment.count);
    assertNull(segment.get(key, hash));

    // cleared
    segment.setTableEntryForTesting(index, entry);
    segment.count++;
    assertEquals(1, segment.count);
    assertSame(oldValue, segment.get(key, hash));
    oldValueRef.clear();
    assertNull(segment.remove(key, hash));
    assertEquals(0, segment.count);
    assertNull(segment.get(key, hash));
  }

  public void testSegmentRemoveValue() {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(createMapMaker().concurrencyLevel(1).weakValues());
    Segment<Object, Object, ?, ?> segment = map.segments[0];

    Object key = new Object();
    int hash = map.hash(key);
    Object oldValue = new Object();
    Object newValue = new Object();
    AtomicReferenceArray<? extends InternalEntry<Object, Object, ?>> table = segment.table;
    int index = hash & (table.length() - 1);

    InternalEntry<Object, Object, ?> entry = segment.newEntryForTesting(key, hash, null);
    WeakValueReference<Object, Object, ?> oldValueRef =
        segment.newWeakValueReferenceForTesting(entry, oldValue);
    segment.setWeakValueReferenceForTesting(entry, oldValueRef);

    // no entry
    assertEquals(0, segment.count);
    assertNull(segment.remove(key, hash));
    assertEquals(0, segment.count);

    // same value
    segment.setTableEntryForTesting(index, entry);
    segment.count++;
    assertEquals(1, segment.count);
    assertSame(oldValue, segment.get(key, hash));
    assertTrue(segment.remove(key, hash, oldValue));
    assertEquals(0, segment.count);
    assertNull(segment.get(key, hash));

    // different value
    segment.setTableEntryForTesting(index, entry);
    segment.count++;
    assertEquals(1, segment.count);
    assertSame(oldValue, segment.get(key, hash));
    assertFalse(segment.remove(key, hash, newValue));
    assertEquals(1, segment.count);
    assertSame(oldValue, segment.get(key, hash));

    // cleared
    assertSame(oldValue, segment.get(key, hash));
    oldValueRef.clear();
    assertFalse(segment.remove(key, hash, oldValue));
    assertEquals(0, segment.count);
    assertNull(segment.get(key, hash));
  }

  public void testExpand() {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(createMapMaker().concurrencyLevel(1).initialCapacity(1));
    Segment<Object, Object, ?, ?> segment = map.segments[0];
    assertEquals(1, segment.table.length());

    // manually add elements to avoid expansion
    int originalCount = 1024;
    InternalEntry<Object, Object, ?> entry = null;
    for (int i = 0; i < originalCount; i++) {
      Object key = new Object();
      Object value = new Object();
      int hash = map.hash(key);
      // chain all entries together as we only have a single bucket
      entry = segment.newEntryForTesting(key, hash, entry);
      segment.setValueForTesting(entry, value);
    }
    segment.setTableEntryForTesting(0, entry);
    segment.count = originalCount;
    ImmutableMap<Object, Object> originalMap = ImmutableMap.copyOf(map);
    assertEquals(originalCount, originalMap.size());
    assertEquals(originalMap, map);

    for (int i = 1; i <= originalCount * 2; i *= 2) {
      if (i > 1) {
        segment.expand();
      }
      assertEquals(i, segment.table.length());
      assertEquals(originalCount, countLiveEntries(map));
      assertEquals(originalCount, segment.count);
      assertEquals(originalMap, map);
    }
  }

  public void testRemoveFromChain() {
    MapMakerInternalMap<Object, Object, ?, ?> map = makeMap(createMapMaker().concurrencyLevel(1));
    Segment<Object, Object, ?, ?> segment = map.segments[0];

    // create 3 objects and chain them together
    Object keyOne = new Object();
    Object valueOne = new Object();
    int hashOne = map.hash(keyOne);
    InternalEntry<Object, Object, ?> entryOne = segment.newEntryForTesting(keyOne, hashOne, null);
    segment.setValueForTesting(entryOne, valueOne);
    Object keyTwo = new Object();
    Object valueTwo = new Object();
    int hashTwo = map.hash(keyTwo);
    InternalEntry<Object, Object, ?> entryTwo =
        segment.newEntryForTesting(keyTwo, hashTwo, entryOne);
    segment.setValueForTesting(entryTwo, valueTwo);
    Object keyThree = new Object();
    Object valueThree = new Object();
    int hashThree = map.hash(keyThree);
    InternalEntry<Object, Object, ?> entryThree =
        segment.newEntryForTesting(keyThree, hashThree, entryTwo);
    segment.setValueForTesting(entryThree, valueThree);

    // alone
    assertNull(segment.removeFromChainForTesting(entryOne, entryOne));

    // head
    assertSame(entryOne, segment.removeFromChainForTesting(entryTwo, entryTwo));

    // middle
    InternalEntry<Object, Object, ?> newFirst =
        segment.removeFromChainForTesting(entryThree, entryTwo);
    assertSame(keyThree, newFirst.getKey());
    assertSame(valueThree, newFirst.getValue());
    assertEquals(hashThree, newFirst.getHash());
    assertSame(entryOne, newFirst.getNext());

    // tail (remaining entries are copied in reverse order)
    newFirst = segment.removeFromChainForTesting(entryThree, entryOne);
    assertSame(keyTwo, newFirst.getKey());
    assertSame(valueTwo, newFirst.getValue());
    assertEquals(hashTwo, newFirst.getHash());
    newFirst = newFirst.getNext();
    assertSame(keyThree, newFirst.getKey());
    assertSame(valueThree, newFirst.getValue());
    assertEquals(hashThree, newFirst.getHash());
    assertNull(newFirst.getNext());
  }

  public void testExpand_cleanup() {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(createMapMaker().concurrencyLevel(1).initialCapacity(1));
    Segment<Object, Object, ?, ?> segment = map.segments[0];
    assertEquals(1, segment.table.length());

    // manually add elements to avoid expansion
    // 1/3 null keys, 1/3 null values
    int originalCount = 1024;
    InternalEntry<Object, Object, ?> entry = null;
    for (int i = 0; i < originalCount; i++) {
      Object key = new Object();
      Object value = (i % 3 == 0) ? null : new Object();
      int hash = map.hash(key);
      if (i % 3 == 1) {
        key = null;
      }
      // chain all entries together as we only have a single bucket
      entry = segment.newEntryForTesting(key, hash, entry);
      segment.setValueForTesting(entry, value);
    }
    segment.setTableEntryForTesting(0, entry);
    segment.count = originalCount;
    int liveCount = originalCount / 3;
    assertEquals(1, segment.table.length());
    assertEquals(liveCount, countLiveEntries(map));
    ImmutableMap<Object, Object> originalMap = ImmutableMap.copyOf(map);
    assertEquals(liveCount, originalMap.size());
    // can't compare map contents until cleanup occurs

    for (int i = 1; i <= originalCount * 2; i *= 2) {
      if (i > 1) {
        segment.expand();
      }
      assertEquals(i, segment.table.length());
      assertEquals(liveCount, countLiveEntries(map));
      // expansion cleanup is sloppy, with a goal of avoiding unnecessary copies
      assertTrue(segment.count >= liveCount);
      assertTrue(segment.count <= originalCount);
      assertEquals(originalMap, ImmutableMap.copyOf(map));
    }
  }

  private static <K, V> int countLiveEntries(MapMakerInternalMap<K, V, ?, ?> map) {
    int result = 0;
    for (Segment<K, V, ?, ?> segment : map.segments) {
      AtomicReferenceArray<? extends InternalEntry<K, V, ?>> table = segment.table;
      for (int i = 0; i < table.length(); i++) {
        for (InternalEntry<K, V, ?> e = table.get(i); e != null; e = e.getNext()) {
          if (map.isLiveForTesting(e)) {
            result++;
          }
        }
      }
    }
    return result;
  }

  public void testClear() {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(createMapMaker().concurrencyLevel(1).initialCapacity(1));
    Segment<Object, Object, ?, ?> segment = map.segments[0];
    AtomicReferenceArray<? extends InternalEntry<Object, Object, ?>> table = segment.table;
    assertEquals(1, table.length());

    Object key = new Object();
    Object value = new Object();
    int hash = map.hash(key);
    InternalEntry<Object, Object, ?> entry = segment.newEntryForTesting(key, hash, null);
    segment.setValueForTesting(entry, value);

    segment.setTableEntryForTesting(0, entry);
    segment.readCount.incrementAndGet();
    segment.count = 1;

    assertSame(entry, table.get(0));

    segment.clear();
    assertNull(table.get(0));
    assertEquals(0, segment.readCount.get());
    assertEquals(0, segment.count);
  }

  public void testRemoveEntry() {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(createMapMaker().concurrencyLevel(1).initialCapacity(1));
    Segment<Object, Object, ?, ?> segment = map.segments[0];
    AtomicReferenceArray<? extends InternalEntry<Object, Object, ?>> table = segment.table;
    assertEquals(1, table.length());

    Object key = new Object();
    Object value = new Object();
    int hash = map.hash(key);
    InternalEntry<Object, Object, ?> entry = segment.newEntryForTesting(key, hash, null);
    segment.setValueForTesting(entry, value);

    // remove absent
    assertFalse(segment.removeTableEntryForTesting(entry));

    segment.setTableEntryForTesting(0, entry);
    segment.count = 1;
    assertTrue(segment.removeTableEntryForTesting(entry));
    assertEquals(0, segment.count);
    assertNull(table.get(0));
  }

  public void testClearValue() {
    MapMakerInternalMap<Object, Object, ?, ?> map =
        makeMap(createMapMaker().concurrencyLevel(1).initialCapacity(1).weakValues());
    Segment<Object, Object, ?, ?> segment = map.segments[0];
    AtomicReferenceArray<? extends InternalEntry<Object, Object, ?>> table = segment.table;
    assertEquals(1, table.length());

    Object key = new Object();
    Object value = new Object();
    int hash = map.hash(key);
    InternalEntry<Object, Object, ?> entry = segment.newEntryForTesting(key, hash, null);
    segment.setValueForTesting(entry, value);
    WeakValueReference<Object, Object, ?> valueRef = segment.getWeakValueReferenceForTesting(entry);

    // clear absent
    assertFalse(segment.clearValueForTesting(key, hash, valueRef));

    segment.setTableEntryForTesting(0, entry);
    // don't increment count; this is used during computation
    assertTrue(segment.clearValueForTesting(key, hash, valueRef));
    // no notification sent with clearValue
    assertEquals(0, segment.count);
    assertNull(table.get(0));

    // clear wrong value reference
    segment.setTableEntryForTesting(0, entry);
    WeakValueReference<Object, Object, ?> otherValueRef =
        segment.newWeakValueReferenceForTesting(entry, value);
    segment.setWeakValueReferenceForTesting(entry, otherValueRef);
    assertFalse(segment.clearValueForTesting(key, hash, valueRef));
    segment.setWeakValueReferenceForTesting(entry, valueRef);
    assertTrue(segment.clearValueForTesting(key, hash, valueRef));
  }

  // reference queues

  public void testDrainKeyReferenceQueueOnWrite() {
    for (MapMaker maker : allWeakKeyStrengthMakers()) {
      MapMakerInternalMap<Object, Object, ?, ?> map = makeMap(maker.concurrencyLevel(1));
      if (maker.getKeyStrength() == Strength.WEAK) {
        Segment<Object, Object, ?, ?> segment = map.segments[0];

        Object keyOne = new Object();
        int hashOne = map.hash(keyOne);
        Object valueOne = new Object();
        Object keyTwo = new Object();
        Object valueTwo = new Object();

        map.put(keyOne, valueOne);
        InternalEntry<Object, Object, ?> entry = segment.getEntry(keyOne, hashOne);

        @SuppressWarnings("unchecked")
        Reference<Object> reference = (Reference) entry;
        reference.enqueue();

        map.put(keyTwo, valueTwo);
        assertFalse(map.containsKey(keyOne));
        assertFalse(map.containsValue(valueOne));
        assertNull(map.get(keyOne));
        assertEquals(1, map.size());
        assertNull(segment.getKeyReferenceQueueForTesting().poll());
      }
    }
  }

  public void testDrainValueReferenceQueueOnWrite() {
    for (MapMaker maker : allWeakValueStrengthMakers()) {
      MapMakerInternalMap<Object, Object, ?, ?> map = makeMap(maker.concurrencyLevel(1));
      if (maker.getValueStrength() == Strength.WEAK) {
        Segment<Object, Object, ?, ?> segment = map.segments[0];

        Object keyOne = new Object();
        int hashOne = map.hash(keyOne);
        Object valueOne = new Object();
        Object keyTwo = new Object();
        Object valueTwo = new Object();

        map.put(keyOne, valueOne);
        @SuppressWarnings("unchecked")
        WeakValueEntry<Object, Object, ?> entry =
            (WeakValueEntry<Object, Object, ?>) segment.getEntry(keyOne, hashOne);
        WeakValueReference<Object, Object, ?> valueReference = entry.getValueReference();

        @SuppressWarnings("unchecked")
        Reference<Object> reference = (Reference) valueReference;
        reference.enqueue();

        map.put(keyTwo, valueTwo);
        assertFalse(map.containsKey(keyOne));
        assertFalse(map.containsValue(valueOne));
        assertNull(map.get(keyOne));
        assertEquals(1, map.size());
        assertNull(segment.getValueReferenceQueueForTesting().poll());
      }
    }
  }

  public void testDrainKeyReferenceQueueOnRead() {
    for (MapMaker maker : allWeakKeyStrengthMakers()) {
      MapMakerInternalMap<Object, Object, ?, ?> map = makeMap(maker.concurrencyLevel(1));
      if (maker.getKeyStrength() == Strength.WEAK) {
        Segment<Object, Object, ?, ?> segment = map.segments[0];

        Object keyOne = new Object();
        int hashOne = map.hash(keyOne);
        Object valueOne = new Object();
        Object keyTwo = new Object();

        map.put(keyOne, valueOne);
        InternalEntry<Object, Object, ?> entry = segment.getEntry(keyOne, hashOne);

        @SuppressWarnings("unchecked")
        Reference<Object> reference = (Reference) entry;
        reference.enqueue();

        for (int i = 0; i < SMALL_MAX_SIZE; i++) {
          Object unused = map.get(keyTwo);
        }
        assertFalse(map.containsKey(keyOne));
        assertFalse(map.containsValue(valueOne));
        assertNull(map.get(keyOne));
        assertEquals(0, map.size());
        assertNull(segment.getKeyReferenceQueueForTesting().poll());
      }
    }
  }

  public void testDrainValueReferenceQueueOnRead() {
    for (MapMaker maker : allWeakValueStrengthMakers()) {
      MapMakerInternalMap<Object, Object, ?, ?> map = makeMap(maker.concurrencyLevel(1));
      if (maker.getValueStrength() == Strength.WEAK) {
        Segment<Object, Object, ?, ?> segment = map.segments[0];

        Object keyOne = new Object();
        int hashOne = map.hash(keyOne);
        Object valueOne = new Object();
        Object keyTwo = new Object();

        map.put(keyOne, valueOne);
        @SuppressWarnings("unchecked")
        WeakValueEntry<Object, Object, ?> entry =
            (WeakValueEntry<Object, Object, ?>) segment.getEntry(keyOne, hashOne);
        WeakValueReference<Object, Object, ?> valueReference = entry.getValueReference();

        @SuppressWarnings("unchecked")
        Reference<Object> reference = (Reference) valueReference;
        reference.enqueue();

        for (int i = 0; i < SMALL_MAX_SIZE; i++) {
          Object unused = map.get(keyTwo);
        }
        assertFalse(map.containsKey(keyOne));
        assertFalse(map.containsValue(valueOne));
        assertNull(map.get(keyOne));
        assertEquals(0, map.size());
        assertNull(segment.getValueReferenceQueueForTesting().poll());
      }
    }
  }

  // utility methods

  private static Iterable<MapMaker> allWeakKeyStrengthMakers() {
    return ImmutableList.of(
        createMapMaker().weakKeys(),
        createMapMaker().weakKeys().weakValues());
  }

  private static Iterable<MapMaker> allWeakValueStrengthMakers() {
    return ImmutableList.of(
        createMapMaker().weakValues(), createMapMaker().weakKeys().weakValues());
  }

  public void testNullParameters() throws Exception {
    NullPointerTester tester = new NullPointerTester();
    tester.testAllPublicInstanceMethods(makeMap(createMapMaker()));
  }
}
