/*
 * 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.testing;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static java.util.Objects.requireNonNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.when;

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

import org.mockito.Mockito;

import com.github.benmanes.caffeine.cache.AsyncCacheLoader;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.CacheWriter;
import com.github.benmanes.caffeine.cache.Expiry;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.RemovalListener;
import com.github.benmanes.caffeine.cache.Weigher;
import com.github.benmanes.caffeine.cache.testing.RemovalListeners.ConsumingRemovalListener;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Interner;
import com.google.common.collect.Interners;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

/**
 * The cache test specification so that a {@link org.testng.annotations.DataProvider} can construct
 * the maximum number of cache combinations to test against.
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
@SuppressWarnings("ImmutableEnumChecker")
@Target(METHOD) @Retention(RUNTIME)
public @interface CacheSpec {

  /* ---------------- Compute -------------- */

  /**
   * Indicates whether the test supports a cache allowing for asynchronous computations. This is
   * for implementation specific tests that may inspect the internal state of a down casted cache.
   */
  Compute[] compute() default {
    Compute.ASYNC,
    Compute.SYNC
  };

  enum Compute {
    ASYNC,
    SYNC,
  }

  /* ---------------- Implementation -------------- */

  /** The implementation, each resulting in a new combination. */
  Implementation[] implementation() default {
    Implementation.Caffeine,
    Implementation.Guava,
  };

  enum Implementation {
    Caffeine,
    Guava
  }

  /* ---------------- Initial capacity -------------- */

  InitialCapacity[] initialCapacity() default {
    InitialCapacity.DEFAULT
  };

  /** The initial capacities, each resulting in a new combination. */
  enum InitialCapacity {
    /** A flag indicating that the initial capacity is not configured. */
    DEFAULT(16),
    /** A configuration where the table grows on the first addition. */
    ZERO(0),
    /** A configuration where the table grows on the second addition. */
    ONE(1),
    /** A configuration where the table grows after the {@link Population#FULL} count. */
    FULL(50),
    /** A configuration where the table grows after the 10 x {@link Population#FULL} count. */
    EXCESSIVE(100);

    private final int size;

    private InitialCapacity(int size) {
      this.size = size;
    }

    public int size() {
      return size;
    }
  }

  /* ---------------- Statistics -------------- */

  Stats[] stats() default {
    Stats.ENABLED,
    Stats.DISABLED
  };

  enum Stats {
    ENABLED,
    DISABLED
  }

  /* ---------------- Maximum size -------------- */

  /** The maximum size, each resulting in a new combination. */
  Maximum[] maximumSize() default {
    Maximum.DISABLED,
    Maximum.UNREACHABLE
  };

  enum Maximum {
    /** A flag indicating that entries are not evicted due to a maximum threshold. */
    DISABLED(Long.MAX_VALUE),
    /** A configuration where entries are evicted immediately. */
    ZERO(0L),
    /** A configuration that holds a single unit. */
    ONE(1L),
    /** A configuration that holds 10 units. */
    TEN(10L),
    /** A configuration that holds 150 units. */
    ONE_FIFTY(150L),
    /** A configuration that holds the {@link Population#FULL} unit count. */
    FULL(InitialCapacity.FULL.size()),
    /** A configuration where the threshold is too high for eviction to occur. */
    UNREACHABLE(Long.MAX_VALUE);

    private final long max;

    private Maximum(long max) {
      this.max = max;
    }

    public long max() {
      return max;
    }
  }

  /* ---------------- Weigher -------------- */

  /** The weigher, each resulting in a new combination. */
  CacheWeigher[] weigher() default {
    CacheWeigher.DEFAULT,
    CacheWeigher.ZERO,
    CacheWeigher.TEN
  };

  enum CacheWeigher implements Weigher<Object, Object> {
    /** A flag indicating that no weigher is set when building the cache. */
    DEFAULT(1),
    /** A flag indicating that every entry is valued at 10 units. */
    TEN(10),
    /** A flag indicating that every entry is valued at 0 unit. */
    ZERO(0),
    /** A flag indicating that every entry is valued at -1 unit. */
    NEGATIVE(-1),
    /** A flag indicating that every entry is valued at Integer.MAX_VALUE units. */
    MAX_VALUE(Integer.MAX_VALUE),
    /** A flag indicating that the entry is weighted by the integer value. */
    VALUE(1) {
      @Override public int weigh(Object key, Object value) {
        requireNonNull(key);
        return ((Integer) value).intValue();
      }
    },
    /** A flag indicating that the entry is weighted by the value's collection size. */
    COLLECTION(1) {
      @Override public int weigh(Object key, Object value) {
        requireNonNull(key);
        return ((Collection<?>) value).size();
      }
    },
    /** A flag indicating that the entry's weight is randomly changing. */
    RANDOM(1) {
      @Override public int weigh(Object key, Object value) {
        requireNonNull(key);
        requireNonNull(value);
        return ThreadLocalRandom.current().nextInt(1, 10);
      }
    };

    private final int units;

    private CacheWeigher(int multiplier) {
      this.units = multiplier;
    }

    @Override
    public int weigh(Object key, Object value) {
      requireNonNull(key);
      requireNonNull(value);
      return units;
    }

    public int unitsPerEntry() {
      return units;
    }
  }

  /* ---------------- Expiration -------------- */

  /** Indicates that the combination must have any of the expiration settings. */
  Expiration[] mustExpireWithAnyOf() default {};

  enum Expiration {
    AFTER_WRITE, AFTER_ACCESS, VARIABLE
  }

  /** The expiration time-to-idle setting, each resulting in a new combination. */
  Expire[] expireAfterAccess() default {
    Expire.DISABLED,
    Expire.FOREVER
  };

  /** The expiration time-to-live setting, each resulting in a new combination. */
  Expire[] expireAfterWrite() default {
    Expire.DISABLED,
    Expire.FOREVER
  };

  /** The refresh setting, each resulting in a new combination. */
  Expire[] refreshAfterWrite() default {
    Expire.DISABLED,
    Expire.FOREVER
  };

  /** The variable expiration setting, each resulting in a new combination. */
  CacheExpiry[] expiry() default {
    CacheExpiry.DISABLED,
    CacheExpiry.ACCESS
  };

  /** The fixed duration for the expiry. */
  Expire expiryTime() default Expire.FOREVER;

  /** Indicates if the amount of time that should be auto-advance for each entry when populating. */
  Advance[] advanceOnPopulation() default {
    Advance.ZERO
  };

  enum CacheExpiry {
    DISABLED {
      @Override public <K, V> Expiry<K, V> createExpiry(Expire expiryTime) {
        return null;
      }
    },
    MOCKITO {
      @Override public <K, V> Expiry<K, V> createExpiry(Expire expiryTime) {
        @SuppressWarnings("unchecked")
        Expiry<K, V> mock = Mockito.mock(Expiry.class);
        when(mock.expireAfterCreate(any(), any(), anyLong()))
            .thenReturn(expiryTime.timeNanos());
        when(mock.expireAfterUpdate(any(), any(), anyLong(), anyLong()))
            .thenReturn(expiryTime.timeNanos());
        when(mock.expireAfterRead(any(), any(), anyLong(), anyLong()))
            .thenReturn(expiryTime.timeNanos());
        return mock;
      }
    },
    CREATE {
      @Override public <K, V> Expiry<K, V> createExpiry(Expire expiryTime) {
        return ExpiryBuilder
            .expiringAfterCreate(expiryTime.timeNanos())
            .build();
      }
    },
    WRITE {
      @Override public <K, V> Expiry<K, V> createExpiry(Expire expiryTime) {
        return ExpiryBuilder
            .expiringAfterCreate(expiryTime.timeNanos())
            .expiringAfterUpdate(expiryTime.timeNanos())
            .build();
      }
    },
    ACCESS {
      @Override public <K, V> Expiry<K, V> createExpiry(Expire expiryTime) {
        return ExpiryBuilder
            .expiringAfterCreate(expiryTime.timeNanos())
            .expiringAfterUpdate(expiryTime.timeNanos())
            .expiringAfterRead(expiryTime.timeNanos())
            .build();
      }
    };

    public abstract <K, V> Expiry<K, V> createExpiry(Expire expiryTime);
  }

  enum Expire {
    /** A flag indicating that entries are not evicted due to expiration. */
    DISABLED(Long.MIN_VALUE),
    /** A configuration where entries are evicted immediately. */
    IMMEDIATELY(0L),
    /** A configuration where entries are evicted almost immediately. */
    ONE_MILLISECOND(TimeUnit.MILLISECONDS.toNanos(1L)),
    /** A configuration that holds a single entry. */
    ONE_MINUTE(TimeUnit.MINUTES.toNanos(1L)),
    /** A configuration that holds the {@link Population#FULL} count. */
    FOREVER(Long.MAX_VALUE);

    private final long timeNanos;

    private Expire(long timeNanos) {
      this.timeNanos = timeNanos;
    }

    public long timeNanos() {
      return timeNanos;
    }
  }

  /** The time increment to advance by after each entry is added when populating the cache. */
  enum Advance {
    ZERO(0),
    ONE_MINUTE(TimeUnit.MINUTES.toNanos(1L));

    private final long timeNanos;

    private Advance(long timeNanos) {
      this.timeNanos = timeNanos;
    }

    public long timeNanos() {
      return timeNanos;
    }
  }

  /* ---------------- Reference-based -------------- */

  /** Indicates that the combination must have a weak or soft reference collection setting. */
  boolean requiresWeakOrSoft() default false;

  /** The reference type of that the cache holds a key with (strong or weak only). */
  ReferenceType[] keys() default {
    ReferenceType.STRONG,
    ReferenceType.WEAK
  };

  /** The reference type of that the cache holds a value with (strong, soft, or weak). */
  ReferenceType[] values() default {
    ReferenceType.STRONG,
    ReferenceType.WEAK,
    ReferenceType.SOFT
  };

  /** The reference type of cache keys and/or values. */
  enum ReferenceType {
    /** Prevents referent from being reclaimed by the garbage collector. */
    STRONG,

    /** Referent reclaimed when no strong or soft references exist. */
    WEAK,

    /**
     * Referent reclaimed in an LRU fashion when the VM runs low on memory and no strong
     * references exist.
     */
    SOFT
  }

  /* ---------------- Removal -------------- */

  /** The removal listeners, each resulting in a new combination. */
  Listener[] removalListener() default {
    Listener.CONSUMING,
    Listener.DEFAULT,
  };

  enum Listener {
    /** A flag indicating that no removal listener is configured. */
    DEFAULT {
      @Override public <K, V> RemovalListener<K, V> create() {
        return null;
      }
    },
    /** A removal listener that rejects all notifications. */
    REJECTING {
      @Override public <K, V> RemovalListener<K, V> create() {
        return RemovalListeners.rejecting();
      }
    },
    /** A {@link ConsumingRemovalListener} retains all notifications for evaluation by the test. */
    CONSUMING {
      @Override public <K, V> RemovalListener<K, V> create() {
        return RemovalListeners.consuming();
      }
    };

    public abstract <K, V> RemovalListener<K, V> create();
  }

  /* ---------------- CacheLoader -------------- */

  // FIXME: A hack to allow the NEGATIVE loader's return value to be retained on refresh
  static final ThreadLocal<Interner<Integer>> interner =
      ThreadLocal.withInitial(Interners::newStrongInterner);

  Loader[] loader() default {
    Loader.NEGATIVE,
  };

  /** The {@link CacheLoader} for constructing the {@link LoadingCache}. */
  enum Loader implements CacheLoader<Integer, Integer> {
    /** A loader that always returns null (no mapping). */
    NULL {
      @Override public Integer load(Integer key) {
        return null;
      }
    },
    /** A loader that returns the key. */
    IDENTITY {
      @Override public Integer load(Integer key) {
        requireNonNull(key);
        return key;
      }
    },
    /** A loader that returns the key's negation. */
    NEGATIVE {
      @Override public Integer load(Integer key) {
        return interner.get().intern(-key);
      }
    },
    /** A loader that always throws an exception. */
    EXCEPTIONAL {
      @Override public Integer load(Integer key) {
        throw new IllegalStateException();
      }
    },

    /** A loader that always returns null (no mapping). */
    BULK_NULL {
      @Override public Integer load(Integer key) {
        throw new UnsupportedOperationException();
      }
      @Override public Map<Integer, Integer> loadAll(Iterable<? extends Integer> keys) {
        return null;
      }
    },
    BULK_IDENTITY {
      @Override public Integer load(Integer key) {
        throw new UnsupportedOperationException();
      }
      @Override public Map<Integer, Integer> loadAll(Iterable<? extends Integer> keys) {
        Map<Integer, Integer> result = new HashMap<>();
        keys.forEach(key -> result.put(key, key));
        return result;
      }
    },
    BULK_NEGATIVE {
      @Override public Integer load(Integer key) {
        throw new UnsupportedOperationException();
      }
      @Override public Map<Integer, Integer> loadAll(Iterable<? extends Integer> keys) {
        Map<Integer, Integer> result = new HashMap<>();
        keys.forEach(key -> result.put(key, interner.get().intern(-key)));
        return result;
      }
    },
    /** A bulk-only loader that loads more than requested. */
    BULK_NEGATIVE_EXCEEDS {
      @Override public Integer load(Integer key) {
        throw new UnsupportedOperationException();
      }
      @Override public Map<Integer, Integer> loadAll(Iterable<? extends Integer> keys)
          throws Exception {
        List<Integer> moreKeys = new ArrayList<>(ImmutableList.copyOf(keys));
        for (int i = 0; i < 10; i++) {
          moreKeys.add(ThreadLocalRandom.current().nextInt());
        }
        return BULK_NEGATIVE.loadAll(moreKeys);
      }
    },
    /** A bulk-only loader that always throws an exception. */
    BULK_EXCEPTIONAL {
      @Override public Integer load(Integer key) {
        throw new UnsupportedOperationException();
      }
      @Override public Map<Integer, Integer> loadAll(Iterable<? extends Integer> keys) {
        throw new IllegalStateException();
      }
    };

    private final boolean bulk;
    private final AsyncCacheLoader<Integer, Integer> asyncLoader;

    private Loader() {
      bulk = name().startsWith("BULK");
      asyncLoader = bulk
          ? new BulkSeriazableAsyncCacheLoader(this)
          : new SeriazableAsyncCacheLoader(this);
    }

    public boolean isBulk() {
      return bulk;
    }

    /** Returns a serializable view restricted to the {@link AsyncCacheLoader} interface. */
    public AsyncCacheLoader<Integer, Integer> async() {
      return asyncLoader;
    }

    private static class SeriazableAsyncCacheLoader
        implements AsyncCacheLoader<Integer, Integer>, Serializable {
      private static final long serialVersionUID = 1L;

      final Loader loader;

      SeriazableAsyncCacheLoader(Loader loader) {
        this.loader = loader;
      }
      @Override public CompletableFuture<Integer> asyncLoad(Integer key, Executor executor) {
        return loader.asyncLoad(key, executor);
      }
      private Object readResolve() throws ObjectStreamException {
        return loader.asyncLoader;
      }
    }

    private static final class BulkSeriazableAsyncCacheLoader extends SeriazableAsyncCacheLoader {
      private static final long serialVersionUID = 1L;

      BulkSeriazableAsyncCacheLoader(Loader loader) {
        super(loader);
      }
      @Override public CompletableFuture<Integer> asyncLoad(Integer key, Executor executor) {
        throw new IllegalStateException();
      }
      @Override public CompletableFuture<Map<Integer, Integer>> asyncLoadAll(
          Iterable<? extends Integer> keys, Executor executor) {
        return loader.asyncLoadAll(keys, executor);
      }
    }
  }

  /* ---------------- CacheWriter -------------- */

  /** Ignored if weak keys are configured. */
  Writer[] writer() default {
    Writer.MOCKITO,
  };

  /** The {@link CacheWriter} for the external resource. */
  enum Writer {
    /** A writer that does nothing. */
    DISABLED {
      @Override public <K, V> CacheWriter<K, V> create() {
        return CacheWriter.disabledWriter();
      }
    },
    /** A writer that records interactions. */
    MOCKITO {
      @Override public <K, V> CacheWriter<K, V> create() {
        @SuppressWarnings("unchecked")
        CacheWriter<K, V> mock = Mockito.mock(CacheWriter.class);
        return mock;
      }
    },
    /** A writer that always throws an exception. */
    EXCEPTIONAL {
      @Override public <K, V> CacheWriter<K, V> create() {
        return new RejectingCacheWriter<K, V>();
      }
    };

    public abstract <K, V> CacheWriter<K, V> create();
  }

  /* ---------------- Executor -------------- */

  /** The executors retrieved from a supplier, each resulting in a new combination. */
  CacheExecutor[] executor() default {
    CacheExecutor.DIRECT,
  };

  /** If the executor is allowed to have failures. */
  ExecutorFailure executorFailure() default ExecutorFailure.DISALLOWED;

  enum ExecutorFailure {
    EXPECTED, DISALLOWED, IGNORED
  }

  ExecutorService cachedExecutorService = Executors.newCachedThreadPool(
      new ThreadFactoryBuilder().setDaemon(true).build());

  /** The executors that the cache can be configured with. */
  enum CacheExecutor {
    DEFAULT { // fork-join common pool
      @Override public Executor create() {
        // Use with caution as may be unpredictable during tests if awaiting completion
        return null;
      }
    },
    DIRECT {
      @Override public Executor create() {
        // Cache implementations must avoid deadlocks by incorrectly assuming async execution
        return new TrackingExecutor(MoreExecutors.newDirectExecutorService());
      }
    },
    THREADED {
      @Override public Executor create() {
        return new TrackingExecutor(cachedExecutorService);
      }
    },
    REJECTING {
      @Override public Executor create() {
        // Cache implementations must avoid corrupting internal state due to rejections
        return new ForkJoinPool() {
          @Override public void execute(Runnable task) {
            throw new RejectedExecutionException();
          }
        };
      }
    };

    public abstract Executor create();
  }

  /* ---------------- Populated -------------- */

  /**
   * The number of entries to populate the cache with. The keys and values are integers starting
   * from above the integer cache limit, with the value being the negated key. The cache will never
   * be populated to exceed the maximum size, if defined, thereby ensuring that no evictions occur
   * prior to the test. Each configuration results in a new combination.
   */
  Population[] population() default {
    Population.EMPTY,
    Population.SINGLETON,
    Population.PARTIAL,
    Population.FULL
  };

  /** The population scenarios. */
  enum Population {
    EMPTY(0),
    SINGLETON(1),
    PARTIAL(InitialCapacity.FULL.size() / 2),
    FULL(InitialCapacity.FULL.size());

    private final long size;

    private Population(long size) {
      this.size = size;
    }

    public long size() {
      return size;
    }
  }
}
