/*
 * Copyright 2015 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.event;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;

import javax.cache.Cache;
import javax.cache.configuration.MutableCacheEntryListenerConfiguration;
import javax.cache.event.CacheEntryCreatedListener;
import javax.cache.event.CacheEntryEventFilter;
import javax.cache.event.CacheEntryExpiredListener;
import javax.cache.event.CacheEntryListener;
import javax.cache.event.CacheEntryRemovedListener;
import javax.cache.event.CacheEntryUpdatedListener;

import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.google.common.util.concurrent.MoreExecutors;

/**
 * @author ben.manes@gmail.com (Ben Manes)
 */
public final class EventDispatcherTest {
  @Mock Cache<Integer, Integer> cache;
  @Mock CacheEntryCreatedListener<Integer, Integer> createdListener;
  @Mock CacheEntryUpdatedListener<Integer, Integer> updatedListener;
  @Mock CacheEntryRemovedListener<Integer, Integer> removedListener;
  @Mock CacheEntryExpiredListener<Integer, Integer> expiredListener;

  CacheEntryEventFilter<Integer, Integer> allowFilter = event -> true;
  CacheEntryEventFilter<Integer, Integer> rejectFilter = event -> false;
  EventDispatcher<Integer, Integer> dispatcher;

  @BeforeMethod
  public void before() {
    MockitoAnnotations.initMocks(this);
    dispatcher = new EventDispatcher<>(MoreExecutors.directExecutor());
  }

  @AfterMethod
  public void after() {
    dispatcher.ignoreSynchronous();
  }

  @Test
  public void register_noListener() {
    MutableCacheEntryListenerConfiguration<Integer, Integer> configuration =
        new MutableCacheEntryListenerConfiguration<>(null, null, false, false);
    dispatcher.register(configuration);
    assertThat(dispatcher.dispatchQueues.keySet(), is(empty()));
  }

  @Test
  public void deregister() {
    MutableCacheEntryListenerConfiguration<Integer, Integer> configuration =
        new MutableCacheEntryListenerConfiguration<>(() -> createdListener, null, false, false);
    dispatcher.register(configuration);
    dispatcher.deregister(configuration);
    assertThat(dispatcher.dispatchQueues.keySet(), is(empty()));
  }

  @Test
  public void publishCreated() {
    registerAll();
    dispatcher.publishCreated(cache, 1, 2);
    verify(createdListener, times(4)).onCreated(any());
    assertThat(EventDispatcher.pending.get(), hasSize(2));
  }

  @Test
  public void publishUpdated() {
    registerAll();
    dispatcher.publishUpdated(cache, 1, 2, 3);
    verify(updatedListener, times(4)).onUpdated(any());
    assertThat(EventDispatcher.pending.get(), hasSize(2));
  }

  @Test
  public void publishRemoved() {
    registerAll();
    dispatcher.publishRemoved(cache, 1, 2);
    verify(removedListener, times(4)).onRemoved(any());
    assertThat(EventDispatcher.pending.get(), hasSize(2));
  }

  @Test
  public void publishExpired() {
    registerAll();
    dispatcher.publishExpired(cache, 1, 2);
    verify(expiredListener, times(4)).onExpired(any());
    assertThat(EventDispatcher.pending.get(), hasSize(2));
  }

  @Test(threadPoolSize = 5, invocationCount = 25)
  public void concurrent() {
    dispatcher.publishCreated(cache, 1, 2);
    dispatcher.publishUpdated(cache, 1, 2, 3);
    dispatcher.publishRemoved(cache, 1, 2);
    dispatcher.publishExpired(cache, 1, 2);
  }

  @Test
  public void awaitSynchronous() {
    EventDispatcher.pending.get().add(CompletableFuture.completedFuture(null));
    dispatcher.awaitSynchronous();
    assertThat(EventDispatcher.pending.get(), is(empty()));
  }

  @Test
  public void awaitSynchronous_failure() {
    CompletableFuture<Void> future = new CompletableFuture<>();
    future.completeExceptionally(new RuntimeException());
    EventDispatcher.pending.get().add(future);
    dispatcher.awaitSynchronous();
    assertThat(EventDispatcher.pending.get(), is(empty()));
  }

  @Test
  public void ignoreSynchronous() {
    EventDispatcher.pending.get().add(CompletableFuture.completedFuture(null));
    dispatcher.ignoreSynchronous();
    assertThat(EventDispatcher.pending.get(), is(empty()));
  }

  /**
   * Registers (4 listeners) * (2 synchronous modes) * (3 filter modes) = 24 configurations. For
   * simplicity, an event is published and ignored if the listener is of the wrong type. For a
   * single event, it should be consumed by (2 filter) * (2 synchronous) = 4 listeners where only
   * 2 are synchronous.
   */
  private void registerAll() {
    List<CacheEntryListener<Integer, Integer>> listeners = Arrays.asList(
        createdListener, updatedListener, removedListener, expiredListener);
    for (CacheEntryListener<Integer, Integer> listener : listeners) {
      for (boolean synchronous : Arrays.asList(true, false)) {
        dispatcher.register(new MutableCacheEntryListenerConfiguration<>(
            () -> listener, null, false, synchronous));
        dispatcher.register(new MutableCacheEntryListenerConfiguration<>(
            () -> listener, () -> allowFilter, false, synchronous));
        dispatcher.register(new MutableCacheEntryListenerConfiguration<>(
            () -> listener, () -> rejectFilter, false, synchronous));
      }
    }
  }
}
