/*
 * Copyright 2014 Ben Manes. 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.github.benmanes.caffeine.cache;

import static com.github.benmanes.caffeine.cache.testing.CacheWriterVerifier.verifyWriter;
import static com.github.benmanes.caffeine.cache.testing.HasRemovalNotifications.hasRemovalNotifications;
import static com.github.benmanes.caffeine.cache.testing.HasStats.hasEvictionCount;
import static com.github.benmanes.caffeine.cache.testing.HasStats.hasEvictionWeight;
import static com.github.benmanes.caffeine.testing.Awaits.await;
import static com.github.benmanes.caffeine.testing.IsEmptyMap.emptyMap;
import static java.util.Arrays.asList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.verify;

import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.OptionalInt;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;

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

import com.github.benmanes.caffeine.cache.Policy.Eviction;
import com.github.benmanes.caffeine.cache.testing.CacheContext;
import com.github.benmanes.caffeine.cache.testing.CacheProvider;
import com.github.benmanes.caffeine.cache.testing.CacheSpec;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.CacheWeigher;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Compute;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Implementation;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.InitialCapacity;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Listener;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Maximum;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Population;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.ReferenceType;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Writer;
import com.github.benmanes.caffeine.cache.testing.CacheValidationListener;
import com.github.benmanes.caffeine.cache.testing.CheckNoStats;
import com.github.benmanes.caffeine.cache.testing.CheckNoWriter;
import com.github.benmanes.caffeine.cache.testing.RejectingCacheWriter.DeleteException;
import com.github.benmanes.caffeine.cache.testing.RemovalListeners.RejectingRemovalListener;
import com.github.benmanes.caffeine.cache.testing.RemovalNotification;
import com.github.benmanes.caffeine.testing.Awaits;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;

/**
 * The test cases for caches with a page replacement algorithm.
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
@Listeners(CacheValidationListener.class)
@Test(dataProviderClass = CacheProvider.class)
public final class EvictionTest {

  /* ---------------- RemovalListener -------------- */

  @Test(dataProvider = "caches")
  @CacheSpec(population = Population.FULL, maximumSize = Maximum.FULL,
      weigher = {CacheWeigher.DEFAULT, CacheWeigher.TEN}, removalListener = Listener.REJECTING)
  public void removalListener_fails(Cache<Integer, Integer> cache, CacheContext context) {
    RejectingRemovalListener<Integer, Integer> removalListener =
        (RejectingRemovalListener<Integer, Integer>) context.removalListener();
    // Guava-style caches reject before the max size is reached & are unpredictable
    removalListener.rejected = 0;
    long size = cache.estimatedSize();
    for (Integer key : context.absentKeys()) {
      cache.put(key, -key);
      if (cache.estimatedSize() != ++size) {
        break;
      }
    }
    assertThat(removalListener.rejected, is(1));
  }

  /* ---------------- Evict (size/weight) -------------- */

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL,
      maximumSize = { Maximum.ZERO, Maximum.ONE, Maximum.FULL },
      weigher = {CacheWeigher.DEFAULT, CacheWeigher.TEN})
  public void evict(Cache<Integer, Integer> cache, CacheContext context,
      Eviction<Integer, Integer> eviction) {
    cache.putAll(context.absent());
    if (eviction.isWeighted()) {
      assertThat(eviction.weightedSize().getAsLong(), is(context.maximumWeight()));
    } else {
      assertThat(cache.estimatedSize(), is(context.maximumSize()));
    }
    int count = context.absentKeys().size();
    assertThat(context, hasEvictionCount(count));
    assertThat(cache, hasRemovalNotifications(context, count, RemovalCause.SIZE));

    verifyWriter(context, (verifier, writer) -> {
      Map<Integer, Integer> all = new HashMap<>(context.original());
      all.putAll(context.absent());
      MapDifference<Integer, Integer> diff = Maps.difference(all, cache.asMap());
      verifier.deletedAll(diff.entriesOnlyOnLeft(), RemovalCause.SIZE);
    });
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY,
      initialCapacity = InitialCapacity.EXCESSIVE, maximumSize = Maximum.TEN,
      weigher = CacheWeigher.COLLECTION, keys = ReferenceType.STRONG, values = ReferenceType.STRONG)
  public void evict_weighted(Cache<Integer, List<Integer>> cache,
      CacheContext context, Eviction<?, ?> eviction) {
    @SuppressWarnings({"unchecked", "rawtypes"})
    CacheWriter<Integer, List<Integer>> writer = (CacheWriter) context.cacheWriter();

    // Enforce full initialization of internal structures
    for (int i = 0; i < context.maximumSize(); i++) {
      cache.put(i, Collections.emptyList());
    }
    cache.invalidateAll();
    Mockito.<Object>reset(writer);

    List<Integer> value1 = asList(8, 9, 10);
    List<Integer> value2 = asList(3, 4, 5, 6, 7);
    List<Integer> value3 = asList(1, 2);
    List<Integer> value4 = asList(11);
    List<Integer> value5 = asList(12, 13, 14, 15, 16, 17, 18, 19, 20);

    // Never evicted
    cache.put(0, asList());

    cache.put(1, value1);
    cache.put(2, value2);
    cache.put(3, value3);
    await().until(cache::estimatedSize, is(4L));
    assertThat(eviction.weightedSize().getAsLong(), is(10L));

    // [0 | 1, 2, 3] -> [0, 4 | 2, 3]
    cache.put(4, value4);
    await().until(cache::estimatedSize, is(4L));
    assertThat(cache.asMap().containsKey(1), is(false));
    assertThat(eviction.weightedSize().getAsLong(), is(8L));
    verifyWriter(context, (verifier, ignored) -> {
      verify(writer).delete(1, value1, RemovalCause.SIZE);
      verifier.deletions(1, RemovalCause.SIZE);
    });

    // [0, 4 | 2, 3] remains (5 exceeds window and has the same usage history, so evicted)
    cache.put(5, value5);
    await().until(cache::estimatedSize, is(4L));
    assertThat(eviction.weightedSize().getAsLong(), is(8L));
    verifyWriter(context, (verifier, ignored) -> {
      verify(writer).delete(5, value5, RemovalCause.SIZE);
      verifier.deletions(2);
    });
    assertThat(context, hasEvictionCount(2L));
    assertThat(context, hasEvictionWeight(12L));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(population = Population.EMPTY, removalListener = Listener.CONSUMING,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG,
      maximumSize = Maximum.TEN, weigher = CacheWeigher.VALUE)
  public void evict_weighted_entryTooBig(Cache<Integer, Integer> cache, CacheContext context) {
    cache.put(1, 1);
    cache.put(9, 9);
    assertThat(cache.estimatedSize(), is(2L));
    cache.policy().eviction().ifPresent(eviction -> {
      assertThat(eviction.weightedSize().getAsLong(), is(10L));
    });

    cache.put(20, 20);
    assertThat(cache.estimatedSize(), is(2L));
    cache.policy().eviction().ifPresent(eviction -> {
      assertThat(eviction.weightedSize().getAsLong(), is(10L));
    });
    assertThat(context.consumedNotifications(), is(equalTo(ImmutableList.of(
        new RemovalNotification<>(20, 20, RemovalCause.SIZE)))));
    if (context.isCaffeine()) {
      assertThat(context, hasEvictionWeight(20L));
    }
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.TEN,
      weigher = CacheWeigher.VALUE, population = Population.EMPTY,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG,
      removalListener = Listener.CONSUMING)
  @SuppressWarnings("FutureReturnValueIgnored")
  public void evict_weighted_async(AsyncLoadingCache<Integer, Integer> cache,
      CacheContext context, Eviction<?, ?> eviction) {
    AtomicBoolean ready = new AtomicBoolean();
    AtomicBoolean done = new AtomicBoolean();
    CompletableFuture<Integer> valueFuture = CompletableFuture.supplyAsync(() -> {
      Awaits.await().untilTrue(ready);
      return 6;
    });
    valueFuture.whenComplete((r, e) -> done.set(true));

    cache.put(5, CompletableFuture.completedFuture(5));
    cache.put(4, CompletableFuture.completedFuture(4));
    cache.put(6, valueFuture);
    assertThat(eviction.weightedSize().getAsLong(), is(9L));
    assertThat(cache.synchronous().estimatedSize(), is(3L));

    ready.set(true);
    Awaits.await().untilTrue(done);
    Awaits.await().until(context::consumedNotifications, hasSize(1));
    Awaits.await().until(() -> cache.synchronous().estimatedSize(), is(2L));
    Awaits.await().until(() -> eviction.weightedSize().getAsLong(), is(10L));

    assertThat(context, hasEvictionWeight(5L));
    assertThat(context, hasRemovalNotifications(context, 1, RemovalCause.SIZE));
    verifyWriter(context, (verifier, writer) -> verifier.deletions(1, RemovalCause.SIZE));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.ZERO,
      weigher = CacheWeigher.COLLECTION, population = Population.EMPTY,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG)
  @SuppressWarnings("FutureReturnValueIgnored")
  public void evict_zero_async(AsyncLoadingCache<Integer, List<Integer>> cache,
      CacheContext context, Eviction<?, ?> eviction) {
    AtomicBoolean ready = new AtomicBoolean();
    AtomicBoolean done = new AtomicBoolean();
    CompletableFuture<List<Integer>> valueFuture = CompletableFuture.supplyAsync(() -> {
      Awaits.await().untilTrue(ready);
      return ImmutableList.of(1, 2, 3, 4, 5);
    });
    valueFuture.whenComplete((r, e) -> done.set(true));

    cache.put(context.absentKey(), valueFuture);
    assertThat(eviction.weightedSize().getAsLong(), is(0L));
    assertThat(cache.synchronous().estimatedSize(), is(1L));

    ready.set(true);
    Awaits.await().untilTrue(done);
    Awaits.await().until(() -> eviction.weightedSize().getAsLong(), is(0L));
    Awaits.await().until(() -> cache.synchronous().estimatedSize(), is(0L));

    assertThat(context, hasRemovalNotifications(context, 1, RemovalCause.SIZE));
    verifyWriter(context, (verifier, writer) -> verifier.deletions(1, RemovalCause.SIZE));
  }

  @CheckNoStats
  @Test(dataProvider = "caches", expectedExceptions = DeleteException.class)
  @CacheSpec(implementation = Implementation.Caffeine, keys = ReferenceType.STRONG,
      population = Population.FULL, maximumSize = Maximum.FULL,
      weigher = {CacheWeigher.DEFAULT, CacheWeigher.TEN},
      compute = Compute.SYNC, writer = Writer.EXCEPTIONAL, removalListener = Listener.REJECTING)
  public void evict_writerFails(Cache<Integer, Integer> cache, CacheContext context) {
    try {
      cache.policy().eviction().ifPresent(policy -> policy.setMaximum(0));
    } finally {
      context.disableRejectingCacheWriter();
      assertThat(cache.asMap(), equalTo(context.original()));
    }
  }

  /* ---------------- Weighted -------------- */

  @CheckNoWriter
  @CacheSpec(maximumSize = Maximum.FULL,
      weigher = CacheWeigher.NEGATIVE, population = Population.EMPTY)
  @Test(dataProvider = "caches",
      expectedExceptions = { IllegalArgumentException.class, IllegalStateException.class })
  public void put_negativeWeight(Cache<Integer, Integer> cache, CacheContext context) {
    cache.put(context.absentKey(), context.absentValue());
  }

  @CacheSpec(maximumSize = Maximum.FULL,
      weigher = CacheWeigher.ZERO, population = Population.EMPTY)
  @Test(dataProvider = "caches")
  public void put_zeroWeight(Cache<Integer, Integer> cache, CacheContext context) {
    cache.put(context.absentKey(), context.absentValue());
    verifyWriter(context, (verifier, writer) -> {
      verifier.wrote(context.absentKey(), context.absentValue());
    });
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      weigher = CacheWeigher.COLLECTION, population = Population.EMPTY,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG)
  public void put(Cache<String, List<Integer>> cache,
      CacheContext context, Eviction<?, ?> eviction) {
    cache.put("a", asList(1, 2, 3));
    assertThat(cache.estimatedSize(), is(1L));
    assertThat(eviction.weightedSize().getAsLong(), is(3L));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      weigher = CacheWeigher.COLLECTION, population = Population.EMPTY,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG)
  public void put_sameWeight(Cache<String, List<Integer>> cache,
      CacheContext context, Eviction<?, ?> eviction) {
    cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1)));

    cache.put("a", asList(-1, -2, -3));
    assertThat(cache.estimatedSize(), is(2L));
    assertThat(eviction.weightedSize().getAsLong(), is(4L));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      weigher = CacheWeigher.COLLECTION, population = Population.EMPTY,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG)
  public void put_changeWeight(Cache<String, List<Integer>> cache,
      CacheContext context, Eviction<?, ?> eviction) {
    @SuppressWarnings({"unchecked", "rawtypes"})
    CacheWriter<String, List<Integer>> writer = (CacheWriter) context.cacheWriter();

    cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1)));

    cache.put("a", asList(-1, -2, -3, -4));
    assertThat(cache.estimatedSize(), is(2L));
    assertThat(eviction.weightedSize().getAsLong(), is(5L));

    verifyWriter(context, (verifier, ignored) -> {
      verify(writer).write("a", asList(1, 2, 3));
      verify(writer).write("b", asList(1));
      verify(writer).write("a", asList(-1, -2, -3, -4));
    });
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      weigher = CacheWeigher.COLLECTION, population = Population.EMPTY,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG)
  @SuppressWarnings("FutureReturnValueIgnored")
  public void put_asyncWeight(AsyncLoadingCache<Integer, List<Integer>> cache,
      CacheContext context, Eviction<?, ?> eviction) {
    AtomicBoolean ready = new AtomicBoolean();
    AtomicBoolean done = new AtomicBoolean();
    CompletableFuture<List<Integer>> valueFuture = CompletableFuture.supplyAsync(() -> {
      Awaits.await().untilTrue(ready);
      return ImmutableList.of(1, 2, 3, 4, 5);
    });
    valueFuture.whenComplete((r, e) -> done.set(true));

    cache.put(context.absentKey(), valueFuture);
    assertThat(eviction.weightedSize().getAsLong(), is(0L));
    assertThat(cache.synchronous().estimatedSize(), is(1L));

    ready.set(true);
    Awaits.await().untilTrue(done);
    Awaits.await().until(() -> eviction.weightedSize().getAsLong(), is(5L));
    Awaits.await().until(() -> cache.synchronous().estimatedSize(), is(1L));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      weigher = CacheWeigher.COLLECTION, population = Population.EMPTY,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG)
  public void replace_sameWeight(Cache<String, List<Integer>> cache,
      CacheContext context, Eviction<?, ?> eviction) {
    cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1)));

    cache.asMap().replace("a", asList(-1, -2, -3));
    assertThat(cache.estimatedSize(), is(2L));
    assertThat(eviction.weightedSize().getAsLong(), is(4L));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      weigher = CacheWeigher.COLLECTION, population = Population.EMPTY,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG)
  public void replace_changeWeight(Cache<String, List<Integer>> cache,
      CacheContext context, Eviction<?, ?> eviction) {
    cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1)));

    cache.asMap().replace("a", asList(-1, -2, -3, -4));
    assertThat(cache.estimatedSize(), is(2L));
    assertThat(eviction.weightedSize().getAsLong(), is(5L));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      weigher = CacheWeigher.COLLECTION, population = Population.EMPTY,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG)
  public void replaceConditionally_sameWeight(
      Cache<String, List<Integer>> cache, CacheContext context, Eviction<?, ?> eviction) {
    cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1)));

    assertThat(cache.asMap().replace("a", asList(1, 2, 3), asList(4, 5, 6)), is(true));
    assertThat(cache.estimatedSize(), is(2L));
    assertThat(eviction.weightedSize().getAsLong(), is(4L));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      weigher = CacheWeigher.COLLECTION, population = Population.EMPTY,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG)
  public void replaceConditionally_changeWeight(
      Cache<String, List<Integer>> cache, CacheContext context, Eviction<?, ?> eviction) {
    cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1)));

    cache.asMap().replace("a", asList(1, 2, 3), asList(-1, -2, -3, -4));
    assertThat(cache.estimatedSize(), is(2L));
    assertThat(eviction.weightedSize().getAsLong(), is(5L));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      weigher = CacheWeigher.COLLECTION, population = Population.EMPTY,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG)
  public void replaceConditionally_fails(
      Cache<String, List<Integer>> cache, CacheContext context, Eviction<?, ?> eviction) {
    cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1)));

    assertThat(cache.asMap().replace("a", asList(1), asList(4, 5)), is(false));
    assertThat(cache.estimatedSize(), is(2L));
    assertThat(eviction.weightedSize().getAsLong(), is(4L));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      weigher = CacheWeigher.COLLECTION, population = Population.EMPTY,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG)
  public void remove(Cache<String, List<Integer>> cache,
      CacheContext context, Eviction<?, ?> eviction) {
    cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1)));

    assertThat(cache.asMap().remove("a"), is(asList(1, 2, 3)));
    assertThat(cache.estimatedSize(), is(1L));
    assertThat(eviction.weightedSize().getAsLong(), is(1L));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      weigher = CacheWeigher.COLLECTION, population = Population.EMPTY,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG)
  public void removeConditionally(Cache<String, List<Integer>> cache,
      CacheContext context, Eviction<?, ?> eviction) {
    cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1)));

    assertThat(cache.asMap().remove("a", asList(1, 2, 3)), is(true));
    assertThat(cache.estimatedSize(), is(1L));
    assertThat(eviction.weightedSize().getAsLong(), is(1L));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      weigher = CacheWeigher.COLLECTION, population = Population.EMPTY,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG)
  public void removeConditionally_fails(
      Cache<String, List<Integer>> cache, CacheContext context, Eviction<?, ?> eviction) {
    cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1)));

    assertThat(cache.asMap().remove("a", asList(-1, -2, -3)), is(false));
    assertThat(cache.estimatedSize(), is(2L));
    assertThat(eviction.weightedSize().getAsLong(), is(4L));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      weigher = CacheWeigher.COLLECTION, population = Population.EMPTY,
      keys = ReferenceType.STRONG, values = ReferenceType.STRONG)
  public void invalidateAll(Cache<String, List<Integer>> cache,
      CacheContext context, Eviction<?, ?> eviction) {
    cache.putAll(ImmutableMap.of("a", asList(1, 2, 3), "b", asList(1)));

    cache.invalidateAll();
    assertThat(cache.estimatedSize(), is(0L));
    assertThat(eviction.weightedSize().getAsLong(), is(0L));
  }

  /* ---------------- Policy: IsWeighted -------------- */

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine,
      maximumSize = Maximum.FULL, population = Population.EMPTY)
  public void isWeighted(CacheContext context, Eviction<Integer, Integer> eviction) {
    assertThat(eviction.isWeighted(), is(context.isWeighted()));
  }

  /* ---------------- Policy: WeightOf -------------- */

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, population = Population.EMPTY,
      maximumSize = Maximum.UNREACHABLE, weigher = CacheWeigher.VALUE)
  public void weightOf(Cache<Integer, Integer> cache, CacheContext context,
      Eviction<Integer, Integer> eviction) {
    Integer key = 1;
    cache.put(key, 1);
    assertThat(eviction.weightOf(key).getAsInt(), is(1));

    cache.put(key, 2);
    assertThat(eviction.weightOf(key).getAsInt(), is(2));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL)
  public void weightOf_absent(Cache<Integer, Integer> cache, CacheContext context,
      Eviction<Integer, Integer> eviction) {
    assertThat(eviction.weightOf(context.absentKey()), is(OptionalInt.empty()));
  }

  /* ---------------- Policy: WeightedSize -------------- */

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine,
      maximumSize = Maximum.FULL, weigher = CacheWeigher.TEN)
  public void weightedSize(Cache<Integer, Integer> cache, CacheContext context,
      Eviction<Integer, Integer> eviction) {
    long weightedSize = 0;
    for (Integer key : cache.asMap().keySet()) {
      weightedSize += eviction.weightOf(key).getAsInt();
    }
    assertThat(weightedSize, is(eviction.weightedSize().getAsLong()));
    assertThat(eviction.weightedSize().getAsLong(), is(10 * cache.estimatedSize()));
  }

  /* ---------------- Policy: MaximumSize -------------- */

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      removalListener = { Listener.DEFAULT, Listener.CONSUMING })
  public void maximumSize_decrease(Cache<Integer, Integer> cache,
      CacheContext context, Eviction<Integer, Integer> eviction) {
    long newSize = context.maximumWeightOrSize() / 2;
    eviction.setMaximum(newSize);
    assertThat(eviction.getMaximum(), is(newSize));
    if (context.initialSize() > newSize) {
      if (context.isZeroWeighted()) {
        assertThat(cache.estimatedSize(), is(context.initialSize()));
        assertThat(cache, hasRemovalNotifications(context, 0, RemovalCause.SIZE));
      } else {
        assertThat(cache.estimatedSize(), is(newSize));
        assertThat(cache, hasRemovalNotifications(context, newSize, RemovalCause.SIZE));
      }
    }
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine,
      maximumSize = Maximum.FULL, weigher = { CacheWeigher.DEFAULT, CacheWeigher.TEN },
      removalListener = { Listener.DEFAULT, Listener.CONSUMING })
  public void maximumSize_decrease_min(Cache<Integer, Integer> cache,
      CacheContext context, Eviction<Integer, Integer> eviction) {
    eviction.setMaximum(0);
    assertThat(eviction.getMaximum(), is(0L));
    if (context.initialSize() > 0) {
      long expect = context.isZeroWeighted() ? context.initialSize() : 0;
      assertThat(cache.estimatedSize(), is(expect));
    }
    assertThat(cache, hasRemovalNotifications(context, context.initialSize(), RemovalCause.SIZE));
  }

  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      removalListener = { Listener.DEFAULT, Listener.CONSUMING })
  @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class)
  public void maximumSize_decrease_negative(Cache<Integer, Integer> cache,
      CacheContext context, Eviction<Integer, Integer> eviction) {
    try {
      eviction.setMaximum(-1);
    } finally {
      assertThat(eviction.getMaximum(), is(context.maximumWeightOrSize()));
      assertThat(cache, hasRemovalNotifications(context, 0, RemovalCause.SIZE));
    }
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL,
      removalListener = { Listener.DEFAULT, Listener.REJECTING })
  public void maximumSize_increase(Cache<Integer, Integer> cache,
      CacheContext context, Eviction<Integer, Integer> eviction) {
    eviction.setMaximum(2 * context.maximumWeightOrSize());
    assertThat(cache.estimatedSize(), is(context.initialSize()));
    assertThat(eviction.getMaximum(), is(2 * context.maximumWeightOrSize()));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine,
      maximumSize = Maximum.FULL, removalListener = Listener.REJECTING)
  public void maximumSize_increase_max(Cache<Integer, Integer> cache,
      CacheContext context, Eviction<Integer, Integer> eviction) {
    eviction.setMaximum(Long.MAX_VALUE);
    assertThat(cache.estimatedSize(), is(context.initialSize()));
    assertThat(eviction.getMaximum(), is(Long.MAX_VALUE - Integer.MAX_VALUE)); // impl detail
  }

  /* ---------------- Policy: Coldest -------------- */

  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL)
  @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class)
  public void coldest_unmodifiable(CacheContext context, Eviction<Integer, Integer> eviction) {
    eviction.coldest(Integer.MAX_VALUE).clear();;
  }

  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL)
  @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class)
  public void coldest_negative(CacheContext context, Eviction<Integer, Integer> eviction) {
    eviction.coldest(-1);
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL)
  public void coldest_zero(CacheContext context, Eviction<Integer, Integer> eviction) {
    assertThat(eviction.coldest(0), is(emptyMap()));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL,
      initialCapacity = InitialCapacity.EXCESSIVE, maximumSize = Maximum.FULL)
  public void coldest_partial(CacheContext context, Eviction<Integer, Integer> eviction) {
    int count = (int) context.initialSize() / 2;
    assertThat(eviction.coldest(count).size(), is(count));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL,
      initialCapacity = InitialCapacity.EXCESSIVE, maximumSize = Maximum.FULL,
      weigher = { CacheWeigher.DEFAULT, CacheWeigher.TEN },
      removalListener = { Listener.DEFAULT, Listener.REJECTING })
  public void coldest_order(CacheContext context, Eviction<Integer, Integer> eviction) {
    Set<Integer> keys = new LinkedHashSet<>(context.original().keySet());
    Set<Integer> coldest = new LinkedHashSet<>(eviction.coldest(Integer.MAX_VALUE).keySet());

    // Ignore the last key; hard to predict with W-TinyLFU
    keys.remove(context.lastKey());
    coldest.remove(context.lastKey());

    assertThat(coldest, contains(keys.toArray()));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, initialCapacity = InitialCapacity.EXCESSIVE,
      maximumSize = Maximum.FULL)
  public void coldest_snapshot(Cache<Integer, Integer> cache, CacheContext context,
      Eviction<Integer, Integer> eviction) {
    Map<Integer, Integer> coldest = eviction.coldest(Integer.MAX_VALUE);
    cache.invalidateAll();
    assertThat(coldest, is(equalTo(context.original())));
  }

  /* ---------------- Policy: Hottest -------------- */

  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL)
  @Test(dataProvider = "caches", expectedExceptions = UnsupportedOperationException.class)
  public void hottest_unmodifiable(CacheContext context, Eviction<Integer, Integer> eviction) {
    eviction.hottest(Integer.MAX_VALUE).clear();
  }

  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL)
  @Test(dataProvider = "caches", expectedExceptions = IllegalArgumentException.class)
  public void hottest_negative(CacheContext context, Eviction<Integer, Integer> eviction) {
    eviction.hottest(-1);
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, maximumSize = Maximum.FULL)
  public void hottest_zero(CacheContext context, Eviction<Integer, Integer> eviction) {
    assertThat(eviction.hottest(0), is(emptyMap()));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL,
      initialCapacity = InitialCapacity.EXCESSIVE, maximumSize = Maximum.FULL)
  public void hottest_partial(CacheContext context, Eviction<Integer, Integer> eviction) {
    int count = (int) context.initialSize() / 2;
    assertThat(eviction.hottest(count).size(), is(count));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, population = Population.FULL,
      initialCapacity = InitialCapacity.EXCESSIVE, maximumSize = Maximum.FULL,
      removalListener = { Listener.DEFAULT, Listener.REJECTING })
  public void hottest_order(CacheContext context, Eviction<Integer, Integer> eviction) {
    Set<Integer> keys = new LinkedHashSet<>(context.original().keySet());
    Set<Integer> hottest = eviction.hottest(Integer.MAX_VALUE).keySet();
    Set<Integer> coldest = new LinkedHashSet<>(ImmutableList.copyOf(hottest).reverse());

    // Ignore the last key; hard to predict with W-TinyLFU
    keys.remove(context.lastKey());
    coldest.remove(context.lastKey());

    assertThat(coldest, contains(keys.toArray()));
  }

  @Test(dataProvider = "caches")
  @CacheSpec(implementation = Implementation.Caffeine, initialCapacity = InitialCapacity.EXCESSIVE,
      maximumSize = Maximum.FULL)
  public void hottest_snapshot(Cache<Integer, Integer> cache,
      CacheContext context, Eviction<Integer, Integer> eviction) {
    Map<Integer, Integer> hottest = eviction.hottest(Integer.MAX_VALUE);
    cache.invalidateAll();
    assertThat(hottest, is(equalTo(context.original())));
  }
}
