/*
 * Copyright 2016 Branimir Lambov. 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.issues;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;

import java.util.Set;
import java.util.function.Function;

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

import com.github.benmanes.caffeine.cache.Cache;
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.Maximum;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Population;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Stats;
import com.github.benmanes.caffeine.cache.testing.CacheValidationListener;

/**
 * CASSANDRA-11452: Hash collisions cause the cache to not admit new entries.
 *
 * @author Branimir Lambov (github.com/blambov)
 * @author ben.manes@gmail.com (Ben Manes)
 */
@Listeners(CacheValidationListener.class)
@Test(groups = "isolated", dataProviderClass = CacheProvider.class)
public final class HashClashTest {
  private static final int STEP = 5;
  private static final Long LONG_1 = 1L;
  private static final long ITERS = 200_000;
  private static final boolean debug = false;

  @Test(dataProvider = "caches")
  @CacheSpec(population = Population.EMPTY, maximumSize = Maximum.ONE_FIFTY, stats = Stats.ENABLED)
  private void testCache(Cache<Long, Long> cache, CacheContext context) {
    for (long j = 0; j < 300; ++j) {
      cache.get(1L, Function.identity());
      cache.get(j, Function.identity());
    }
    printStats(cache);
    printKeys(cache);

    // add a hashcode clash for 1
    Long CLASH = (ITERS << 32) ^ ITERS ^ 1;
    assertThat(CLASH.hashCode(), is(LONG_1.hashCode()));
    cache.get(CLASH, Function.identity());
    printKeys(cache);

    // repeat some entries to let CLASH flow to the probation head
    for (long j = 0; j < 300; ++j) {
      cache.get(1L, Function.identity());
      cache.get(j, Function.identity());
    }
    printKeys(cache);

    // Now run a repeating sequence which has a longer length than eden space size.
    for (long i = 0; i < ITERS; i += STEP) {
      cache.get(1L, Function.identity());
      for (long j = 0; j < STEP; ++j) {
        cache.get(-j, Function.identity());
      }
    }
    printKeys(cache);
    printStats(cache);

    assertThat(cache.stats().hitRate(), is(greaterThan(0.99d)));
  }

  static void printStats(Cache<Long, Long> cache) {
    if (debug) {
      System.out.printf("size %,d requests %,d hit ratio %f%n",
          cache.estimatedSize(), cache.stats().requestCount(), cache.stats().hitRate());
    }
  }

  static void printKeys(Cache<Long, Long> cache) {
    if (debug) {
      Set<Long> keys = cache.policy().eviction()
          .map(policy -> policy.hottest(Integer.MAX_VALUE).keySet())
          .orElseGet(cache.asMap()::keySet);
      System.out.println(keys);
    }
  }
}
