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

import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.apache.activemq.advisory.AdvisorySupport;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.command.ActiveMQMessage;
import org.apache.activemq.command.ActiveMQTopic;
import org.apache.activemq.command.BrokerId;
import org.apache.activemq.command.BrokerInfo;
import org.apache.activemq.command.ConnectionId;
import org.apache.activemq.command.ConsumerId;
import org.apache.activemq.command.ConsumerInfo;
import org.apache.activemq.command.MessageAck;
import org.apache.activemq.command.MessageDispatch;
import org.apache.activemq.command.RemoveInfo;
import org.apache.activemq.command.Response;
import org.apache.activemq.command.SessionId;
import org.apache.activemq.transport.FutureResponse;
import org.apache.activemq.transport.ResponseCallback;
import org.apache.activemq.transport.Transport;
import org.apache.activemq.transport.TransportListener;
import org.easymock.EasyMock;
import org.easymock.IAnswer;
import org.easymock.IMocksControl;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class NetworkRouteTest {

   private IMocksControl control;
   private BrokerService brokerService;
   private Transport localBroker;
   private Transport remoteBroker;
   private TransportListener localListener;
   private TransportListener remoteListener;
   private MessageDispatch msgDispatch;
   private ActiveMQMessage path1Msg;
   private ActiveMQMessage path2Msg;
   private ActiveMQMessage removePath1Msg;
   private ActiveMQMessage removePath2Msg;

   // this sort of mockery is very brittle but it is fast!

   @Test
   public void verifyNoRemoveOnOneConduitRemove() throws Exception {
      EasyMock.expect(localBroker.request(EasyMock.isA(ConsumerInfo.class))).andReturn(null);
      control.replay();

      remoteListener.onCommand(path2Msg);
      remoteListener.onCommand(path1Msg);

      remoteListener.onCommand(removePath2Msg);
      control.verify();
   }

   @Test
   public void addAndRemoveOppositeOrder() throws Exception {
      // from (1)
      localBroker.request(EasyMock.isA(ConsumerInfo.class));
      ArgHolder localConsumer = ArgHolder.holdArgsForLastObjectCall();
      // from (2a)
      remoteBroker.asyncRequest(EasyMock.isA(ActiveMQMessage.class), EasyMock.isA(ResponseCallback.class));
      ArgHolder firstMessageFuture = ArgHolder.holdArgsForLastFutureRequestCall();
      localBroker.oneway(EasyMock.isA(MessageAck.class));
      // from (2b)
      remoteBroker.asyncRequest(EasyMock.isA(ActiveMQMessage.class), EasyMock.isA(ResponseCallback.class));
      ArgHolder secondMessageFuture = ArgHolder.holdArgsForLastFutureRequestCall();

      localBroker.oneway(EasyMock.isA(MessageAck.class));
      // from (3)
      localBroker.oneway(EasyMock.isA(RemoveInfo.class));
      ExpectationWaiter waitForRemove = ExpectationWaiter.waiterForLastVoidCall();
      control.replay();

      // (1) send advisory of path 1
      remoteListener.onCommand(path1Msg);
      msgDispatch.setConsumerId(((ConsumerInfo) localConsumer.arguments[0]).getConsumerId());
      // send advisory of path 2, doesn't send a ConsumerInfo to localBroker
      remoteListener.onCommand(path2Msg);
      // (2a) send a message
      localListener.onCommand(msgDispatch);
      ResponseCallback callback = (ResponseCallback) firstMessageFuture.arguments[1];
      FutureResponse response = new FutureResponse(callback);
      response.set(new Response());

      // send advisory of path 2 remove, doesn't send a RemoveInfo to localBroker
      remoteListener.onCommand(removePath2Msg);
      // (2b) send a message
      localListener.onCommand(msgDispatch);
      callback = (ResponseCallback) secondMessageFuture.arguments[1];
      response = new FutureResponse(callback);
      response.set(new Response());

      // (3) send advisory of path 1 remove, sends a RemoveInfo to localBroker
      remoteListener.onCommand(removePath1Msg);
      waitForRemove.assertHappens(5, TimeUnit.SECONDS);
      // send a message, does not send message as in 2a and 2b
      localListener.onCommand(msgDispatch);

      control.verify();
   }

   @Test
   public void addAndRemoveSameOrder() throws Exception {
      // from (1)
      localBroker.request(EasyMock.isA(ConsumerInfo.class));
      ArgHolder localConsumer = ArgHolder.holdArgsForLastObjectCall();

      // from (2a)
      remoteBroker.asyncRequest(EasyMock.isA(ActiveMQMessage.class), EasyMock.isA(ResponseCallback.class));
      ArgHolder firstMessageFuture = ArgHolder.holdArgsForLastFutureRequestCall();

      localBroker.oneway(EasyMock.isA(MessageAck.class));

      // from (2b)
      remoteBroker.asyncRequest(EasyMock.isA(ActiveMQMessage.class), EasyMock.isA(ResponseCallback.class));
      ArgHolder secondMessageFuture = ArgHolder.holdArgsForLastFutureRequestCall();

      localBroker.oneway(EasyMock.isA(MessageAck.class));

      // from (3)
      localBroker.oneway(EasyMock.isA(RemoveInfo.class));
      ExpectationWaiter waitForRemove = ExpectationWaiter.waiterForLastVoidCall();
      control.replay();

      // (1) send advisory of path 1
      remoteListener.onCommand(path1Msg);
      msgDispatch.setConsumerId(((ConsumerInfo) localConsumer.arguments[0]).getConsumerId());
      // send advisory of path 2, doesn't send a ConsumerInfo to localBroker
      remoteListener.onCommand(path2Msg);
      // (2a) send a message
      localListener.onCommand(msgDispatch);
      ResponseCallback callback = (ResponseCallback) firstMessageFuture.arguments[1];
      FutureResponse response = new FutureResponse(callback);
      response.set(new Response());

      // send advisory of path 1 remove, shouldn't send a RemoveInfo to localBroker
      remoteListener.onCommand(removePath1Msg);
      // (2b) send a message, should send the message as in 2a
      localListener.onCommand(msgDispatch);
      callback = (ResponseCallback) secondMessageFuture.arguments[1];
      response = new FutureResponse(callback);
      response.set(new Response());

      // (3) send advisory of path 1 remove, should send a RemoveInfo to localBroker
      remoteListener.onCommand(removePath2Msg);
      waitForRemove.assertHappens(5, TimeUnit.SECONDS);
      // send a message, does not send message as in 2a
      localListener.onCommand(msgDispatch);

      control.verify();
   }

   @Before
   public void before() throws Exception {
      control = EasyMock.createControl();
      localBroker = control.createMock(Transport.class);
      remoteBroker = control.createMock(Transport.class);

      NetworkBridgeConfiguration configuration = new NetworkBridgeConfiguration();
      brokerService = new BrokerService();
      BrokerInfo remoteBrokerInfo = new BrokerInfo();

      configuration.setDuplex(true);
      configuration.setNetworkTTL(5);
      brokerService.setBrokerId("broker-1");
      brokerService.setPersistent(false);
      brokerService.setUseJmx(false);
      brokerService.start();
      brokerService.waitUntilStarted();
      remoteBrokerInfo.setBrokerId(new BrokerId("remote-broker-id"));
      remoteBrokerInfo.setBrokerName("remote-broker-name");

      localBroker.setTransportListener(EasyMock.isA(TransportListener.class));
      ArgHolder localListenerRef = ArgHolder.holdArgsForLastVoidCall();

      remoteBroker.setTransportListener(EasyMock.isA(TransportListener.class));
      ArgHolder remoteListenerRef = ArgHolder.holdArgsForLastVoidCall();
      localBroker.start();
      remoteBroker.start();

      remoteBroker.oneway(EasyMock.isA(Object.class));
      EasyMock.expectLastCall().times(4);
      remoteBroker.oneway(EasyMock.isA(Object.class));
      ExpectationWaiter remoteInitWaiter = ExpectationWaiter.waiterForLastVoidCall();

      localBroker.oneway(remoteBrokerInfo);
      EasyMock.expect(localBroker.request(EasyMock.isA(Object.class))).andReturn(null);
      localBroker.oneway(EasyMock.isA(Object.class));
      ExpectationWaiter localInitWaiter = ExpectationWaiter.waiterForLastVoidCall();

      control.replay();

      DurableConduitBridge bridge = new DurableConduitBridge(configuration, localBroker, remoteBroker);
      bridge.setBrokerService(brokerService);
      bridge.start();

      localListener = (TransportListener) localListenerRef.getArguments()[0];
      Assert.assertNotNull(localListener);
      remoteListener = (TransportListener) remoteListenerRef.getArguments()[0];
      Assert.assertNotNull(remoteListener);

      remoteListener.onCommand(remoteBrokerInfo);

      remoteInitWaiter.assertHappens(5, TimeUnit.SECONDS);
      localInitWaiter.assertHappens(5, TimeUnit.SECONDS);

      control.verify();
      control.reset();

      ActiveMQMessage msg = new ActiveMQMessage();
      msg.setDestination(new ActiveMQTopic("test"));
      msgDispatch = new MessageDispatch();
      msgDispatch.setMessage(msg);

      ConsumerInfo path1 = new ConsumerInfo();
      path1.setDestination(msg.getDestination());
      path1.setConsumerId(new ConsumerId(new SessionId(new ConnectionId("conn-id-1"), 1), 3));
      path1.setBrokerPath(new BrokerId[]{new BrokerId("remote-broker-id"), new BrokerId("server(1)-broker-id"),});
      path1Msg = new ActiveMQMessage();
      path1Msg.setDestination(AdvisorySupport.getConsumerAdvisoryTopic(path1.getDestination()));
      path1Msg.setDataStructure(path1);

      ConsumerInfo path2 = new ConsumerInfo();
      path2.setDestination(path1.getDestination());
      path2.setConsumerId(new ConsumerId(new SessionId(new ConnectionId("conn-id-2"), 2), 4));
      path2.setBrokerPath(new BrokerId[]{new BrokerId("remote-broker-id"), new BrokerId("server(2)-broker-id"), new BrokerId("server(1)-broker-id"),});
      path2Msg = new ActiveMQMessage();
      path2Msg.setDestination(path1Msg.getDestination());
      path2Msg.setDataStructure(path2);

      RemoveInfo removePath1 = new RemoveInfo(path1.getConsumerId());
      RemoveInfo removePath2 = new RemoveInfo(path2.getConsumerId());

      removePath1Msg = new ActiveMQMessage();
      removePath1Msg.setDestination(path1Msg.getDestination());
      removePath1Msg.setDataStructure(removePath1);

      removePath2Msg = new ActiveMQMessage();
      removePath2Msg.setDestination(path1Msg.getDestination());
      removePath2Msg.setDataStructure(removePath2);
   }

   @After
   public void after() throws Exception {
      control.reset();
      brokerService.stop();
      brokerService.waitUntilStopped();
   }

   private static class ArgHolder {

      public Object[] arguments;

      public static ArgHolder holdArgsForLastVoidCall() {
         final ArgHolder holder = new ArgHolder();
         EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
            @Override
            public Object answer() throws Throwable {
               Object[] args = EasyMock.getCurrentArguments();
               holder.arguments = Arrays.copyOf(args, args.length);
               return null;
            }
         });
         return holder;
      }

      public static ArgHolder holdArgsForLastObjectCall() {
         final ArgHolder holder = new ArgHolder();
         EasyMock.expect(new Object()).andAnswer(new IAnswer<Object>() {
            @Override
            public Object answer() throws Throwable {
               Object[] args = EasyMock.getCurrentArguments();
               holder.arguments = Arrays.copyOf(args, args.length);
               return null;
            }
         });
         return holder;
      }

      public static ArgHolder holdArgsForLastFutureRequestCall() {
         final ArgHolder holder = new ArgHolder();
         EasyMock.expect(new FutureResponse(null)).andAnswer(new IAnswer<FutureResponse>() {
            @Override
            public FutureResponse answer() throws Throwable {
               Object[] args = EasyMock.getCurrentArguments();
               holder.arguments = Arrays.copyOf(args, args.length);
               return null;
            }
         });

         return holder;
      }

      public Object[] getArguments() {
         Assert.assertNotNull(arguments);
         return arguments;
      }
   }

   private static class ExpectationWaiter {

      private CountDownLatch latch = new CountDownLatch(1);

      public static ExpectationWaiter waiterForLastVoidCall() {
         final ExpectationWaiter waiter = new ExpectationWaiter();
         EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
            @Override
            public Object answer() throws Throwable {
               waiter.latch.countDown();
               return null;
            }
         });
         return waiter;
      }

      public void assertHappens(long timeout, TimeUnit unit) throws InterruptedException {
         Assert.assertTrue(latch.await(timeout, unit));
      }
   }
}