/*
 * Copyright 2011 Google Inc. All Rights Reserved.
 *
 * 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.googlecode.concurrentlinkedhashmap;

import static com.google.common.collect.Maps.newLinkedHashMap;
import static com.google.common.collect.Sets.newLinkedHashSet;
import static com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.MAXIMUM_CAPACITY;
import static com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.READ_BUFFER_THRESHOLD;
import static com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.readBufferIndex;
import static com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.DrainStatus.REQUIRED;
import static com.googlecode.concurrentlinkedhashmap.EvictionTest.Status.ALIVE;
import static com.googlecode.concurrentlinkedhashmap.EvictionTest.Status.DEAD;
import static com.googlecode.concurrentlinkedhashmap.EvictionTest.Status.RETIRED;
import static com.googlecode.concurrentlinkedhashmap.IsEmptyCollection.emptyCollection;
import static com.googlecode.concurrentlinkedhashmap.IsEmptyMap.emptyMap;
import static com.googlecode.concurrentlinkedhashmap.IsValidConcurrentLinkedHashMap.valid;
import static com.googlecode.concurrentlinkedhashmap.benchmark.Benchmarks.createWorkingSet;
import static com.googlecode.concurrentlinkedhashmap.benchmark.Benchmarks.determineEfficiency;
import static com.jayway.awaitility.Awaitility.await;
import static com.jayway.awaitility.Awaitility.to;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonMap;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;

import org.mockito.Mockito;
import org.testng.annotations.Test;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.Builder;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.Node;
import com.googlecode.concurrentlinkedhashmap.benchmark.Benchmarks.EfficiencyRun;
import com.googlecode.concurrentlinkedhashmap.caches.CacheFactory;
import com.googlecode.concurrentlinkedhashmap.generator.Generator;
import com.googlecode.concurrentlinkedhashmap.generator.ScrambledZipfianGenerator;

/**
 * A unit-test for the page replacement algorithm and its public methods.
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
@Test(groups = "development")
public final class EvictionTest extends AbstractTest {

  /* ---------------- Capacity -------------- */

  @Test(dataProvider = "warmedMap")
  public void capacity_increase(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = ImmutableMap.copyOf(newWarmedMap());
    long newMaxCapacity = 2 * capacity();

    map.setCapacity(newMaxCapacity);
    assertThat(map, is(equalTo(expected)));
    assertThat(map.capacity(), is(equalTo(newMaxCapacity)));
  }

  @Test(dataProvider = "warmedMap")
  public void capacity_increaseToMaximum(ConcurrentLinkedHashMap<Integer, Integer> map) {
    map.setCapacity(MAXIMUM_CAPACITY);
    assertThat(map.capacity(), is(equalTo(MAXIMUM_CAPACITY)));
  }

  @Test(dataProvider = "warmedMap")
  public void capacity_increaseAboveMaximum(ConcurrentLinkedHashMap<Integer, Integer> map) {
    map.setCapacity(MAXIMUM_CAPACITY + 1);
    assertThat(map.capacity(), is(equalTo(MAXIMUM_CAPACITY)));
  }

  @Test
  public void capacity_decrease() {
    checkDecreasedCapacity(capacity() / 2);
  }

  @Test
  public void capacity_decreaseToMinimum() {
    checkDecreasedCapacity(0);
  }

  private void checkDecreasedCapacity(long newMaxCapacity) {
    ConcurrentLinkedHashMap<Integer, Integer> map = new Builder<Integer, Integer>()
        .maximumWeightedCapacity(capacity())
        .listener(listener)
        .build();
    warmUp(map, 0, capacity());
    map.setCapacity(newMaxCapacity);

    assertThat(map, is(valid()));
    assertThat(map.size(), is(equalTo((int) newMaxCapacity)));
    assertThat(map.capacity(), is(equalTo(newMaxCapacity)));
    verify(listener, times((int) (capacity() - newMaxCapacity))).onEviction(anyInt(), anyInt());
  }

  @Test(dataProvider = "warmedMap", expectedExceptions = IllegalArgumentException.class)
  public void capacity_decreaseBelowMinimum(ConcurrentLinkedHashMap<Integer, Integer> map) {
    try {
      map.setCapacity(-1);
    } finally {
      assertThat(map.capacity(), is(equalTo(capacity())));
    }
  }

  /* ---------------- Eviction -------------- */

  @Test(dataProvider = "builder", expectedExceptions = IllegalStateException.class)
  public void evict_listenerFails(Builder<Integer, Integer> builder) {
    doThrow(new IllegalStateException()).when(listener).onEviction(anyInt(), anyInt());
    ConcurrentLinkedHashMap<Integer, Integer> map = builder
        .maximumWeightedCapacity(0)
        .listener(listener)
        .build();
    try {
      warmUp(map, 0, capacity());
    } finally {
      assertThat(map, is(valid()));
    }
  }

  @Test
  public void evict_alwaysDiscard() {
    ConcurrentLinkedHashMap<Integer, Integer> map = new Builder<Integer, Integer>()
        .maximumWeightedCapacity(0)
        .listener(listener)
        .build();
    warmUp(map, 0, 100);

    assertThat(map, is(valid()));
    verify(listener, times(100)).onEviction(anyInt(), anyInt());
  }

  @Test
  public void evict() {
    ConcurrentLinkedHashMap<Integer, Integer> map = new Builder<Integer, Integer>()
        .maximumWeightedCapacity(10)
        .listener(listener)
        .build();
    warmUp(map, 0, 20);

    assertThat(map, is(valid()));
    assertThat(map.size(), is(10));
    assertThat(map.weightedSize(), is(10L));
    verify(listener, times(10)).onEviction(anyInt(), anyInt());
  }

  @Test(dataProvider = "builder")
  public void evict_weighted(Builder<Integer, Collection<Integer>> builder) {
    ConcurrentLinkedHashMap<Integer, Collection<Integer>> map = builder
        .weigher(Weighers.<Integer>collection())
        .maximumWeightedCapacity(10)
        .build();

    map.put(1, asList(1, 2));
    map.put(2, asList(3, 4, 5, 6, 7));
    map.put(3, asList(8, 9, 10));
    assertThat(map.weightedSize(), is(10L));

    // evict (1)
    map.put(4, asList(11));
    assertThat(map.containsKey(1), is(false));
    assertThat(map.weightedSize(), is(9L));

    // evict (2, 3)
    map.put(5, asList(12, 13, 14, 15, 16, 17, 18, 19, 20));
    assertThat(map.weightedSize(), is(10L));

    assertThat(map, is(valid()));
  }

  @Test(dataProvider = "builder")
  public void evict_maximumCapacity(Builder<Integer, Integer> builder) {
    ConcurrentLinkedHashMap<Integer, Integer> map = builder
        .maximumWeightedCapacity(MAXIMUM_CAPACITY)
        .build();
    map.put(1, 2);
    map.capacity.set(MAXIMUM_CAPACITY);
    map.weightedSize.set(MAXIMUM_CAPACITY);

    map.put(2, 3);
    assertThat(map.weightedSize(), is(MAXIMUM_CAPACITY));
    assertThat(map, is(equalTo(singletonMap(2, 3))));
  }

  @Test
  public void evict_alreadyRemoved() throws Exception {
    final ConcurrentLinkedHashMap<Integer, Integer> map = new Builder<Integer, Integer>()
        .maximumWeightedCapacity(1)
        .listener(listener)
        .build();
    map.put(0, 0);
    map.evictionLock.lock();
    try {
      Node<Integer, Integer> node = map.data.get(0);
      checkStatus(map, node, ALIVE);
      new Thread() {
        @Override public void run() {
          map.put(1, 1);
          assertThat(map.remove(0), is(0));
        }
      }.start();
      await().untilCall(to((Map<?, ?>) map).containsKey(0), is(false));
      checkStatus(map, node, RETIRED);
      map.drainBuffers();

      checkStatus(map, node, DEAD);
      assertThat(map.containsKey(1), is(true));
      verify(listener, never()).onEviction(anyInt(), anyInt());
    } finally {
      map.evictionLock.unlock();
    }
  }

  enum Status { ALIVE, RETIRED, DEAD }

  private static void checkStatus(ConcurrentLinkedHashMap<Integer, Integer> map,
      Node<Integer, Integer> node, Status expected) {
    assertThat(node.get().isAlive(), is(expected == ALIVE));
    assertThat(node.get().isRetired(), is(expected == RETIRED));
    assertThat(node.get().isDead(), is(expected == DEAD));

    if (node.get().isRetired() || node.get().isDead()) {
      assertThat(map.tryToRetire(node, node.get()), is(false));
    }
    if (node.get().isDead()) {
      map.makeRetired(node);
      assertThat(node.get().isRetired(), is(false));
    }
  }

  @Test(dataProvider = "builder")
  public void evict_lru(Builder<Integer, Integer> builder) {
    ConcurrentLinkedHashMap<Integer, Integer> map = builder
        .maximumWeightedCapacity(10)
        .build();
    warmUp(map, 0, 10);
    checkContainsInOrder(map, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9);

    // re-order
    checkReorder(map, asList(0, 1, 2), 3, 4, 5, 6, 7, 8, 9, 0, 1, 2);

    // evict 3, 4, 5
    checkEvict(map, asList(10, 11, 12), 6, 7, 8, 9, 0, 1, 2, 10, 11, 12);

    // re-order
    checkReorder(map, asList(6, 7, 8), 9, 0, 1, 2, 10, 11, 12, 6, 7, 8);

    // evict 9, 0, 1
    checkEvict(map, asList(13, 14, 15), 2, 10, 11, 12, 6, 7, 8, 13, 14, 15);

    assertThat(map, is(valid()));
  }

  private void checkReorder(ConcurrentLinkedHashMap<Integer, Integer> map,
      List<Integer> keys, Integer... expect) {
    for (int i : keys) {
      map.get(i);
    }
    checkContainsInOrder(map, expect);
  }

  private void checkEvict(ConcurrentLinkedHashMap<Integer, Integer> map,
      List<Integer> keys, Integer... expect) {
    for (int i : keys) {
      map.put(i, i);
    }
    checkContainsInOrder(map, expect);
  }

  private void checkContainsInOrder(ConcurrentLinkedHashMap<Integer, Integer> map,
      Integer... expect) {
    map.drainBuffers();
    List<Integer> evictionList = Lists.newArrayList();
    for (Node<Integer, Integer> node : map.evictionDeque) {
      evictionList.add(node.key);
    }
    assertThat(map.size(), is(equalTo(expect.length)));
    assertThat(map.keySet(), containsInAnyOrder(expect));
    assertThat(evictionList, is(equalTo(asList(expect))));
  }

  @Test
  public void evict_efficiency() {
    Map<String, String> expected = new CacheFactory()
        .maximumCapacity((int) capacity())
        .makeCache(CacheType.LinkedHashMap_Lru_Sync);
    Map<String, String> actual = new Builder<String, String>()
        .maximumWeightedCapacity(capacity())
        .build();

    Generator generator = new ScrambledZipfianGenerator(10 * capacity());
    List<String> workingSet = createWorkingSet(generator, 10 * (int) capacity());

    EfficiencyRun runExpected = determineEfficiency(expected, workingSet);
    EfficiencyRun runActual = determineEfficiency(actual, workingSet);

    String reason = String.format("Expected [%s] but was [%s]", runExpected, runActual);
    assertThat(reason, runActual.hitCount, is(runExpected.hitCount));
  }

  @Test(dataProvider = "warmedMap")
  public void updateRecency_onGet(final ConcurrentLinkedHashMap<Integer, Integer> map) {
    final Node<Integer, Integer> first = map.evictionDeque.peek();
    updateRecency(map, new Runnable() {
      @Override public void run() {
        map.get(first.key);
      }
    });
  }

  @Test(dataProvider = "warmedMap")
  public void updateRecency_onGetQuietly(final ConcurrentLinkedHashMap<Integer, Integer> map) {
    AtomicLong drainCounter = map.readBufferDrainAtWriteCount[readBufferIndex()];

    Node<Integer, Integer> first = map.evictionDeque.peek();
    Node<Integer, Integer> last = map.evictionDeque.peekLast();
    long drained = drainCounter.get();

    map.getQuietly(first.key);
    map.drainBuffers();

    assertThat(map.evictionDeque.peekFirst(), is(first));
    assertThat(map.evictionDeque.peekLast(), is(last));
    assertThat(drainCounter.get(), is(drained));
  }

  @Test(dataProvider = "warmedMap")
  public void updateRecency_onPutIfAbsent(final ConcurrentLinkedHashMap<Integer, Integer> map) {
    final Node<Integer, Integer> first = map.evictionDeque.peek();
    updateRecency(map, new Runnable() {
      @Override public void run() {
        map.putIfAbsent(first.key, first.key);
      }
    });
  }

  @Test(dataProvider = "warmedMap")
  public void updateRecency_onPut(final ConcurrentLinkedHashMap<Integer, Integer> map) {
    final Node<Integer, Integer> first = map.evictionDeque.peek();
    updateRecency(map, new Runnable() {
      @Override public void run() {
        map.put(first.key, first.key);
      }
    });
  }

  @Test(dataProvider = "warmedMap")
  public void updateRecency_onReplace(final ConcurrentLinkedHashMap<Integer, Integer> map) {
    final Node<Integer, Integer> first = map.evictionDeque.peek();
    updateRecency(map, new Runnable() {
      @Override public void run() {
        map.replace(first.key, first.key);
      }
    });
  }

  @Test(dataProvider = "warmedMap")
  public void updateRecency_onReplaceConditionally(
      final ConcurrentLinkedHashMap<Integer, Integer> map) {
    final Node<Integer, Integer> first = map.evictionDeque.peek();
    updateRecency(map, new Runnable() {
      @Override public void run() {
        map.replace(first.key, first.key, first.key);
      }
    });
  }

  private void updateRecency(ConcurrentLinkedHashMap<Integer, Integer> map, Runnable operation) {
    Node<Integer, Integer> first = map.evictionDeque.peek();

    operation.run();
    map.drainBuffers();

    assertThat(map.evictionDeque.peekFirst(), is(not(first)));
    assertThat(map.evictionDeque.peekLast(), is(first));
    assertThat(map, is(valid()));
  }

  @Test(dataProvider = "guardedMap")
  public void exceedsMaximumBufferSize_onRead(ConcurrentLinkedHashMap<Integer, Integer> map) {
    AtomicLong drainCounter = map.readBufferDrainAtWriteCount[readBufferIndex()];
    map.readBufferWriteCount[readBufferIndex()].set(READ_BUFFER_THRESHOLD - 1);

    map.afterRead(null);
    assertThat(drainCounter.get(), is(0L));

    map.afterRead(null);
    assertThat(drainCounter.get(), is(READ_BUFFER_THRESHOLD + 1L));
  }

  @Test(dataProvider = "guardedMap")
  public void exceedsMaximumBufferSize_onWrite(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Runnable task = Mockito.mock(Runnable.class);
    map.afterWrite(task);
    verify(task).run();

    assertThat(map.writeBuffer, hasSize(0));
  }

  @Test(dataProvider = "warmedMap")
  public void drain_onRead(ConcurrentLinkedHashMap<Integer, Integer> map) {
    AtomicReference<Node<Integer, Integer>>[] buffer = map.readBuffers[readBufferIndex()];
    AtomicLong writeCounter = map.readBufferWriteCount[readBufferIndex()];

    for (int i = 0; i < READ_BUFFER_THRESHOLD; i++) {
      map.get(1);
    }

    int pending = 0;
    for (AtomicReference<?> slot : buffer) {
      if (slot.get() != null) {
        pending++;
      }
    }
    assertThat(pending, is(equalTo(READ_BUFFER_THRESHOLD)));
    assertThat((int) writeCounter.get(), is(equalTo(pending)));

    map.get(1);
    assertThat(map.readBufferReadCount[readBufferIndex()], is(equalTo(writeCounter.get())));
    for (int i = 0; i < map.readBuffers.length; i++) {
      assertThat(map.readBuffers[readBufferIndex()][i].get(), is(nullValue()));
    }
  }

  @Test(dataProvider = "guardedMap")
  public void drain_onWrite(ConcurrentLinkedHashMap<Integer, Integer> map) {
    map.put(1, 1);
    assertThat(map.writeBuffer, hasSize(0));
    assertThat(map.evictionDeque, hasSize(1));
  }

  @Test(dataProvider = "guardedMap")
  public void drain_nonblocking(final ConcurrentLinkedHashMap<Integer, Integer> map)
      throws Exception {
    final AtomicBoolean done = new AtomicBoolean();
    Thread thread = new Thread() {
      @Override public void run() {
        map.drainStatus.set(REQUIRED);
        map.tryToDrainBuffers();
        done.set(true);
      }
    };
    map.evictionLock.lock();
    try {
      thread.start();
      await().untilTrue(done);
    } finally {
      map.evictionLock.unlock();
    }
  }

  @Test(dataProvider = "guardedMap")
  public void drain_blocksClear(final ConcurrentLinkedHashMap<Integer, Integer> map)
      throws Exception {
    checkDrainBlocks(map, new Runnable() {
      @Override public void run() {
        map.clear();
      }
    });
  }

  @Test(dataProvider = "guardedMap")
  public void drain_blocksAscendingKeySet(final ConcurrentLinkedHashMap<Integer, Integer> map)
      throws Exception {
    checkDrainBlocks(map, new Runnable() {
      @Override public void run() {
        map.ascendingKeySet();
      }
    });
    checkDrainBlocks(map, new Runnable() {
      @Override public void run() {
        map.ascendingKeySetWithLimit((int) capacity());
      }
    });
  }

  @Test(dataProvider = "guardedMap")
  public void drain_blocksDescendingKeySet(final ConcurrentLinkedHashMap<Integer, Integer> map)
      throws Exception {
    checkDrainBlocks(map, new Runnable() {
      @Override public void run() {
        map.descendingKeySet();
      }
    });
    checkDrainBlocks(map, new Runnable() {
      @Override public void run() {
        map.descendingKeySetWithLimit((int) capacity());
      }
    });
  }

  @Test(dataProvider = "guardedMap")
  public void drain_blocksAscendingMap(final ConcurrentLinkedHashMap<Integer, Integer> map)
      throws Exception {
    checkDrainBlocks(map, new Runnable() {
      @Override public void run() {
        map.ascendingMap();
      }
    });
    checkDrainBlocks(map, new Runnable() {
      @Override public void run() {
        map.ascendingMapWithLimit((int) capacity());
      }
    });
  }

  @Test(dataProvider = "guardedMap")
  public void drain_blocksDescendingMap(final ConcurrentLinkedHashMap<Integer, Integer> map)
      throws Exception {
    checkDrainBlocks(map, new Runnable() {
      @Override public void run() {
        map.descendingMap();
      }
    });
    checkDrainBlocks(map, new Runnable() {
      @Override public void run() {
        map.descendingMapWithLimit((int) capacity());
      }
    });
  }

  @Test(dataProvider = "guardedMap")
  public void drain_blocksCapacity(final ConcurrentLinkedHashMap<Integer, Integer> map)
      throws Exception {
    checkDrainBlocks(map, new Runnable() {
      @Override public void run() {
        map.setCapacity(0);
      }
    });
  }

  void checkDrainBlocks(final ConcurrentLinkedHashMap<Integer, Integer> map, final Runnable task)
      throws Exception {
    final ReentrantLock lock = (ReentrantLock) map.evictionLock;
    final AtomicBoolean done = new AtomicBoolean();
    final Thread thread = new Thread() {
      @Override public void run() {
        map.drainStatus.set(REQUIRED);
        task.run();
        done.set(true);
      }
    };
    lock.lock();
    try {
      thread.start();
      await().until(new Callable<Boolean>() {
        @Override public Boolean call() {
          return lock.hasQueuedThread(thread);
        }
      });
    } finally {
      lock.unlock();
    }
    await().untilTrue(done);
  }

  /* ---------------- Ascending KeySet -------------- */

  @Test(dataProvider = "warmedMap")
  public void ascendingKeySet(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = newLinkedHashMap();
    warmUp(expected, 0, capacity());

    assertThat(map.ascendingKeySet(), is(equalTo(expected.keySet())));
  }

  @Test(dataProvider = "warmedMap")
  public void ascendingKeySet_snapshot(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = newLinkedHashMap();
    warmUp(expected, 0, capacity());

    Set<Integer> original = map.ascendingKeySet();
    map.put((int) capacity(), (int) -capacity());

    assertThat(original, is(equalTo(expected.keySet())));
  }

  @Test(dataProvider = "warmedMap")
  public void ascendingKeySetWithLimit_greaterThan(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = newLinkedHashMap();
    warmUp(expected, 0, capacity() / 2);

    assertThat(map.ascendingKeySetWithLimit((int) capacity() / 2), is(equalTo(expected.keySet())));
  }

  @Test(dataProvider = "warmedMap")
  public void ascendingKeySetWithLimit_lessThan(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = newLinkedHashMap();
    warmUp(expected, 0, capacity());

    assertThat(map.ascendingKeySetWithLimit((int) capacity() * 2), is(equalTo(expected.keySet())));
  }

  @Test(dataProvider = "warmedMap")
  public void ascendingKeySetWithLimit_snapshot(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = newLinkedHashMap();
    warmUp(expected, 0, capacity() / 2);

    Set<Integer> original = map.ascendingKeySetWithLimit((int) capacity() / 2);
    map.put((int) capacity(), (int) -capacity());

    assertThat(original, is(equalTo(expected.keySet())));
  }

  @Test(dataProvider = "warmedMap")
  public void ascendingKeySetWithLimit_zero(ConcurrentLinkedHashMap<Integer, Integer> map) {
    assertThat(map.ascendingKeySetWithLimit(0), is(emptyCollection()));
  }

  @Test(dataProvider = "warmedMap", expectedExceptions = IllegalArgumentException.class)
  public void ascendingKeySetWithLimit_negative(ConcurrentLinkedHashMap<Integer, Integer> map) {
    map.ascendingKeySetWithLimit(-1);
  }

  /* ---------------- Descending KeySet -------------- */

  @Test(dataProvider = "warmedMap")
  public void descendingKeySet(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Set<Integer> expected = newLinkedHashSet();
    for (int i = (int) capacity() - 1; i >= 0; i--) {
      expected.add(i);
    }

    assertThat(map.descendingKeySet(), is(equalTo(expected)));
  }

  @Test(dataProvider = "warmedMap")
  public void descendingKeySet_snapshot(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Set<Integer> expected = newLinkedHashSet();
    for (int i = (int) capacity() - 1; i >= 0; i--) {
      expected.add(i);
    }

    Set<Integer> original = map.descendingKeySet();
    map.put((int) capacity(), (int) -capacity());

    assertThat(original, is(equalTo(original)));
  }

  @Test(dataProvider = "warmedMap")
  public void descendingKeySetWithLimit_greaterThan(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Set<Integer> expected = newLinkedHashSet();
    for (int i = (int) capacity() - 1; i >= capacity() / 2; i--) {
      expected.add(i);
    }
    assertThat(map.descendingKeySetWithLimit((int) capacity() / 2), is(equalTo(expected)));
  }

  @Test(dataProvider = "warmedMap")
  public void descendingKeySetWithLimit_lessThan(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Set<Integer> expected = newLinkedHashSet();
    for (int i = (int) capacity() - 1; i >= 0; i--) {
      expected.add(i);
    }
    assertThat(map.descendingKeySetWithLimit((int) capacity() * 2), is(equalTo(expected)));
  }

  @Test(dataProvider = "warmedMap")
  public void descendingKeySetWithLimit_snapshot(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Set<Integer> expected = newLinkedHashSet();
    for (int i = (int) capacity() - 1; i >= capacity() / 2; i--) {
      expected.add(i);
    }

    Set<Integer> original = map.descendingKeySetWithLimit((int) capacity() / 2);
    map.put((int) capacity(), (int) -capacity());

    assertThat(original, is(equalTo(expected)));
  }

  @Test(dataProvider = "warmedMap")
  public void descendingKeySetWithLimit_zero(ConcurrentLinkedHashMap<Integer, Integer> map) {
    assertThat(map.descendingKeySetWithLimit(0), is(emptyCollection()));
  }

  @Test(dataProvider = "warmedMap", expectedExceptions = IllegalArgumentException.class)
  public void descendingKeySetWithLimit_negative(ConcurrentLinkedHashMap<Integer, Integer> map) {
    map.descendingKeySetWithLimit(-1);
  }

  /* ---------------- Ascending Map -------------- */

  @Test(dataProvider = "warmedMap")
  public void ascendingMap(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = newLinkedHashMap();
    warmUp(expected, 0, capacity());

    assertThat(map.ascendingMap(), is(equalTo(expected)));
  }

  @Test(dataProvider = "warmedMap")
  public void ascendingMap_snapshot(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = newLinkedHashMap();
    warmUp(expected, 0, capacity());

    Map<Integer, Integer> original = map.ascendingMap();
    map.put((int) capacity(), (int) -capacity());

    assertThat(original, is(equalTo(expected)));
  }

  @Test(dataProvider = "warmedMap")
  public void ascendingMapWithLimit_greaterThan(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = newLinkedHashMap();
    warmUp(expected, 0, capacity() / 2);

    assertThat(map.ascendingMapWithLimit((int) capacity() / 2), is(equalTo(expected)));
  }

  @Test(dataProvider = "warmedMap")
  public void ascendingMapWithLimit_lessThan(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = newLinkedHashMap();
    warmUp(expected, 0, capacity());

    assertThat(map.ascendingMapWithLimit((int) capacity() * 2), is(equalTo(expected)));
  }

  @Test(dataProvider = "warmedMap")
  public void ascendingMapWithLimit_snapshot(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = newLinkedHashMap();
    warmUp(expected, 0, capacity() / 2);

    Map<Integer, Integer> original = map.ascendingMapWithLimit((int) capacity() / 2);
    map.put((int) capacity(), (int) -capacity());

    assertThat(original, is(equalTo(expected)));
  }

  @Test(dataProvider = "warmedMap")
  public void ascendingMapWithLimit_zero(ConcurrentLinkedHashMap<Integer, Integer> map) {
    assertThat(map.ascendingMapWithLimit(0), is(emptyMap()));
  }

  @Test(dataProvider = "warmedMap", expectedExceptions = IllegalArgumentException.class)
  public void ascendingMapWithLimit_negative(ConcurrentLinkedHashMap<Integer, Integer> map) {
    map.ascendingMapWithLimit(-1);
  }

  /* ---------------- Descending Map -------------- */

  @Test(dataProvider = "warmedMap")
  public void descendingMap(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = newLinkedHashMap();
    for (int i = (int) capacity() - 1; i >= 0; i--) {
      expected.put(i, -i);
    }
    assertThat(map.descendingMap(), is(equalTo(expected)));
  }

  @Test(dataProvider = "warmedMap")
  public void descendingMap_snapshot(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = newLinkedHashMap();
    for (int i = (int) capacity() - 1; i >= 0; i--) {
      expected.put(i, -i);
    }

    Map<Integer, Integer> original = map.descendingMap();
    map.put((int) capacity(), (int) -capacity());

    assertThat(original, is(equalTo(expected)));
  }

  @Test(dataProvider = "warmedMap")
  public void descendingMapWithLimit_greaterThan(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = newLinkedHashMap();
    for (int i = (int) capacity() - 1; i >= capacity() / 2; i--) {
      expected.put(i, -i);
    }
    assertThat(map.descendingMapWithLimit((int) capacity() / 2), is(equalTo(expected)));
  }

  @Test(dataProvider = "warmedMap")
  public void descendingMapWithLimit_lessThan(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = newLinkedHashMap();
    for (int i = (int) capacity() - 1; i >= 0; i--) {
      expected.put(i, -i);
    }
    assertThat(map.descendingMapWithLimit((int) capacity() * 2), is(equalTo(expected)));
  }

  @Test(dataProvider = "warmedMap")
  public void descendingMapWithLimit_snapshot(ConcurrentLinkedHashMap<Integer, Integer> map) {
    Map<Integer, Integer> expected = newLinkedHashMap();
    for (int i = (int) capacity() - 1; i >= capacity() / 2; i--) {
      expected.put(i, -i);
    }

    Map<Integer, Integer> original = map.descendingMapWithLimit((int) capacity() / 2);
    map.put((int) capacity(), (int) -capacity());

    assertThat(original, is(equalTo(expected)));
  }

  @Test(dataProvider = "warmedMap")
  public void descendingMapWithLimit_zero(ConcurrentLinkedHashMap<Integer, Integer> map) {
    assertThat(map.descendingMapWithLimit(0), is(emptyMap()));
  }

  @Test(dataProvider = "warmedMap", expectedExceptions = IllegalArgumentException.class)
  public void descendingMapWithLimit_negative(ConcurrentLinkedHashMap<Integer, Integer> map) {
    map.descendingMapWithLimit(-1);
  }
}
