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

import org.apache.streams.core.StreamsDatum;
import org.apache.streams.local.counters.StreamsTaskCounter;
import org.apache.streams.local.test.processors.PassthroughDatumCounterProcessor;
import org.apache.streams.local.test.providers.NumericMessageProvider;
import org.apache.streams.local.test.writer.DatumCounterWriter;
import org.apache.streams.util.ComponentUtils;

import com.google.common.util.concurrent.Uninterruptibles;
import org.junit.After;
import org.junit.Test;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 *
 */
public class BasicTasksTest {


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

  @Test
  public void testProviderTask() {
    int numMessages = 100;
    NumericMessageProvider provider = new NumericMessageProvider(numMessages);
    StreamsProviderTask task = new StreamsProviderTask(provider, false, null);
    BlockingQueue<StreamsDatum> outQueue = new LinkedBlockingQueue<>();
    task.addOutputQueue(outQueue);
    //Test that adding input queues to providers is not valid
    BlockingQueue<StreamsDatum> inQueue = createInputQueue(numMessages);
    Exception exp = null;
    try {
      task.addInputQueue(inQueue);
    } catch (UnsupportedOperationException uoe) {
      exp = uoe;
    }
    assertNotNull(exp);

    ExecutorService service = Executors.newFixedThreadPool(1);
    service.submit(task);
    int attempts = 0;
    while(outQueue.size() != numMessages) {
      Uninterruptibles.sleepUninterruptibly(500, TimeUnit.MILLISECONDS);
      if(attempts == 10) {
        fail("Provider task failed to output "+numMessages+" in a timely fashion.");
      }
    }
    service.shutdown();
    try {
      if(!service.awaitTermination(10, TimeUnit.SECONDS)){
        service.shutdownNow();
        fail("Service did not terminate.");
      }
      assertTrue("Task should have completed running in allotted time.", service.isTerminated());
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
  }

  @Test
  public void testProcessorTask() {
    int numMessages = 100;
    PassthroughDatumCounterProcessor processor = new PassthroughDatumCounterProcessor("");
    StreamsProcessorTask task = new StreamsProcessorTask(processor);
    StreamsTaskCounter counter = new StreamsTaskCounter(MBEAN_ID, null, -1);
    task.setStreamsTaskCounter(counter);
    BlockingQueue<StreamsDatum> outQueue = new LinkedBlockingQueue<>();
    BlockingQueue<StreamsDatum> inQueue = createInputQueue(numMessages);
    task.addOutputQueue(outQueue);
    task.addInputQueue(inQueue);
    assertEquals(numMessages, task.getInputQueues().get(0).size());
    ExecutorService service = Executors.newFixedThreadPool(1);
    service.submit(task);
    int attempts = 0;
    while(inQueue.size() != 0 && outQueue.size() != numMessages) {
      Uninterruptibles.sleepUninterruptibly(500, TimeUnit.MILLISECONDS);
      ++attempts;
      if(attempts == 10) {
        fail("Processor task failed to output "+numMessages+" in a timely fashion.");
      }
    }
    task.stopTask();;
    service.shutdown();
    try {
      if(!service.awaitTermination(5, TimeUnit.SECONDS)){
        service.shutdownNow();
        fail("Service did not terminate.");
      }
      assertTrue("Task should have completed running in allotted time.", service.isTerminated());
    } catch (InterruptedException e) {
      fail("Test Interrupted.");
    }
    assertEquals(numMessages, processor.getMessageCount());
    assertEquals(numMessages, counter.getNumReceived());
    assertEquals(numMessages, counter.getNumEmitted());
    assertEquals(0, counter.getNumUnhandledErrors());
    assertEquals(0.0, counter.getErrorRate(), 0.0);
  }

  @Test
  public void testWriterTask() {
    int numMessages = 100;
    DatumCounterWriter writer = new DatumCounterWriter("");
    StreamsPersistWriterTask task = new StreamsPersistWriterTask(writer);
    StreamsTaskCounter counter = new StreamsTaskCounter(MBEAN_ID, null, -1);
    task.setStreamsTaskCounter(counter);
    BlockingQueue<StreamsDatum> outQueue = new LinkedBlockingQueue<>();
    BlockingQueue<StreamsDatum> inQueue = createInputQueue(numMessages);

    Exception exp = null;
    try {
      task.addOutputQueue(outQueue);
    } catch (UnsupportedOperationException uoe) {
      exp = uoe;
    }
    assertNotNull(exp);
    task.addInputQueue(inQueue);
    assertEquals(numMessages, task.getInputQueues().get(0).size());
    ExecutorService service = Executors.newFixedThreadPool(1);
    service.submit(task);
    int attempts = 0;
    while(inQueue.size() != 0 ) {
      Uninterruptibles.sleepUninterruptibly(500, TimeUnit.MILLISECONDS);
      ++attempts;
      if(attempts == 10) {
        fail("Processor task failed to output "+numMessages+" in a timely fashion.");
      }
    }
    task.stopTask();
    service.shutdown();
    try {
      if(!service.awaitTermination(15, TimeUnit.SECONDS)){
        service.shutdownNow();
        fail("Service did not terminate.");
      }
      assertTrue("Task should have completed running in allotted time.", service.isTerminated());
    } catch (InterruptedException e) {
      fail("Test Interrupted.");
    }
    assertEquals(numMessages, writer.getDatumsCounted());
    assertEquals(numMessages, counter.getNumReceived());
    assertEquals(0, counter.getNumEmitted());
    assertEquals(0, counter.getNumUnhandledErrors());
    assertEquals(0.0, counter.getErrorRate(), 0.0);
  }

  @Test
  public void testMergeTask() {
    int numMessages = 100;
    int incoming = 5;
    StreamsMergeTask task = new StreamsMergeTask();
    BlockingQueue<StreamsDatum> outQueue = new LinkedBlockingQueue<>();
    task.addOutputQueue(outQueue);
    for(int i=0; i < incoming; ++i) {
      task.addInputQueue(createInputQueue(numMessages));
    }
    ExecutorService service = Executors.newFixedThreadPool(1);
    service.submit(task);
    int attempts = 0;
    while(outQueue.size() != incoming * numMessages ) {
      Uninterruptibles.sleepUninterruptibly(500, TimeUnit.MILLISECONDS);
      ++attempts;
      if(attempts == 10) {
        assertEquals("Processor task failed to output " + (numMessages * incoming) + " in a timely fashion.", (numMessages * incoming), outQueue.size());
      }
    }
    task.stopTask();
    service.shutdown();
    try {
      if(!service.awaitTermination(5, TimeUnit.SECONDS)){
        service.shutdownNow();
        fail("Service did not terminate.");
      }
      assertTrue("Task should have completed running in allotted time.", service.isTerminated());
    } catch (InterruptedException e) {
      fail("Test Interrupted.");
    }
  }

  @Test
  public void testBranching() {
    int numMessages = 100;
    PassthroughDatumCounterProcessor processor = new PassthroughDatumCounterProcessor("");
    StreamsProcessorTask task = new StreamsProcessorTask(processor);
    BlockingQueue<StreamsDatum> outQueue1 = new LinkedBlockingQueue<>();
    BlockingQueue<StreamsDatum> outQueue2 = new LinkedBlockingQueue<>();
    BlockingQueue<StreamsDatum> inQueue = createInputQueue(numMessages);
    task.addOutputQueue(outQueue1);
    task.addOutputQueue(outQueue2);
    task.addInputQueue(inQueue);
    assertEquals(numMessages, task.getInputQueues().get(0).size());
    ExecutorService service = Executors.newFixedThreadPool(1);
    service.submit(task);
    int attempts = 0;
    while(inQueue.size() != 0 ) {
      Uninterruptibles.sleepUninterruptibly(500, TimeUnit.MILLISECONDS);
      ++attempts;
      if(attempts == 10) {
        assertEquals("Processor task failed to output "+(numMessages)+" in a timely fashion.", 0, inQueue.size());
      }
    }
    task.stopTask();

    service.shutdown();
    try {
      if(!service.awaitTermination(5, TimeUnit.SECONDS)){
        service.shutdownNow();
        fail("Service did not terminate.");
      }
      assertTrue("Task should have completed running in allotted time.", service.isTerminated());
    } catch (InterruptedException e) {
      fail("Test Interrupted.");
    }
    assertEquals(numMessages, processor.getMessageCount());
    assertEquals(numMessages, outQueue1.size());
    assertEquals(numMessages, outQueue2.size());
  }

  @Test
  public void testBranchingSerialization() {
    int numMessages = 1;
    PassthroughDatumCounterProcessor processor = new PassthroughDatumCounterProcessor("");
    StreamsProcessorTask task = new StreamsProcessorTask(processor);
    BlockingQueue<StreamsDatum> outQueue1 = new LinkedBlockingQueue<>();
    BlockingQueue<StreamsDatum> outQueue2 = new LinkedBlockingQueue<>();
    BlockingQueue<StreamsDatum> inQueue = createInputQueue(numMessages);
    task.addOutputQueue(outQueue1);
    task.addOutputQueue(outQueue2);
    task.addInputQueue(inQueue);
    ExecutorService service = Executors.newFixedThreadPool(1);
    service.submit(task);
    int attempts = 0;
    while(inQueue.size() != 0 ) {
      Uninterruptibles.sleepUninterruptibly(500, TimeUnit.MILLISECONDS);
      ++attempts;
      if(attempts == 10) {
        assertEquals("Processor task failed to output "+(numMessages)+" in a timely fashion.", 0, inQueue.size());
      }
    }
    task.stopTask();

    service.shutdown();
    try {
      if(!service.awaitTermination(5, TimeUnit.SECONDS)){
        service.shutdownNow();
        fail("Service did not terminate.");
      }
      assertTrue("Task should have completed running in allotted time.", service.isTerminated());
    } catch (InterruptedException e) {
      fail("Test Interrupted.");
    }
    assertEquals(numMessages, processor.getMessageCount());
    assertEquals(numMessages, outQueue1.size());
    assertEquals(numMessages, outQueue2.size());
    StreamsDatum datum1 = outQueue1.poll();
    StreamsDatum datum2 = outQueue2.poll();
    assertNotNull(datum1);
    assertEquals(datum1, datum2);
    datum1.setDocument("a");
    assertNotEquals(datum1, datum2);
  }

  private BlockingQueue<StreamsDatum> createInputQueue(int numDatums) {
    BlockingQueue<StreamsDatum> queue = new LinkedBlockingQueue<>();
    for(int i=0; i < numDatums; ++i) {
      queue.add(new StreamsDatum(i));
    }
    return queue;
  }


}
