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

import static com.github.benmanes.caffeine.IsValidSingleConsumerQueue.validate;
import static com.github.benmanes.caffeine.testing.IsEmptyIterable.deeplyEmpty;
import static com.google.common.collect.Iterators.elementsEqual;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.emptyArray;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.hasToString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;

import org.testng.IInvokedMethod;
import org.testng.IInvokedMethodListener;
import org.testng.ITestResult;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;

import com.github.benmanes.caffeine.SingleConsumerQueue.LinearizableNode;
import com.github.benmanes.caffeine.SingleConsumerQueueTest.ValidatingQueueListener;
import com.github.benmanes.caffeine.testing.Awaits;
import com.github.benmanes.caffeine.testing.ConcurrentTestHarness;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.testing.SerializableTester;

/**
 * @author ben.manes@gmail.com (Ben Manes)
 */
@Listeners(ValidatingQueueListener.class)
public class SingleConsumerQueueTest {
  private static final int PRODUCE = 10_000;
  private static final int NUM_PRODUCERS = 10;
  private static final int POPULATED_SIZE = 10;

  @Test(dataProvider = "empty")
  public void clear_whenEmpty(Queue<?> queue) {
    queue.clear();
    assertThat(queue, is(deeplyEmpty()));
  }

  @Test(dataProvider = "populated")
  public void clear_whenPopulated(Queue<?> queue) {
    queue.clear();
    assertThat(queue, is(deeplyEmpty()));
  }

  @Test(dataProvider = "empty")
  public void isEmpty_whenEmpty(Queue<?> queue) {
    assertThat(queue.isEmpty(), is(true));
  }

  @Test(dataProvider = "populated")
  public void isEmpty_whenPopulated(Queue<?> queue) {
    assertThat(queue.isEmpty(), is(false));
  }

  @Test(dataProvider = "empty")
  public void size_whenEmpty(Queue<?> queue) {
    assertThat(queue.size(), is(0));
    assertThat(queue.size(), is(equalTo(Iterables.size(queue))));
  }

  @Test(dataProvider = "populated")
  public void size_whenPopulated(Queue<?> queue) {
    assertThat(queue.size(), is(POPULATED_SIZE));
    assertThat(Iterables.size(queue), is(POPULATED_SIZE));
    assertThat(queue.size(), is(equalTo(Iterables.size(queue))));
  }

  /* ---------------- Contains -------------- */

  @Test(dataProvider = "empty")
  public void contains_withNull(Queue<?> queue) {
    assertThat(queue.contains(null), is(false));
  }

  @Test(dataProvider = "populated")
  public void contains_whenFound(Queue<Integer> queue) {
    assertThat(queue.contains(Iterables.get(queue, POPULATED_SIZE / 2)), is(true));
  }

  @Test(dataProvider = "populated")
  public void contains_whenNotFound(Queue<Integer> queue) {
    assertThat(queue.contains(-1), is(false));
  }

  @Test(dataProvider = "empty", expectedExceptions = NullPointerException.class)
  public void containsAll_withNull(Queue<?> queue) {
    queue.containsAll(null);
  }

  @Test(dataProvider = "populated")
  @SuppressWarnings("ModifyingCollectionWithItself")
  public void containsAll_whenFound(Queue<Integer> queue) {
    assertThat(queue.containsAll(
        ImmutableList.of(0, POPULATED_SIZE / 2, POPULATED_SIZE - 1)), is(true));
    assertThat(queue.containsAll(queue), is(true));
  }

  @Test(dataProvider = "populated")
  public void containsAll_whenNotFound(Queue<Integer> queue) {
    assertThat(queue.containsAll(
        ImmutableList.of(-1, -(POPULATED_SIZE / 2), -POPULATED_SIZE)), is(false));
  }

  /* ---------------- Peek -------------- */

  @Test(dataProvider = "empty")
  public void peek_whenEmpty(Queue<Integer> queue) {
    assertThat(queue.peek(), is(nullValue()));
  }

  @Test(dataProvider = "populated")
  public void peek_whenPopulated(SingleConsumerQueue<Integer> queue) {
    Integer first = queue.head.next.value;
    assertThat(queue.peek(), is(first));
    assertThat(queue, hasSize(POPULATED_SIZE));
    assertThat(queue.contains(first), is(true));
  }

  /* ---------------- Element -------------- */

  @Test(dataProvider = "empty", expectedExceptions = NoSuchElementException.class)
  public void element_whenEmpty(Queue<Integer> queue) {
    queue.element();
  }

  @Test(dataProvider = "populated")
  public void element_whenPopulated(SingleConsumerQueue<Integer> queue) {
    Integer first = queue.head.next.value;
    assertThat(queue.element(), is(first));
    assertThat(queue, hasSize(POPULATED_SIZE));
    assertThat(queue.contains(first), is(true));
  }

  /* ---------------- Offer -------------- */

  @Test(dataProvider = "empty")
  public void offer_whenEmpty(Queue<Integer> queue) {
    assertThat(queue.offer(1), is(true));
    assertThat(queue, hasSize(1));
  }

  @Test(dataProvider = "populated")
  public void offer_whenPopulated(Queue<Integer> queue) {
    assertThat(queue.offer(1), is(true));
    assertThat(queue, hasSize(POPULATED_SIZE + 1));
  }

  /* ---------------- Add -------------- */

  @Test(dataProvider = "empty")
  public void add_whenEmpty(Queue<Integer> queue) {
    assertThat(queue.add(1), is(true));
    assertThat(queue.peek(), is(1));
    assertThat(Iterables.getLast(queue), is(1));
    assertThat(queue, hasSize(1));
    assertThat(queue.size(), is(equalTo(Iterables.size(queue))));
  }

  @Test(dataProvider = "populated")
  public void add_whenPopulated(Queue<Integer> queue) {
    assertThat(queue.add(-1), is(true));
    assertThat(queue.peek(), is(not(-1)));
    assertThat(Iterables.getLast(queue), is(-1));
    assertThat(queue, hasSize(POPULATED_SIZE + 1));
    assertThat(queue.size(), is(equalTo(Iterables.size(queue))));
  }

  @Test(dataProvider = "empty")
  public void addAll_whenEmpty(Queue<Integer> queue) {
    List<Integer> list = new ArrayList<>();
    populate(list, POPULATED_SIZE);

    assertThat(queue.addAll(list), is(true));
    assertThat(queue.peek(), is(0));
    assertThat(Iterables.getLast(queue), is(POPULATED_SIZE - 1));
    assertThat(String.format("%nExpected: %s%n     but: %s", queue, list),
      elementsEqual(queue.iterator(), list.iterator()));
  }

  @Test(dataProvider = "singleton,populated")
  public void addAll_whenPopulated(Queue<Integer> queue) {
    List<Integer> list = ImmutableList.of(POPULATED_SIZE, POPULATED_SIZE + 1, POPULATED_SIZE + 2);
    List<Integer> expect = ImmutableList.copyOf(Iterables.concat(queue, list));

    assertThat(queue.addAll(list), is(true));
    assertThat(queue.peek(), is(0));
    assertThat(Iterables.getLast(queue), is(POPULATED_SIZE + 2));
    assertThat(String.format("%nExpected: %s%n     but: %s", queue, expect),
        elementsEqual(queue.iterator(), expect.iterator()));
  }

  /* ---------------- Poll -------------- */

  @Test(dataProvider = "empty")
  public void poll_whenEmpty(Queue<Integer> queue) {
    assertThat(queue.poll(), is(nullValue()));
  }

  @Test(dataProvider = "populated")
  public void poll_whenPopulated(Queue<Integer> queue) {
    Integer first = queue.peek();
    assertThat(queue.poll(), is(first));
    assertThat(queue, hasSize(POPULATED_SIZE - 1));
    assertThat(queue.contains(first), is(false));
  }

  @Test(dataProvider = "populated")
  public void poll_toEmpty(Queue<Integer> queue) {
    Integer value;
    while ((value = queue.poll()) != null) {
      assertThat(queue.contains(value), is(false));
    }
    assertThat(queue, is(deeplyEmpty()));
  }

  /* ---------------- Remove -------------- */

  @Test(dataProvider = "empty", expectedExceptions = NoSuchElementException.class)
  public void remove_whenEmpty(Queue<Integer> queue) {
    queue.remove();
  }

  @Test(dataProvider = "populated")
  public void remove_whenPopulated(Queue<Integer> queue) {
    Integer first = queue.peek();
    assertThat(queue.remove(), is(first));
    assertThat(queue, hasSize(POPULATED_SIZE - 1));
    assertThat(queue.contains(first), is(false));
  }

  @Test(dataProvider = "populated")
  public void remove_toEmpty(Queue<Integer> queue) {
    while (!queue.isEmpty()) {
      Integer value = queue.remove();
      assertThat(queue.contains(value), is(false));
    }
    assertThat(queue, is(deeplyEmpty()));
  }

  @Test(dataProvider = "empty,singleton,populated")
  public void removeElement_notFound(Queue<Integer> queue) {
    assertThat(queue.remove(-1), is(false));
  }

  @Test(dataProvider = "populated")
  public void removeElement_whenFound(Queue<Integer> queue) {
    Integer first = queue.peek();
    assertThat(queue.remove(first), is(true));
    assertThat(queue, hasSize(POPULATED_SIZE - 1));
    assertThat(queue.contains(first), is(false));
  }

  @Test(dataProvider = "populated")
  public void removeElement_toEmpty(Queue<Integer> queue) {
    while (!queue.isEmpty()) {
      Integer value = queue.peek();
      assertThat(queue.remove(value), is(true));
      assertThat(queue.contains(value), is(false));
    }
    assertThat(queue, is(deeplyEmpty()));
  }

  @Test(dataProvider = "empty")
  public void removeAll_withEmpty(Queue<Integer> queue) {
    assertThat(queue.removeAll(ImmutableList.of()), is(false));
    assertThat(queue, is(deeplyEmpty()));
  }

  @Test(dataProvider = "populated")
  public void removeAll_withPopulated(Queue<Integer> queue) {
    Integer first = queue.peek();
    assertThat(queue.removeAll(ImmutableList.of(first)), is(true));
    assertThat(queue, hasSize(POPULATED_SIZE - 1));
    assertThat(queue.contains(first), is(false));
  }

  @Test(dataProvider = "populated")
  public void removeAll_toEmpty(Queue<Integer> queue) {
    assertThat(queue.removeAll(ImmutableList.copyOf(queue)), is(true));
    assertThat(queue, is(deeplyEmpty()));
  }

  /* ---------------- Retain -------------- */

  @Test(dataProvider = "empty")
  public void retainAll_withEmpty(Queue<Integer> queue) {
    assertThat(queue.retainAll(ImmutableList.of()), is(false));
    assertThat(queue, is(deeplyEmpty()));
  }

  @Test(dataProvider = "populated")
  public void retainAll_withPopulated(Queue<Integer> queue) {
    Integer first = queue.peek();
    assertThat(queue.retainAll(ImmutableList.of(first)), is(true));
    assertThat(queue, hasSize(1));
    assertThat(queue.contains(first), is(true));
  }

  @Test(dataProvider = "populated")
  public void retainAll_toEmpty(Queue<Integer> queue) {
    assertThat(queue.retainAll(ImmutableList.of()), is(true));
    assertThat(queue, is(deeplyEmpty()));
  }

  /* ---------------- Iterators -------------- */

  @Test(dataProvider = "empty", expectedExceptions = NoSuchElementException.class)
  public void iterator_noMoreElements(Queue<Integer> queue) {
    queue.iterator().next();
  }

  @Test(dataProvider = "empty")
  public void iterator_whenEmpty(Queue<Integer> queue) {
    assertThat(queue.iterator().hasNext(), is(false));
  }

  @Test(dataProvider = "singleton,populated")
  public void iterator_whenPopulated(Queue<Integer> queue) {
    List<Integer> copy = new ArrayList<>();
    populate(copy, queue.size());
    assertThat(String.format("%nExpected: %s%n     but: %s", queue, copy),
        elementsEqual(queue.iterator(), copy.iterator()));
  }

  @Test(dataProvider = "populated", expectedExceptions = IllegalStateException.class)
  public void iterator_removal_unread(Queue<Integer> queue) {
    queue.iterator().remove();
  }

  @Test(dataProvider = "populated", expectedExceptions = IllegalStateException.class)
  public void iterator_removal_duplicate(Queue<Integer> queue) {
    Iterator<Integer> it = queue.iterator();
    it.next();
    it.remove();
    it.remove();
  }

  @Test(dataProvider = "populated")
  public void iterator_removal(Queue<Integer> queue) {
    Iterator<Integer> it = queue.iterator();
    it.next();
    it.remove();
  }

  @Test(dataProvider = "populated")
  public void iterator_removal_toEmpty(Queue<Integer> queue) {
    for (Iterator<Integer> it = queue.iterator(); it.hasNext();) {
      it.next();
      it.remove();
    }
    assertThat(queue, is(deeplyEmpty()));
  }

  /* ---------------- toArray -------------- */

  @Test(dataProvider = "empty,singleton,populated")
  public void toArray(Queue<Integer> queue) {
    Object[] expect = new ArrayList<>(queue).toArray();
    Object[] actual = queue.toArray();
    assertThat(actual, queue.isEmpty() ? emptyArray() : arrayContaining(expect));
  }

  @Test(dataProvider = "empty,singleton,populated")
  public void toTypedArray(Queue<Integer> queue) {
    Integer[] expect = new ArrayList<>(queue).toArray(new Integer[] {});
    Integer[] actual = queue.toArray(new Integer[] {});
    assertThat(actual, queue.isEmpty() ? emptyArray() : arrayContaining(expect));
  }

  /* ---------------- toString -------------- */

  @Test(dataProvider = "empty,singleton,populated")
  public void toString(Queue<Integer> queue) {
    List<Integer> list = new ArrayList<>();
    populate(list, queue.size());
    assertThat(queue, hasToString(list.toString()));
  }

  /* ---------------- Serialization -------------- */

  @Test(dataProvider = "empty,singleton,populated")
  public void serializable(Queue<Integer> queue) {
    Queue<Integer> copy = SerializableTester.reserialize(queue);
    assertThat(String.format("%nExpected: %s%n     but: %s", queue, copy),
        elementsEqual(queue.iterator(), copy.iterator()));
  }

  /* ---------------- Concurrency -------------- */

  @Test(dataProvider = "empty")
  public void oneProducer_oneConsumer(Queue<Integer> queue) {
    AtomicInteger started = new AtomicInteger();
    AtomicInteger finished = new AtomicInteger();

    ConcurrentTestHarness.execute(() -> {
      started.incrementAndGet();
      Awaits.await().untilAtomic(started, is(2));
      for (int i = 0; i < PRODUCE; i++) {
        queue.add(i);
      }
      finished.incrementAndGet();
    });
    ConcurrentTestHarness.execute(() -> {
      started.incrementAndGet();
      Awaits.await().untilAtomic(started, is(2));
      for (int i = 0; i < PRODUCE; i++) {
        while (queue.poll() == null) {}
      }
      finished.incrementAndGet();
    });

    Awaits.await().untilAtomic(finished, is(2));
    assertThat(queue, is(deeplyEmpty()));
  }

  @Test(dataProvider = "empty")
  public void manyProducers_noConsumer(Queue<Integer> queue) {
    ConcurrentTestHarness.timeTasks(NUM_PRODUCERS, () -> {
      for (int i = 0; i < PRODUCE; i++) {
        queue.add(i);
      }
    });
    assertThat(queue, hasSize(NUM_PRODUCERS * PRODUCE));
    assertThat(queue.size(), is(equalTo(Iterables.size(queue))));
  }

  @Test(dataProvider = "empty")
  public void manyProducers_oneConsumer(Queue<Integer> queue) {
    AtomicInteger started = new AtomicInteger();
    AtomicInteger finished = new AtomicInteger();

    ConcurrentTestHarness.execute(() -> {
      started.incrementAndGet();
      Awaits.await().untilAtomic(started, is(NUM_PRODUCERS + 1));
      for (int i = 0; i < (NUM_PRODUCERS * PRODUCE); i++) {
        while (queue.poll() == null) {}
      }
      finished.incrementAndGet();
    });

    ConcurrentTestHarness.timeTasks(NUM_PRODUCERS, () -> {
      started.incrementAndGet();
      Awaits.await().untilAtomic(started, is(NUM_PRODUCERS + 1));
      for (int i = 0; i < PRODUCE; i++) {
        queue.add(i);
      }
      finished.incrementAndGet();
    });

    Awaits.await().untilAtomic(finished, is(NUM_PRODUCERS + 1));
    assertThat(queue, is(deeplyEmpty()));
  }

  /* ---------------- Queue providers -------------- */

  @DataProvider(name = "empty")
  public Object[][] providesEmpty() {
    return new Object[][] {{ makePopulated(0, true) }, { makePopulated(0, false) }};
  }

  @DataProvider(name = "singleton")
  public Object[][] providesSingleton() {
    return new Object[][] {{ makePopulated(1, true) }, { makePopulated(1, false) }};
  }

  @DataProvider(name = "populated")
  public Object[][] providesPopulated() {
    return new Object[][] {
        { makePopulated(POPULATED_SIZE, true) },
        { makePopulated(POPULATED_SIZE, false) }};
  }

  @DataProvider(name = "singleton,populated")
  public Object[][] providesSingletonAndPopulated() {
    return new Object[][] {
        { makePopulated(1, true) }, { makePopulated(1, false) },
        { makePopulated(POPULATED_SIZE, true) }, { makePopulated(POPULATED_SIZE, false) }};
  }

  @DataProvider(name = "empty,singleton,populated")
  public Object[][] providesEmptyAndSingletonAndPopulated() {
    return new Object[][] {
        { makePopulated(0, true) }, { makePopulated(0, false) },
        { makePopulated(1, true) }, { makePopulated(1, false) },
        { makePopulated(POPULATED_SIZE, true) }, { makePopulated(POPULATED_SIZE, false) }};
  }

  static SingleConsumerQueue<Integer> makePopulated(int size, boolean optimistic) {
    SingleConsumerQueue<Integer> queue = optimistic
        ? SingleConsumerQueue.optimistic()
        : SingleConsumerQueue.linearizable();
    populate(queue, size);
    return queue;
  }

  static void populate(Collection<Integer> collection, int start) {
    for (int i = 0; i < start; i++) {
      collection.add(i);
    }
  }

  /** A listener that validates the internal structure after a successful test execution. */
  public static final class ValidatingQueueListener implements IInvokedMethodListener {
    @Override
    public void beforeInvocation(IInvokedMethod method, ITestResult testResult) {}

    @Override
    public void afterInvocation(IInvokedMethod method, ITestResult testResult) {
      try {
        if (testResult.isSuccess()) {
          for (Object param : testResult.getParameters()) {
            if (param instanceof SingleConsumerQueue<?>) {
              assertThat((SingleConsumerQueue<?>) param, is(validate()));
            }
          }
        }
      } catch (AssertionError caught) {
        testResult.setStatus(ITestResult.FAILURE);
        testResult.setThrowable(caught);
      } finally {
        cleanUp(testResult);
      }
    }
  }

  /** Free memory by clearing unused resources after test execution. */
  static void cleanUp(ITestResult testResult) {
    Object[] params = testResult.getParameters();
    for (int i = 0; i < params.length; i++) {
      Object param = params[i];
      if ((param instanceof SingleConsumerQueue<?>)) {
        boolean linearizable =
            (((SingleConsumerQueue<?>) param).factory.apply(null) instanceof LinearizableNode<?>);
        params[i] = param.getClass().getSimpleName() + "_"
            + (linearizable ? "linearizable" : "optimistic");
      } else {
        params[i] = Objects.toString(param);
      }
    }
  }
}
