/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 *
 *   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 org.apache.streams.local.queues;

import org.apache.streams.util.ComponentUtils;

import com.carrotsearch.randomizedtesting.RandomizedTest;
import com.carrotsearch.randomizedtesting.annotations.Repeat;
import org.joda.time.DateTime;
import org.junit.After;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.management.ManagementFactory;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javax.management.InstanceNotFoundException;
import javax.management.ObjectName;

import org.junit.Assert;




/**
 * MultiThread unit tests for {@link org.apache.streams.local.queues.ThroughputQueue}
 */
public class ThroughputQueueMultiThreadTest extends RandomizedTest {

  private static final Logger LOGGER = LoggerFactory.getLogger(ThroughputQueueMultiThreadTest.class);
  private static final String MBEAN_ID = "testQueue";
  private static final String STREAM_ID = "test_stream";
  private static long STREAM_START_TIME = (new DateTime()).getMillis();

  /**
   * Remove registered mbeans from previous tests
   * @throws Exception
   */
  @After
  public void unregisterMXBean() throws Exception {
    try {
      ManagementFactory.getPlatformMBeanServer().unregisterMBean(new ObjectName(String.format(ThroughputQueue.NAME_TEMPLATE, MBEAN_ID, STREAM_ID, STREAM_START_TIME)));
    } catch (InstanceNotFoundException ife) {
      //No-op
    }
  }

  @After
  public void removeLocalMBeans() {
    try {
      ComponentUtils.removeAllMBeansOfDomain("org.apache.streams.local");
    } catch (Exception e) {
      //No op.  proceed to next test
    }
  }


  /**
   * Test that queue will block on puts when the queue is full
   * @throws InterruptedException
   */
  @Test
  public void testBlockOnFullQueue() throws InterruptedException {
    int queueSize = randomIntBetween(1, 3000);
    ExecutorService executor = Executors.newSingleThreadExecutor();
    CountDownLatch full = new CountDownLatch(1);
    CountDownLatch finished = new CountDownLatch(1);
    ThroughputQueue queue = new ThroughputQueue(queueSize);
    BlocksOnFullQueue testThread = new BlocksOnFullQueue(full, finished, queue, queueSize);
    executor.submit(testThread);
    full.await();
    Assert.assertEquals(queueSize, queue.size());
    Assert.assertEquals(queueSize, queue.getCurrentSize());
    Assert.assertFalse(testThread.isComplete()); //test that it is blocked
    safeSleep(1000);
    Assert.assertFalse(testThread.isComplete()); //still blocked
    queue.take();
    finished.await();
    Assert.assertEquals(queueSize, queue.size());
    Assert.assertEquals(queueSize, queue.getCurrentSize());
    Assert.assertTrue(testThread.isComplete());
    executor.shutdownNow();
    executor.awaitTermination(500, TimeUnit.MILLISECONDS);
  }

  /**
   * Test that queue will block on Take when queue is empty
   * @throws InterruptedException
   */
  @Test
  public void testBlockOnEmptyQueue() throws InterruptedException {
    int queueSize = randomIntBetween(1, 3000);
    ExecutorService executor = Executors.newSingleThreadExecutor();
    CountDownLatch empty = new CountDownLatch(1);
    CountDownLatch finished = new CountDownLatch(1);
    ThroughputQueue queue = new ThroughputQueue();
    BlocksOnEmptyQueue testThread = new BlocksOnEmptyQueue(empty, finished, queueSize, queue);
    for(int i=0; i < queueSize; ++i) {
      queue.put(i);
    }
    executor.submit(testThread);
    empty.await();
    Assert.assertEquals(0, queue.size());
    Assert.assertEquals(0, queue.getCurrentSize());
    Assert.assertFalse(testThread.isComplete());
    safeSleep(1000);
    Assert.assertFalse(testThread.isComplete());
    queue.put(1);
    finished.await();
    Assert.assertEquals(0, queue.size());
    Assert.assertEquals(0, queue.getCurrentSize());
    Assert.assertTrue(testThread.isComplete());
    executor.shutdownNow();
    executor.awaitTermination(500, TimeUnit.MILLISECONDS);
  }


  /**
   * Test multiple threads putting and taking from the queue while
   * this thread repeatedly calls the MXBean measurement methods.
   * Should hammer the queue with request from multiple threads
   * of all request types.  Purpose is to expose current modification exceptions
   * and/or dead locks.
   */
  @Test
  @Repeat(iterations = 3)
  public void testMultiThreadAccessAndInteruptResponse() throws Exception {
    int putTakeThreadCount = randomIntBetween(1, 10);
    int dataCount = randomIntBetween(1, 2000000);
    int pollCount = randomIntBetween(1, 2000000);
    int maxSize = randomIntBetween(1, 1000);
    CountDownLatch finished = new CountDownLatch(putTakeThreadCount);
    ThroughputQueue queue = new ThroughputQueue(maxSize, MBEAN_ID);
    ExecutorService executor = Executors.newFixedThreadPool(putTakeThreadCount * 2);
    for(int i=0; i < putTakeThreadCount; ++i) {
      executor.submit(new PutData(finished, queue, dataCount));
      executor.submit(new TakeData(queue));
    }
    for(int i=0; i < pollCount; ++i) {
      queue.getAvgWait();
      queue.getAdded();
      queue.getCurrentSize();
      queue.getMaxWait();
      queue.getRemoved();
      queue.getThroughput();
    }
    finished.await();
    while(!queue.isEmpty()) {
      LOGGER.info("Waiting for queue to be emptied...");
      safeSleep(500);
    }
    long totalData = ((long) dataCount) * putTakeThreadCount;
    Assert.assertEquals(totalData, queue.getAdded());
    Assert.assertEquals(totalData, queue.getRemoved());
    executor.shutdown();
    executor.awaitTermination(1000, TimeUnit.MILLISECONDS); //shutdown puts
    executor.shutdownNow();
    executor.awaitTermination(1000, TimeUnit.MILLISECONDS); //shutdown takes
    //Randomized should not report thread leak
  }



  private void safeSleep(long sleep) {
    try {
      Thread.sleep(sleep);
    } catch (InterruptedException ie) {
      Thread.currentThread().interrupt();
    }
  }




  /**
   * Helper runnable for test {@link ThroughputQueueMultiThreadTest#testBlockOnFullQueue()}
   */
  private class BlocksOnFullQueue implements Runnable {

    private CountDownLatch full;
    volatile private boolean complete;
    private int queueSize;
    private CountDownLatch finished;
    private BlockingQueue queue;

    public BlocksOnFullQueue(CountDownLatch latch, CountDownLatch finished, BlockingQueue queue, int queueSize) {
      this.full = latch;
      this.complete = false;
      this.queueSize = queueSize;
      this.finished = finished;
      this.queue = queue;
    }

    @Override
    public void run() {
      try {
        for (int i = 0; i < this.queueSize; ++i) {
          this.queue.put(i);
        }
        this.full.countDown();
        this.queue.put(0);
        this.complete = true;
        this.finished.countDown();
      } catch (InterruptedException ie) {
        Thread.currentThread().interrupt();
      }
    }

    public boolean isComplete() {
      return this.complete;
    }
  }


  /**
   * Helper runnable class for test {@link ThroughputQueueMultiThreadTest#testBlockOnEmptyQueue()}
   */
  private class BlocksOnEmptyQueue implements Runnable {

    private CountDownLatch full;
    volatile private boolean complete;
    private int queueSize;
    private CountDownLatch finished;
    private BlockingQueue queue;

    public BlocksOnEmptyQueue(CountDownLatch full, CountDownLatch finished, int queueSize, BlockingQueue queue) {
      this.full = full;
      this.finished = finished;
      this.queueSize = queueSize;
      this.queue = queue;
      this.complete = false;
    }


    @Override
    public void run() {
      try {
        for(int i=0; i < this.queueSize; ++i) {
          this.queue.take();
        }
        this.full.countDown();
        this.queue.take();
        this.complete = true;
        this.finished.countDown();
      } catch (InterruptedException ie) {
        Thread.currentThread().interrupt();
      }
    }

    public boolean isComplete() {
      return this.complete;
    }
  }


  private class PutData implements Runnable {

    private BlockingQueue queue;
    private int dataCount;
    private CountDownLatch finished;

    public PutData(CountDownLatch finished, BlockingQueue queue, int dataCount) {
      this.queue = queue;
      this.dataCount = dataCount;
      this.finished = finished;
    }


    @Override
    public void run() {
      try {
        for(int i=0; i < this.dataCount; ++i) {
          this.queue.put(i);
        }
      } catch (InterruptedException ie) {
        LOGGER.error("PUT DATA interupted !");
        Thread.currentThread().interrupt();
      }
      this.finished.countDown();
    }
  }


  private class TakeData implements Runnable {

    private BlockingQueue queue;

    public TakeData(BlockingQueue queue) {
      this.queue = queue;
    }


    @Override
    public void run() {
      try {
        while(true) {
          this.queue.take();
        }
      } catch (InterruptedException ie) {
        LOGGER.error("PUT DATA interupted !");
        Thread.currentThread().interrupt();
      }
    }
  }

}
