/*
 * Copyright 2016 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.jcache.expiry;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.anEmptyMap;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;

import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import javax.cache.expiry.AccessedExpiryPolicy;
import javax.cache.expiry.Duration;
import javax.cache.expiry.ExpiryPolicy;
import javax.cache.processor.EntryProcessorResult;

import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import com.github.benmanes.caffeine.jcache.AbstractJCacheTest;
import com.github.benmanes.caffeine.jcache.Expirable;
import com.github.benmanes.caffeine.jcache.configuration.CaffeineConfiguration;

/**
 * The test cases that ensure the <tt>expiry for access</tt> time is updated for the accessed
 * entries. The TCK asserts that the {@link ExpiryPolicy#getExpiryForAccess()} is only called for
 * the following methods, but does not check that the expiration time was updated.
 * <ul>
 *   <li>get
 *   <li>getAll
 *   <li>invoke
 *   <li>invokeAll
 *   <li>conditional remove (failure)
 *   <li>conditional replace (failure)
 * </ul>
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
@Test(singleThreaded = true)
public final class JCacheAccessExpiryTest extends AbstractJCacheTest {

  @BeforeMethod
  public void setup() {
    for (int i = 0; i < 100; i++) {
      jcacheLoading.put(i, -i);
      jcache.put(i, -i);
    }
  }

  @Override
  protected CaffeineConfiguration<Integer, Integer> getConfiguration() {
    CaffeineConfiguration<Integer, Integer> configuration = new CaffeineConfiguration<>();
    configuration.setExpiryPolicyFactory(() -> new AccessedExpiryPolicy(
        new Duration(TimeUnit.MILLISECONDS, EXPIRY_DURATION)));
    configuration.setTickerFactory(() -> ticker::read);
    return configuration;
  }

  @DataProvider(name = "eternal")
  public Object[] providesEternal() {
    return new Object[] { true, false };
  }

  /* ---------------- get -------------- */

  @Test
  public void get_absent() {
    assertThat(jcache.get(KEY_1), is(VALUE_1));

    advancePastExpiry();
    assertThat(jcache.get(KEY_1), is(nullValue()));
    assertThat(getExpirable(jcache, KEY_1), is(nullValue()));
  }

  @Test(dataProvider = "eternal")
  public void get_present(boolean eternal) {
    Expirable<Integer> expirable = getExpirable(jcache, KEY_1);
    if (eternal) {
      expirable.setExpireTimeMS(Long.MAX_VALUE);
    }

    assertThat(jcache.get(KEY_1), is(VALUE_1));
    assertThat(expirable.getExpireTimeMS(), is(currentTimeMillis() + EXPIRY_DURATION));
  }

  /* ---------------- get (loading) -------------- */

  @Test
  public void get_loading_absent() {
    assertThat(jcacheLoading.get(KEY_1), is(VALUE_1));

    advancePastExpiry();
    assertThat(jcacheLoading.get(KEY_1), is(KEY_1));

    Expirable<Integer> expirable = getExpirable(jcacheLoading, KEY_1);
    assertThat(expirable.getExpireTimeMS(), is(currentTimeMillis() + EXPIRY_DURATION));
  }

  @Test(dataProvider = "eternal")
  public void get_loading_present(boolean eternal) {
    Expirable<Integer> expirable = getExpirable(jcacheLoading, KEY_1);
    if (eternal) {
      expirable.setExpireTimeMS(Long.MAX_VALUE);
    }

    assertThat(jcacheLoading.get(KEY_1), is(VALUE_1));
    assertThat(expirable.getExpireTimeMS(), is(currentTimeMillis() + EXPIRY_DURATION));
  }

  /* ---------------- getAllPresent -------------- */

  @Test
  public void getAll_absent() {
    assertThat(jcache.getAll(keys), is(entries));

    advancePastExpiry();
    assertThat(jcache.getAll(keys), is(anEmptyMap()));

    for (Integer key : keys) {
      assertThat(getExpirable(jcache, key), is(nullValue()));
    }
  }

  @Test(dataProvider = "eternal")
  public void getAll_present(boolean eternal) {
    for (Integer key : keys) {
      Expirable<Integer> expirable = getExpirable(jcacheLoading, key);
      if (eternal) {
        expirable.setExpireTimeMS(Long.MAX_VALUE);
      }
    }

    assertThat(jcache.getAll(keys), is(entries));

    for (Integer key : keys) {
      Expirable<Integer> expirable = getExpirable(jcache, key);
      assertThat(expirable.getExpireTimeMS(), is(currentTimeMillis() + EXPIRY_DURATION));
    }
  }

  /* ---------------- invoke -------------- */

  @Test
  public void invoke_absent() {
    assertThat(jcache.get(KEY_1), is(VALUE_1));

    advancePastExpiry();
    assertThat(jcache.invoke(KEY_1, (entry, args) -> entry.getValue()), is(nullValue()));

    assertThat(getExpirable(jcache, KEY_1), is(nullValue()));
  }

  @Test(dataProvider = "eternal")
  public void invoke_present(boolean eternal) {
    Expirable<Integer> expirable = getExpirable(jcache, KEY_1);
    if (eternal) {
      expirable.setExpireTimeMS(Long.MAX_VALUE);
    }

    assertThat(jcache.invoke(KEY_1, (entry, args) -> entry.getValue()), is(VALUE_1));
    assertThat(expirable.getExpireTimeMS(), is(currentTimeMillis() + EXPIRY_DURATION));
  }

  /* ---------------- invokeAll -------------- */

  @Test
  public void invokeAll_absent() {
    assertThat(jcache.getAll(keys), is(entries));

    advancePastExpiry();
    assertThat(jcache.invokeAll(keys, (entry, args) -> entry.getValue()), is(anEmptyMap()));

    for (Integer key : keys) {
      assertThat(getExpirable(jcache, key), is(nullValue()));
    }
  }

  @Test
  public void invokeAll_present() {
    Map<Integer, EntryProcessorResult<Integer>> result =
        jcache.invokeAll(keys, (entry, args) -> entry.getValue());
    Map<Integer, Integer> unwrapped = result.entrySet().stream().collect(
        Collectors.toMap(Entry::getKey, entry -> entry.getValue().get()));
    assertThat(unwrapped, is(entries));

    for (Integer key : keys) {
      Expirable<Integer> expirable = getExpirable(jcache, key);
      assertThat(expirable.getExpireTimeMS(), is(currentTimeMillis() + EXPIRY_DURATION));
    }
  }

  /* ---------------- conditional remove -------------- */

  @Test
  public void removeConditionally() {
    assertThat(jcache.remove(KEY_1, VALUE_2), is(false));

    Expirable<Integer> expirable = getExpirable(jcache, KEY_1);
    assertThat(expirable.getExpireTimeMS(), is(currentTimeMillis() + EXPIRY_DURATION));
  }

  /* ---------------- conditional replace -------------- */

  @Test
  public void replaceConditionally() {
    assertThat(jcache.replace(KEY_1, VALUE_2, VALUE_3), is(false));

    Expirable<Integer> expirable = getExpirable(jcache, KEY_1);
    assertThat(expirable.getExpireTimeMS(), is(currentTimeMillis() + EXPIRY_DURATION));
  }
}
