/*
 * 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 com.google.common.base.Preconditions.checkNotNull;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;

import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;

import javax.annotation.Nullable;

import org.hamcrest.Description;
import org.hamcrest.TypeSafeDiagnosingMatcher;

import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.testing.CacheSpec.Listener;
import com.github.benmanes.caffeine.cache.testing.RemovalListeners.ConsumingRemovalListener;
import com.github.benmanes.caffeine.testing.DescriptionBuilder;

/**
 * A matcher that evaluates the {@link ConsumingRemovalListener} recorded all of the removal
 * notifications.
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
public final class HasRemovalNotifications<K, V> extends TypeSafeDiagnosingMatcher<Object> {
  private final int count;
  private final RemovalCause cause;
  private final CacheContext context;

  public HasRemovalNotifications(CacheContext context, int count, @Nullable RemovalCause cause) {
    this.context = checkNotNull(context);
    this.cause = cause;
    this.count = count;
  }

  @Override
  public void describeTo(Description description) {
    description.appendText("consumed");
  }

  @Override
  protected boolean matchesSafely(Object ignored, Description description) {
    DescriptionBuilder desc = new DescriptionBuilder(description);

    if (context.removalListenerType == Listener.CONSUMING) {
      List<RemovalNotification<Integer, Integer>> notifications = context.consumedNotifications();
      ForkJoinPool.commonPool().awaitQuiescence(10, TimeUnit.SECONDS);

      int size = 0;
      for (RemovalNotification<?, ?> notification : notifications) {
        if (notification.getCause() == cause) {
          checkNotification(notification);
          size++;
        }
      }
      desc.expectThat("notification count", size, is(count));
    }

    return desc.matches();
  }

  private void checkNotification(RemovalNotification<?, ?> notification) {
    switch (notification.getCause()) {
      case EXPLICIT: case REPLACED:
        assertThat(notification.wasEvicted(), is(false));
        assertThat(notification.getKey(), is(not(nullValue())));
        assertThat(notification.getValue(), is(not(nullValue())));
        break;
      case EXPIRED: case SIZE:
        assertThat(notification.wasEvicted(), is(true));
        assertThat(notification.getKey(), is(not(nullValue())));
        assertThat(notification.getValue(), is(not(nullValue())));
        break;
      case COLLECTED:
        assertThat(notification.wasEvicted(), is(true));
        if (context.isStrongKeys()) {
          assertThat(notification.getKey(), is(not(nullValue())));
        }
        if (context.isStrongValues()) {
          assertThat(notification.getValue(), is(not(nullValue())));
        }
    }
    if (cause != null) {
      assertThat("Wrong removal cause for " + notification, notification.getCause(), is(cause));
    }
  }

  public static <K, V> HasRemovalNotifications<K, V> hasRemovalNotifications(
      CacheContext context, long count, RemovalCause cause) {
    return new HasRemovalNotifications<>(context, (int) count, cause);
  }

  public static <K, V> HasRemovalNotifications<K, V> hasRemovalNotifications(
      CacheContext context, int count, RemovalCause cause) {
    return new HasRemovalNotifications<>(context, count, cause);
  }
}
