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

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeThat;

import java.util.Collection;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;

import org.hamcrest.Matcher;
import org.jctools.queues.QueueFactory;
import org.jctools.queues.atomic.AtomicQueueFactory;
import org.jctools.queues.spec.ConcurrentQueueSpec;
import org.jctools.queues.spec.Ordering;
import org.jctools.queues.spec.Preference;
import org.jctools.util.Pow2;
import org.junit.Before;
import org.junit.Test;

/**
 * @author nitsanw@yahoo.com (Nitsan Wakart)
 */
public abstract class QueueSanityTest {

  public static final int SIZE = 8192 * 2;

  private final Queue<Integer> queue;
  private final ConcurrentQueueSpec spec;

  public QueueSanityTest(ConcurrentQueueSpec spec, Queue<Integer> queue) {
    this.queue = queue;
    this.spec = spec;
  }

  @Before
  public void clear() {
    queue.clear();
  }

  @Test
  public void sanity() {
    for (int i = 0; i < SIZE; i++) {
      assertNull(queue.poll());
      assertThat(queue, emptyAndZeroSize());
    }
    int i = 0;
    while (i < SIZE && queue.offer(i)) {
      i++;
    }
    int size = i;
    assertEquals(size, queue.size());
    if (spec.ordering == Ordering.FIFO) {
      // expect FIFO
      i = 0;
      Integer p;
      Integer e;
      while ((p = queue.peek()) != null) {
        e = queue.poll();
        assertEquals(p, e);
        assertEquals(size - (i + 1), queue.size());
        assertEquals(i++, e.intValue());
      }
      assertEquals(size, i);
    } else {
      // expect sum of elements is (size - 1) * size / 2 = 0 + 1 + .... + (size - 1)
      int sum = (size - 1) * size / 2;
      i = 0;
      Integer e;
      while ((e = queue.poll()) != null) {
        assertEquals(--size, queue.size());
        sum -= e;
      }
      assertEquals(0, sum);
    }
    assertNull(queue.poll());
    assertThat(queue, emptyAndZeroSize());
  }

  @Test
  public void testSizeIsTheNumberOfOffers() {
    int currentSize = 0;
    while (currentSize < SIZE && queue.offer(currentSize)) {
      currentSize++;
      assertThat(queue, hasSize(currentSize));
    }
  }

  @Test
  public void whenFirstInThenFirstOut() {
    assumeThat(spec.ordering, is(Ordering.FIFO));

    // Arrange
    int i = 0;
    while (i < SIZE && queue.offer(i)) {
      i++;
    }
    final int size = queue.size();

    // Act
    i = 0;
    Integer prev;
    while ((prev = queue.peek()) != null) {
      final Integer item = queue.poll();

      assertThat(item, is(prev));
      assertThat(queue, hasSize(size - (i + 1)));
      assertThat(item, is(i));
      i++;
    }

    // Assert
    assertThat(i, is(size));
  }

  @Test(expected = NullPointerException.class)
  public void offerNullResultsInNPE() {
    queue.offer(null);
  }

  @Test
  public void whenOfferItemAndPollItemThenSameInstanceReturnedAndQueueIsEmpty() {
    assertThat(queue, emptyAndZeroSize());

    // Act
    final Integer e = 1876876;
    queue.offer(e);
    assertFalse(queue.isEmpty());
    assertEquals(1, queue.size());

    final Integer oh = queue.poll();
    assertEquals(e, oh);

    // Assert
    assertThat(oh, sameInstance(e));
    assertThat(queue, emptyAndZeroSize());
  }

  @Test
  public void testPowerOf2Capacity() {
    assumeThat(spec.isBounded(), is(true));
    int n = Pow2.roundToPowerOfTwo(spec.capacity);

    for (int i = 0; i < n; i++) {
      assertTrue("Failed to insert:" + i, queue.offer(i));
    }
    assertFalse(queue.offer(n));
  }

  static final class Val {
    public int value;
  }

  @Test
  @SuppressWarnings({"rawtypes", "unchecked"})
  public void testHappensBefore() throws Exception {
    final AtomicBoolean stop = new AtomicBoolean();
    final Queue q = queue;
    final Val fail = new Val();
    Thread t1 = new Thread(new Runnable() {
      @Override public void run() {
        while (!stop.get()) {
          for (int i = 1; i <= 10; i++) {
            Val v = new Val();
            v.value = i;
            q.offer(v);
          }
          // slow down the producer, this will make the queue mostly empty encouraging visibility
          // issues.
          Thread.yield();
        }
      }
    });
    Thread t2 = new Thread(new Runnable() {
      @Override
      public void run() {
        while (!stop.get()) {
          for (int i = 0; i < 10; i++) {
            Val v = (Val) q.peek();
            if (v != null && v.value == 0) {
              fail.value = 1;
              stop.set(true);
            }
            q.poll();
          }
        }
      }
    });

    t1.start();
    t2.start();
    Thread.sleep(1000);
    stop.set(true);
    t1.join();
    t2.join();
    assertEquals("reordering detected", 0, fail.value);

  }

  @Test
  public void testSize() throws Exception {
    final AtomicBoolean stop = new AtomicBoolean();
    final Queue<Integer> q = queue;
    final Val fail = new Val();
    Thread t1 = new Thread(new Runnable() {
      @Override
      public void run() {
        while (!stop.get()) {
          q.offer(1);
          q.poll();
        }
      }
    });
    Thread t2 = new Thread(new Runnable() {
      @Override
      public void run() {
        while (!stop.get()) {
          int size = q.size();
          if (size != 0 && size != 1) {
            fail.value++;
          }
        }
      }
    });

    t1.start();
    t2.start();
    Thread.sleep(1000);
    stop.set(true);
    t1.join();
    t2.join();
    assertEquals("Unexpected size observed", 0, fail.value);

  }

  @Test
  public void testPollAfterIsEmpty() throws Exception {
    final AtomicBoolean stop = new AtomicBoolean();
    final Queue<Integer> q = queue;
    final Val fail = new Val();
    Thread t1 = new Thread(new Runnable() {
      @Override
      public void run() {
        while (!stop.get()) {
          q.offer(1);
          // slow down the producer, this will make the queue mostly empty encouraging visibility
          // issues.
          Thread.yield();
        }
      }
    });
    Thread t2 = new Thread(new Runnable() {
      @Override
      public void run() {
        while (!stop.get()) {
          if (!q.isEmpty() && q.poll() == null) {
            fail.value++;
          }
        }
      }
    });

    t1.start();
    t2.start();
    Thread.sleep(1000);
    stop.set(true);
    t1.join();
    t2.join();
    assertEquals("Observed no element in non-empty queue", 0, fail.value);

  }

  public static Object[] makeQueue(int producers, int consumers, int capacity, Ordering ordering,
      Queue<Integer> q) {
    ConcurrentQueueSpec spec =
        new ConcurrentQueueSpec(producers, consumers, capacity, ordering, Preference.NONE);
    if (q == null) {
      q = QueueFactory.newQueue(spec);
    }
    return new Object[] {spec, q};
  }

  public static Object[] makeAtomic(int producers, int consumers, int capacity, Ordering ordering,
      Queue<Integer> q) {
    ConcurrentQueueSpec spec =
        new ConcurrentQueueSpec(producers, consumers, capacity, ordering, Preference.NONE);
    if (q == null) {
      q = AtomicQueueFactory.newQueue(spec);
    }
    return new Object[] {spec, q};
  }

  public static Matcher<Collection<?>> emptyAndZeroSize() {
    return allOf(hasSize(0), empty());
  }
}
