package org.cache2k.jcache.tests;

/*
 * #%L
 * cache2k JCache tests
 * %%
 * Copyright (C) 2000 - 2020 headissue GmbH, Munich
 * %%
 * 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.
 * #L%
 */

import org.jsr107.tck.event.CacheEntryListenerClient;
import org.jsr107.tck.event.CacheEntryListenerServer;
import org.jsr107.tck.testutil.CacheTestSupport;
import org.jsr107.tck.testutil.ExcludeListExcluder;
import static org.junit.Assert.*;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.MethodRule;

import javax.cache.configuration.FactoryBuilder;
import javax.cache.configuration.MutableCacheEntryListenerConfiguration;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.event.CacheEntryCreatedListener;
import javax.cache.event.CacheEntryEvent;
import javax.cache.event.CacheEntryExpiredListener;
import javax.cache.event.CacheEntryListenerException;
import javax.cache.event.CacheEntryRemovedListener;
import javax.cache.event.CacheEntryUpdatedListener;
import javax.cache.event.EventType;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeoutException;

/**
 * @author Jens Wilke
 */
public class AdditionalCacheListenerTest extends CacheTestSupport<Integer, String> {

  @Rule
  public MethodRule rule = new ExcludeListExcluder(this.getClass());

  private CacheEntryListenerServer cacheEntryListenerServer;
  private RecordingListener<Integer, String> listener;

  @Override
  protected MutableConfiguration<Integer, String> newMutableConfiguration() {
    return new MutableConfiguration<Integer, String>().setTypes(Integer.class, String.class);
  }

  @Override
  protected MutableConfiguration<Integer, String> extraSetup(MutableConfiguration<Integer, String> cfg) {
    // cfg.setExpiryPolicyFactory(ModifiedExpiryPolicy.factoryOf(new Duration(TimeUnit.MILLISECONDS, 5)));
    cacheEntryListenerServer = new CacheEntryListenerServer<>(10011, Integer.class, String.class);
    try {
      cacheEntryListenerServer.open();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    listener = new RecordingListener<>();
    cacheEntryListenerServer.addCacheEventListener(listener);
    CacheEntryListenerClient<Integer, String> clientListener =
      new CacheEntryListenerClient<>(cacheEntryListenerServer.getInetAddress(), cacheEntryListenerServer.getPort());
    boolean _isSynchronous = false;
    listenerConfiguration = new MutableCacheEntryListenerConfiguration<>(
      FactoryBuilder.factoryOf(clientListener), null, true, _isSynchronous);
    return cfg.addCacheEntryListenerConfiguration(listenerConfiguration);
  }

  /**
   * Test whether async events arrive and are in correct order. Also test with negative key.
   */
  @Test
  public void testInOrder() throws Exception {
    int KEY = -123;
    cache.put(KEY, "hello");
    cache.put(KEY, "mike");
    cache.remove(KEY);
    listener.await(3, 60 * 1000);
    assertEquals(
      Arrays.asList(EventType.CREATED, EventType.UPDATED, EventType.REMOVED),
      listener.extractLogForKey(KEY));
  }

  public static class RecordingListener<K, V> implements CacheEntryCreatedListener<K, V>,
    CacheEntryUpdatedListener<K, V>, CacheEntryExpiredListener<K, V>,
    CacheEntryRemovedListener<K, V> {

    List<RecordedEvent<K>> log = Collections.synchronizedList(new ArrayList<RecordedEvent<K>>());

    @Override
    public void onCreated(final Iterable<CacheEntryEvent<? extends K, ? extends V>> events) throws CacheEntryListenerException {
      record(EventType.CREATED, events);
    }

    @Override
    public void onExpired(final Iterable<CacheEntryEvent<? extends K, ? extends V>> events) throws CacheEntryListenerException {
      record(EventType.EXPIRED, events);
    }

    @Override
    public void onRemoved(final Iterable<CacheEntryEvent<? extends K, ? extends V>> events) throws CacheEntryListenerException {
      record(EventType.REMOVED, events);
    }

    @Override
    public void onUpdated(final Iterable<CacheEntryEvent<? extends K, ? extends V>> events) throws CacheEntryListenerException {
      record(EventType.UPDATED, events);
    }

    public List<EventType> extractLogForKey(K key) {
      List<EventType> l = new ArrayList<>();
      for (RecordedEvent<K> e : log) {
        if (e.key.equals(key)) {
          l.add(e.type);
        }
      }
      return l;
    }

    public void await(int _eventCount, long _timeoutMillis) throws TimeoutException {
      long t0 = System.currentTimeMillis();
      while (_eventCount != log.size()) {
        long t = System.currentTimeMillis();
        if (t - t0 >= _timeoutMillis) {
          throw new TimeoutException();
        }
        synchronized (log) {
          try {
            log.wait(_timeoutMillis - (t - t0));
          } catch (InterruptedException ex) {}
        }
      }
    }

    public void record(final EventType _expectedType, final Iterable<CacheEntryEvent<? extends K, ? extends V>> events) {
      for (CacheEntryEvent e0 : events) {
        CacheEntryEvent<K,V> e = e0;
        assertEquals(_expectedType, e.getEventType());
        log.add(new RecordedEvent<K>(e.getEventType(), e.getKey()));
      }
      synchronized (log) {
        log.notifyAll();
      }
    }

  }

  static class RecordedEvent<K> {

    EventType type;
    K key;

    public RecordedEvent(final EventType _type, final K _key) {
      type = _type;
      key = _key;
    }
  }

}