/**
 * 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 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 org.apache.activemq.transport.vm;

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

import java.io.IOException;
import java.net.URI;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.activemq.command.BaseCommand;
import org.apache.activemq.command.ExceptionResponse;
import org.apache.activemq.command.Response;
import org.apache.activemq.command.ShutdownInfo;
import org.apache.activemq.state.CommandVisitor;
import org.apache.activemq.transport.FutureResponse;
import org.apache.activemq.transport.MutexTransport;
import org.apache.activemq.transport.ResponseCallback;
import org.apache.activemq.transport.ResponseCorrelator;
import org.apache.activemq.transport.Transport;
import org.apache.activemq.transport.TransportDisposedIOException;
import org.apache.activemq.transport.TransportListener;
import org.apache.activemq.util.Wait;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class VMTransportThreadSafeTest {

   private static final Logger LOG = LoggerFactory.getLogger(VMTransportThreadSafeTest.class);

   private final static String location1 = "vm://transport1";
   private final static String location2 = "vm://transport2";

   private final ConcurrentLinkedQueue<DummyCommand> localReceived = new ConcurrentLinkedQueue<DummyCommand>();
   private final ConcurrentLinkedQueue<DummyCommand> remoteReceived = new ConcurrentLinkedQueue<DummyCommand>();

   private class DummyCommand extends BaseCommand {

      public final int sequenceId;

      public DummyCommand() {
         this.sequenceId = 0;
      }

      public DummyCommand(int id) {
         this.sequenceId = id;
      }

      @Override
      public Response visit(CommandVisitor visitor) throws Exception {
         return null;
      }

      @Override
      public byte getDataStructureType() {
         return 42;
      }
   }

   private class VMTestTransportListener implements TransportListener {

      protected final Queue<DummyCommand> received;

      public boolean shutdownReceived = false;

      public VMTestTransportListener(Queue<DummyCommand> receiveQueue) {
         this.received = receiveQueue;
      }

      @Override
      public void onCommand(Object command) {

         if (command instanceof ShutdownInfo) {
            shutdownReceived = true;
         }
         else {
            received.add((DummyCommand) command);
         }
      }

      @Override
      public void onException(IOException error) {
      }

      @Override
      public void transportInterupted() {
      }

      @Override
      public void transportResumed() {
      }
   }

   private class VMResponderTransportListener implements TransportListener {

      protected final Queue<DummyCommand> received;

      private final Transport peer;

      public VMResponderTransportListener(Queue<DummyCommand> receiveQueue, Transport peer) {
         this.received = receiveQueue;
         this.peer = peer;
      }

      @Override
      public void onCommand(Object command) {

         if (command instanceof ShutdownInfo) {
            return;
         }
         else {
            received.add((DummyCommand) command);

            if (peer != null) {
               try {
                  peer.oneway(command);
               }
               catch (IOException e) {
               }
            }
         }
      }

      @Override
      public void onException(IOException error) {
      }

      @Override
      public void transportInterupted() {
      }

      @Override
      public void transportResumed() {
      }
   }

   private class SlowVMTestTransportListener extends VMTestTransportListener {

      private final TimeUnit delayUnit;
      private final long delay;

      public SlowVMTestTransportListener(Queue<DummyCommand> receiveQueue) {
         this(receiveQueue, 10, TimeUnit.MILLISECONDS);
      }

      public SlowVMTestTransportListener(Queue<DummyCommand> receiveQueue, long delay, TimeUnit delayUnit) {
         super(receiveQueue);

         this.delay = delay;
         this.delayUnit = delayUnit;
      }

      @Override
      public void onCommand(Object command) {
         super.onCommand(command);
         try {
            delayUnit.sleep(delay);
         }
         catch (InterruptedException e) {
         }
      }
   }

   private class GatedVMTestTransportListener extends VMTestTransportListener {

      private final CountDownLatch gate;

      public GatedVMTestTransportListener(Queue<DummyCommand> receiveQueue) {
         this(receiveQueue, new CountDownLatch(1));
      }

      public GatedVMTestTransportListener(Queue<DummyCommand> receiveQueue, CountDownLatch gate) {
         super(receiveQueue);

         this.gate = gate;
      }

      @Override
      public void onCommand(Object command) {
         super.onCommand(command);
         try {
            gate.await();
         }
         catch (InterruptedException e) {
         }
      }
   }

   private void assertMessageAreOrdered(ConcurrentLinkedQueue<DummyCommand> queue) {
      int lastSequenceId = 0;
      for (DummyCommand command : queue) {
         int id = command.sequenceId;
         assertTrue("Last id: " + lastSequenceId + " should be less than current id: " + id, id > lastSequenceId);
      }
   }

   @Before
   public void setUp() throws Exception {
      localReceived.clear();
      remoteReceived.clear();
   }

   @After
   public void tearDown() throws Exception {
   }

   @Test(timeout = 60000)
   public void testStartWthoutListenerIOE() throws Exception {

      final VMTransport local = new VMTransport(new URI(location1));
      final VMTransport remote = new VMTransport(new URI(location2));

      local.setPeer(remote);
      remote.setPeer(local);

      remote.setTransportListener(new VMTestTransportListener(localReceived));

      try {
         local.start();
         fail("Should have thrown an IOExcoption");
      }
      catch (IOException e) {
      }
   }

   @Test(timeout = 60000)
   public void testOnewayOnStoppedTransportTDE() throws Exception {

      final VMTransport local = new VMTransport(new URI(location1));
      final VMTransport remote = new VMTransport(new URI(location2));

      local.setPeer(remote);
      remote.setPeer(local);

      local.setTransportListener(new VMTestTransportListener(localReceived));
      remote.setTransportListener(new VMTestTransportListener(remoteReceived));

      local.start();
      local.stop();

      try {
         local.oneway(new DummyCommand());
         fail("Should have thrown a TransportDisposedException");
      }
      catch (TransportDisposedIOException e) {
      }
   }

   @Test(timeout = 60000)
   public void testStopSendsShutdownToPeer() throws Exception {

      final VMTransport local = new VMTransport(new URI(location1));
      final VMTransport remote = new VMTransport(new URI(location2));

      local.setPeer(remote);
      remote.setPeer(local);

      final VMTestTransportListener remoteListener = new VMTestTransportListener(remoteReceived);

      local.setTransportListener(new VMTestTransportListener(localReceived));
      remote.setTransportListener(remoteListener);

      local.start();
      local.stop();

      assertTrue(Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return remoteListener.shutdownReceived;
         }
      }));
   }

   @Test(timeout = 60000)
   public void testRemoteStopSendsExceptionToPendingRequests() throws Exception {

      final VMTransport local = new VMTransport(new URI(location1));
      final VMTransport remote = new VMTransport(new URI(location2));

      local.setPeer(remote);
      remote.setPeer(local);

      final VMTestTransportListener remoteListener = new VMTestTransportListener(remoteReceived);
      remote.setTransportListener(remoteListener);
      remote.start();

      final Response[] answer = new Response[1];
      ResponseCorrelator responseCorrelator = new ResponseCorrelator(local);
      responseCorrelator.setTransportListener(new VMTestTransportListener(localReceived));
      responseCorrelator.start();
      responseCorrelator.asyncRequest(new DummyCommand(), new ResponseCallback() {
         @Override
         public void onCompletion(FutureResponse resp) {
            try {
               answer[0] = resp.getResult();
            }
            catch (IOException e) {
               e.printStackTrace();
            }
         }
      });

      // simulate broker stop
      remote.stop();

      assertTrue(Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            LOG.info("answer: " + answer[0]);
            return answer[0] instanceof ExceptionResponse && ((ExceptionResponse) answer[0]).getException() instanceof TransportDisposedIOException;
         }
      }));

      local.stop();
   }

   @Test(timeout = 60000)
   public void testMultipleStartsAndStops() throws Exception {

      final VMTransport local = new VMTransport(new URI(location1));
      final VMTransport remote = new VMTransport(new URI(location2));

      local.setPeer(remote);
      remote.setPeer(local);

      local.setTransportListener(new VMTestTransportListener(localReceived));
      remote.setTransportListener(new VMTestTransportListener(remoteReceived));

      local.start();
      remote.start();

      local.start();
      remote.start();

      for (int i = 0; i < 100; ++i) {
         local.oneway(new DummyCommand());
      }

      for (int i = 0; i < 100; ++i) {
         remote.oneway(new DummyCommand());
      }

      local.start();
      remote.start();

      assertTrue(Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return remoteReceived.size() == 100;
         }
      }));

      assertTrue(Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return localReceived.size() == 100;
         }
      }));

      local.stop();
      local.stop();
      remote.stop();
      remote.stop();
   }

   @Test(timeout = 60000)
   public void testStartWithPeerNotStartedEnqueusCommandsNonAsync() throws Exception {
      doTestStartWithPeerNotStartedEnqueusCommands(false);
   }

   private void doTestStartWithPeerNotStartedEnqueusCommands(boolean async) throws Exception {

      final VMTransport local = new VMTransport(new URI(location1));
      final VMTransport remote = new VMTransport(new URI(location2));

      remote.setAsync(async);

      local.setPeer(remote);
      remote.setPeer(local);

      local.setTransportListener(new VMTestTransportListener(localReceived));
      remote.setTransportListener(new VMTestTransportListener(remoteReceived));

      local.start();

      for (int i = 0; i < 100; ++i) {
         local.oneway(new DummyCommand());
      }

      assertEquals(100, remote.getMessageQueue().size());

      remote.start();

      assertTrue(Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return remoteReceived.size() == 100;
         }
      }));

      local.stop();
      remote.stop();
   }

   @Test(timeout = 60000)
   public void testBlockedOnewayEnqeueAandStopTransportAsync() throws Exception {
      doTestBlockedOnewayEnqeueAandStopTransport(true);
   }

   @Test(timeout = 60000)
   public void testBlockedOnewayEnqeueAandStopTransportNonAsync() throws Exception {
      doTestBlockedOnewayEnqeueAandStopTransport(false);
   }

   private void doTestBlockedOnewayEnqeueAandStopTransport(boolean async) throws Exception {

      final VMTransport local = new VMTransport(new URI(location1));
      final VMTransport remote = new VMTransport(new URI(location2));

      final AtomicInteger sequenceId = new AtomicInteger();

      remote.setAsync(async);
      remote.setAsyncQueueDepth(99);

      local.setPeer(remote);
      remote.setPeer(local);

      local.setTransportListener(new VMTestTransportListener(localReceived));
      remote.setTransportListener(new VMTestTransportListener(remoteReceived));

      local.start();

      Thread t = new Thread(new Runnable() {

         @Override
         public void run() {
            for (int i = 0; i < 100; ++i) {
               try {
                  local.oneway(new DummyCommand(sequenceId.incrementAndGet()));
               }
               catch (Exception e) {
               }
            }

         }
      });
      t.start();

      LOG.debug("Started async delivery, wait for remote's queue to fill up");

      assertTrue(Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return remote.getMessageQueue().remainingCapacity() == 0;
         }
      }));

      LOG.debug("Remote messageQ is full, start it and stop all");

      remote.start();
      local.stop();
      remote.stop();
   }

   @Test(timeout = 60000)
   public void testBlockedOnewayEnqeueWhileStartedDetectsStop() throws Exception {
      final VMTransport local = new VMTransport(new URI(location1));
      final VMTransport remote = new VMTransport(new URI(location2));

      final AtomicInteger sequenceId = new AtomicInteger();

      remote.setAsync(true);
      remote.setAsyncQueueDepth(2);

      local.setPeer(remote);
      remote.setPeer(local);

      local.setTransportListener(new VMTestTransportListener(localReceived));
      remote.setTransportListener(new GatedVMTestTransportListener(remoteReceived));

      local.start();
      remote.start();

      Thread t = new Thread(new Runnable() {

         @Override
         public void run() {
            for (int i = 0; i < 3; ++i) {
               try {
                  local.oneway(new DummyCommand(sequenceId.incrementAndGet()));
               }
               catch (Exception e) {
               }
            }

         }
      });
      t.start();

      LOG.debug("Started async delivery, wait for remote's queue to fill up");
      assertTrue(Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return remote.getMessageQueue().remainingCapacity() == 0;
         }
      }));

      LOG.debug("Starting async gate open.");
      Thread gateman = new Thread(new Runnable() {
         @Override
         public void run() {
            try {
               Thread.sleep(100);
            }
            catch (InterruptedException e) {
            }
            ((GatedVMTestTransportListener) remote.getTransportListener()).gate.countDown();
         }
      });
      gateman.start();

      remote.stop();
      local.stop();

      assertEquals(1, remoteReceived.size());
      assertMessageAreOrdered(remoteReceived);
   }

   @Test(timeout = 60000)
   public void testStopWhileStartingAsyncWithNoAsyncLimit() throws Exception {
      // In the async case the iterate method should see that we are stopping and
      // drop out before we dispatch all the messages but it should get at least 49 since
      // the stop thread waits 500 mills and the listener is waiting 10 mills on each receive.
      doTestStopWhileStartingWithNoAsyncLimit(true, 49);
   }

   @Test(timeout = 60000)
   public void testStopWhileStartingNonAsyncWithNoAsyncLimit() throws Exception {
      // In the non-async case the start dispatches all messages up front and then continues on
      doTestStopWhileStartingWithNoAsyncLimit(false, 100);
   }

   private void doTestStopWhileStartingWithNoAsyncLimit(boolean async, final int expect) throws Exception {

      final VMTransport local = new VMTransport(new URI(location1));
      final VMTransport remote = new VMTransport(new URI(location2));

      remote.setAsync(async);

      local.setPeer(remote);
      remote.setPeer(local);

      local.setTransportListener(new VMTestTransportListener(localReceived));
      remote.setTransportListener(new SlowVMTestTransportListener(remoteReceived));

      local.start();

      for (int i = 0; i < 100; ++i) {
         local.oneway(new DummyCommand(i));
      }

      Thread t = new Thread(new Runnable() {

         @Override
         public void run() {
            try {
               Thread.sleep(1000);
               remote.stop();
            }
            catch (Exception e) {
            }
         }
      });

      remote.start();

      t.start();

      assertTrue("Remote should receive: " + expect + ", commands but got: " + remoteReceived.size(), Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return remoteReceived.size() >= expect;
         }
      }));

      LOG.debug("Remote listener received " + remoteReceived.size() + " messages");

      local.stop();

      assertTrue("Remote transport never was disposed.", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return remote.isDisposed();
         }
      }));
   }

   @Test(timeout = 120000)
   public void TestTwoWayMessageThroughPutSync() throws Exception {

      long totalTimes = 0;
      final long executions = 20;

      for (int i = 0; i < 20; ++i) {
         totalTimes += doTestTwoWayMessageThroughPut(false);
      }

      LOG.info("Total time of one way sync send throughput test: " + (totalTimes / executions) + "ms");
   }

   @Test(timeout = 120000)
   public void TestTwoWayMessageThroughPutAsnyc() throws Exception {

      long totalTimes = 0;
      final long executions = 50;

      for (int i = 0; i < executions; ++i) {
         totalTimes += doTestTwoWayMessageThroughPut(false);
      }

      LOG.info("Total time of one way async send throughput test: " + (totalTimes / executions) + "ms");
   }

   private long doTestTwoWayMessageThroughPut(boolean async) throws Exception {

      final VMTransport local = new VMTransport(new URI(location1));
      final VMTransport remote = new VMTransport(new URI(location2));

      final AtomicInteger sequenceId = new AtomicInteger();

      remote.setAsync(async);

      local.setPeer(remote);
      remote.setPeer(local);

      local.setTransportListener(new VMTestTransportListener(localReceived));
      remote.setTransportListener(new VMTestTransportListener(remoteReceived));

      final int messageCount = 200000;

      local.start();
      remote.start();

      long startTime = System.currentTimeMillis();

      Thread localSend = new Thread(new Runnable() {

         @Override
         public void run() {
            for (int i = 0; i < messageCount; ++i) {
               try {
                  local.oneway(new DummyCommand(sequenceId.incrementAndGet()));
               }
               catch (Exception e) {
               }
            }

         }
      });

      Thread remoteSend = new Thread(new Runnable() {

         @Override
         public void run() {
            for (int i = 0; i < messageCount; ++i) {
               try {
                  remote.oneway(new DummyCommand(sequenceId.incrementAndGet()));
               }
               catch (Exception e) {
               }
            }

         }
      });

      localSend.start();
      remoteSend.start();

      // Wait for both to finish and then check that each side go the correct amount
      localSend.join();
      remoteSend.join();

      long endTime = System.currentTimeMillis();

      assertTrue(Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return remoteReceived.size() == messageCount;
         }
      }));

      assertTrue(Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return localReceived.size() == messageCount;
         }
      }));

      LOG.debug("All messages sent,stop all");

      local.stop();
      remote.stop();

      localReceived.clear();
      remoteReceived.clear();

      return endTime - startTime;
   }

   @Test(timeout = 120000)
   public void TestOneWayMessageThroughPutSync() throws Exception {

      long totalTimes = 0;
      final long executions = 30;

      for (int i = 0; i < executions; ++i) {
         totalTimes += doTestOneWayMessageThroughPut(false);
      }

      LOG.info("Total time of one way sync send throughput test: " + (totalTimes / executions) + "ms");
   }

   @Test(timeout = 120000)
   public void TestOneWayMessageThroughPutAsnyc() throws Exception {

      long totalTimes = 0;
      final long executions = 20;

      for (int i = 0; i < 20; ++i) {
         totalTimes += doTestOneWayMessageThroughPut(true);
      }

      LOG.info("Total time of one way async send throughput test: " + (totalTimes / executions) + "ms");
   }

   private long doTestOneWayMessageThroughPut(boolean async) throws Exception {

      final VMTransport local = new VMTransport(new URI(location1));
      final VMTransport remote = new VMTransport(new URI(location2));

      final AtomicInteger sequenceId = new AtomicInteger();

      remote.setAsync(async);

      local.setPeer(remote);
      remote.setPeer(local);

      local.setTransportListener(new VMTestTransportListener(localReceived));
      remote.setTransportListener(new VMTestTransportListener(remoteReceived));

      final int messageCount = 100000;

      local.start();
      remote.start();

      long startTime = System.currentTimeMillis();

      Thread localSend = new Thread(new Runnable() {

         @Override
         public void run() {
            for (int i = 0; i < messageCount; ++i) {
               try {
                  local.oneway(new DummyCommand(sequenceId.incrementAndGet()));
               }
               catch (Exception e) {
               }
            }

         }
      });

      localSend.start();

      // Wait for both to finish and then check that each side go the correct amount
      localSend.join();

      long endTime = System.currentTimeMillis();

      assertTrue(Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return remoteReceived.size() == messageCount;
         }
      }));

      LOG.debug("All messages sent,stop all");

      local.stop();
      remote.stop();

      localReceived.clear();
      remoteReceived.clear();

      return endTime - startTime;
   }

   @Test(timeout = 120000)
   public void testTwoWayTrafficWithMutexTransportSync1() throws Exception {

      for (int i = 0; i < 20; ++i) {
         doTestTwoWayTrafficWithMutexTransport(false, false);
      }
   }

   @Test(timeout = 120000)
   public void testTwoWayTrafficWithMutexTransportSync2() throws Exception {

      for (int i = 0; i < 20; ++i) {
         doTestTwoWayTrafficWithMutexTransport(true, false);
      }
   }

   @Test(timeout = 120000)
   public void testTwoWayTrafficWithMutexTransportSync3() throws Exception {

      for (int i = 0; i < 20; ++i) {
         doTestTwoWayTrafficWithMutexTransport(false, true);
      }
   }

   @Test(timeout = 120000)
   public void testTwoWayTrafficWithMutexTransportSync4() throws Exception {

      for (int i = 0; i < 20; ++i) {
         doTestTwoWayTrafficWithMutexTransport(false, false);
      }
   }

   public void doTestTwoWayTrafficWithMutexTransport(boolean localAsync, boolean remoteAsync) throws Exception {

      final VMTransport vmlocal = new VMTransport(new URI(location1));
      final VMTransport vmremote = new VMTransport(new URI(location2));

      final MutexTransport local = new MutexTransport(vmlocal);
      final MutexTransport remote = new MutexTransport(vmremote);

      final AtomicInteger sequenceId = new AtomicInteger();

      vmlocal.setAsync(localAsync);
      vmremote.setAsync(remoteAsync);

      vmlocal.setPeer(vmremote);
      vmremote.setPeer(vmlocal);

      local.setTransportListener(new VMTestTransportListener(localReceived));
      remote.setTransportListener(new VMResponderTransportListener(remoteReceived, remote));

      final int messageCount = 200000;

      Thread localSend = new Thread(new Runnable() {

         @Override
         public void run() {
            for (int i = 0; i < messageCount; ++i) {
               try {
                  local.oneway(new DummyCommand(sequenceId.incrementAndGet()));
               }
               catch (Exception e) {
               }
            }
         }
      });

      Thread remoteSend = new Thread(new Runnable() {

         @Override
         public void run() {
            for (int i = 0; i < messageCount; ++i) {
               try {
                  remote.oneway(new DummyCommand(sequenceId.incrementAndGet()));
               }
               catch (Exception e) {
               }
            }
         }
      });

      localSend.start();
      remoteSend.start();

      Thread.sleep(10);

      local.start();
      remote.start();

      // Wait for both to finish and then check that each side go the correct amount
      localSend.join();
      remoteSend.join();

      assertTrue("Remote should have received (" + messageCount + ") but got ()" + remoteReceived.size(), Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return remoteReceived.size() == messageCount;
         }
      }));

      assertTrue("Local should have received (" + messageCount * 2 + ") but got ()" + localReceived.size(), Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return localReceived.size() == messageCount * 2;
         }
      }));

      LOG.debug("All messages sent,stop all");

      local.stop();
      remote.stop();

      localReceived.clear();
      remoteReceived.clear();
   }

}
