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

import javax.jms.Connection;
import javax.jms.Destination;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.Session;
import java.net.URI;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQPrefetchPolicy;
import org.apache.activemq.JmsMultipleBrokersTestSupport;
import org.apache.activemq.broker.BrokerPlugin;
import org.apache.activemq.broker.BrokerPluginSupport;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.ProducerBrokerExchange;
import org.apache.activemq.broker.region.policy.PolicyEntry;
import org.apache.activemq.broker.region.policy.PolicyMap;
import org.apache.activemq.command.ActiveMQDestination;
import org.apache.activemq.network.ConditionalNetworkBridgeFilterFactory;
import org.apache.activemq.network.NetworkConnector;
import org.apache.activemq.store.kahadb.KahaDBPersistenceAdapter;
import org.apache.activemq.util.Wait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 */
public class TwoBrokerQueueClientsReconnectTest extends JmsMultipleBrokersTestSupport {

   protected static final int MESSAGE_COUNT = 100; // Best if a factor of 100
   protected static final int PREFETCH_COUNT = 1;
   protected static final int NETWORK_PREFETCH = 1;
   private static final Logger LOG = LoggerFactory.getLogger(TwoBrokerQueueClientsReconnectTest.class);

   protected int msgsClient1;
   protected int msgsClient2;
   protected String broker1;
   protected String broker2;

   public void testClientAReceivesOnly() throws Exception {
      broker1 = "BrokerA";
      broker2 = "BrokerB";

      doOneClientReceivesOnly();
   }

   public void testClientBReceivesOnly() throws Exception {
      broker1 = "BrokerB";
      broker2 = "BrokerA";

      doOneClientReceivesOnly();
   }

   public void doOneClientReceivesOnly() throws Exception {
      // allow immediate replay back to origin
      applyRateLimitNetworkFilter(0);

      // Bridge brokers
      bridgeBrokers(broker1, broker2);
      bridgeBrokers(broker2, broker1);

      // Run brokers
      startAllBrokers();

      // Create queue
      Destination dest = createDestination("TEST.FOO", false);

      // Create consumers
      MessageConsumer client1 = createConsumer(broker1, dest);
      MessageConsumer client2 = createConsumer(broker2, dest);

      // Give clients time to register with broker
      Thread.sleep(500);

      // Always send messages to broker A
      sendMessages("BrokerA", dest, MESSAGE_COUNT);

      // Close the second client, messages should be sent to the first client
      client2.close();

      // Let the first client receive all messages
      msgsClient1 += receiveAllMessages(client1);
      client1.close();

      // First client should have received 100 messages
      assertEquals("Client for " + broker1 + " should have receive all messages.", MESSAGE_COUNT, msgsClient1);
   }

   public void testClientAReceivesOnlyAfterReconnect() throws Exception {
      broker1 = "BrokerA";
      broker2 = "BrokerB";

      doOneClientReceivesOnlyAfterReconnect();
   }

   public void testClientBReceivesOnlyAfterReconnect() throws Exception {
      broker1 = "BrokerB";
      broker2 = "BrokerA";

      doOneClientReceivesOnlyAfterReconnect();
   }

   public void doOneClientReceivesOnlyAfterReconnect() throws Exception {
      // allow immediate replay back to origin
      applyRateLimitNetworkFilter(0);

      // Bridge brokers
      bridgeBrokers(broker1, broker2);
      bridgeBrokers(broker2, broker1);

      // Run brokers
      startAllBrokers();

      // Create queue
      Destination dest = createDestination("TEST.FOO", false);

      // Create first consumer
      MessageConsumer client1 = createConsumer(broker1, dest);
      MessageConsumer client2 = createConsumer(broker2, dest);

      // Give clients time to register with broker
      Thread.sleep(500);

      // Always send message to broker A
      sendMessages("BrokerA", dest, MESSAGE_COUNT);

      // Let the first client receive the first 20% of messages
      msgsClient1 += receiveExactMessages(client1, (int) (MESSAGE_COUNT * 0.20));

      // Disconnect the first client
      client1.close();

      // Create another client for the first broker
      client1 = createConsumer(broker1, dest);
      Thread.sleep(500);

      // Close the second client, messages should be sent to the first client
      client2.close();

      // Receive the rest of the messages
      msgsClient1 += receiveAllMessages(client1);
      client1.close();

      // The first client should have received 100 messages
      assertEquals("Client for " + broker1 + " should have received all messages.", MESSAGE_COUNT, msgsClient1);
   }

   public void testTwoClientsReceiveClientADisconnects() throws Exception {
      broker1 = "BrokerA";
      broker2 = "BrokerB";

      doTwoClientsReceiveOneClientDisconnects();
   }

   public void testTwoClientsReceiveClientBDisconnects() throws Exception {
      broker1 = "BrokerB";
      broker2 = "BrokerA";

      doTwoClientsReceiveOneClientDisconnects();
   }

   public void doTwoClientsReceiveOneClientDisconnects() throws Exception {
      // ensure all message do not flow across the network too quickly
      applyRateLimitNetworkFilter(0.8 * MESSAGE_COUNT);

      // Bridge brokers
      bridgeBrokers(broker1, broker2);
      bridgeBrokers(broker2, broker1);

      // Run brokers
      startAllBrokers();

      // Create queue
      Destination dest = createDestination("TEST.FOO", false);

      // Create first client
      MessageConsumer client1 = createConsumer(broker1, dest);
      MessageConsumer client2 = createConsumer(broker2, dest);

      // Give clients time to register with broker
      Thread.sleep(500);

      // Always send messages to broker A
      sendMessages("BrokerA", dest, MESSAGE_COUNT);

      LOG.info("Let each client receive 20% of the messages - 40% total");
      msgsClient1 += receiveExactMessages(client1, (int) (MESSAGE_COUNT * 0.20));
      msgsClient2 += receiveExactMessages(client2, (int) (MESSAGE_COUNT * 0.20));

      // Disconnect the first client
      client1.close();

      LOG.info("Let the second client receive the rest of the messages");
      msgsClient2 += receiveAllMessages(client2);
      client2.close();

      // First client should have received 20% of the messages
      assertEquals("Client for " + broker1 + " should have received 20% of the messages.", (int) (MESSAGE_COUNT * 0.20), msgsClient1);

      // Second client should have received 80% of the messages
      assertEquals("Client for " + broker2 + " should have received 80% of the messages.", (int) (MESSAGE_COUNT * 0.80), msgsClient2);
   }

   public void testTwoClientsReceiveClientAReconnects() throws Exception {
      broker1 = "BrokerA";
      broker2 = "BrokerB";

      doTwoClientsReceiveOneClientReconnects();
   }

   public void testTwoClientsReceiveClientBReconnects() throws Exception {
      broker1 = "BrokerB";
      broker2 = "BrokerA";

      doTwoClientsReceiveOneClientReconnects();
   }

   public void doTwoClientsReceiveOneClientReconnects() throws Exception {
      // ensure all message do not flow across the network too quickly
      applyRateLimitNetworkFilter(0.2 * MESSAGE_COUNT);

      // Bridge brokers
      bridgeBrokers(broker1, broker2);
      bridgeBrokers(broker2, broker1);

      // Run brokers
      startAllBrokers();

      // Create queue
      Destination dest = createDestination("TEST.FOO", false);

      // Create the first client
      MessageConsumer client1 = createConsumer(broker1, dest);
      MessageConsumer client2 = createConsumer(broker2, dest);

      // Give clients time to register with broker
      Thread.sleep(500);

      // Always send messages to broker A
      sendMessages("BrokerA", dest, MESSAGE_COUNT);

      // Let each client receive 20% of the messages - 40% total
      msgsClient1 += receiveExactMessages(client1, (int) (MESSAGE_COUNT * 0.20));
      msgsClient2 += receiveExactMessages(client2, (int) (MESSAGE_COUNT * 0.20));

      LOG.info("msgsClient1=" + msgsClient1);
      LOG.info("msgsClient2=" + msgsClient2);

      Thread.sleep(1000);
      LOG.info("Disconnect the first client");
      client1.close();

      LOG.info("Let the second client receive 20% more of the total messages");
      msgsClient2 += receiveExactMessages(client2, (int) (MESSAGE_COUNT * 0.20));

      LOG.info("msgsClient2=" + msgsClient2);

      // Create another client for broker 1
      client1 = createConsumer(broker1, dest);
      Thread.sleep(1000);

      // Let each client receive 20% of the messages - 40% total
      msgsClient1 += receiveExactMessages(client1, (int) (MESSAGE_COUNT * 0.20));
      client1.close();
      LOG.info("new consumer addition, msgsClient1=" + msgsClient1);

      Thread.sleep(2000);
      msgsClient2 += receiveExactMessages(client2, (int) (MESSAGE_COUNT * 0.20));
      client2.close();
      LOG.info("msgsClient2=" + msgsClient2);

      // First client should have received 40 messages
      assertEquals("Client for " + broker1 + " should have received 40% of the messages.", (int) (MESSAGE_COUNT * 0.40), msgsClient1);

      // Second client should have received 60 messages
      assertEquals("Client for " + broker2 + " should have received 60% of the messages.", (int) (MESSAGE_COUNT * 0.60), msgsClient2);
   }

   private void applyRateLimitNetworkFilter(double rateLimit) {
      ConditionalNetworkBridgeFilterFactory filterFactory = new ConditionalNetworkBridgeFilterFactory();
      filterFactory.setReplayWhenNoConsumers(true);
      filterFactory.setRateLimit((int) rateLimit);
      filterFactory.setRateDuration(1000);

      Collection<BrokerItem> brokerList = brokers.values();
      for (Iterator<BrokerItem> i = brokerList.iterator(); i.hasNext(); ) {
         BrokerService broker = i.next().broker;
         broker.getDestinationPolicy().getDefaultEntry().setNetworkBridgeFilterFactory(filterFactory);
      }
   }

   public void testTwoClientsReceiveTwoClientReconnects() throws Exception {
      // ensure all message do not flow across the network too quickly
      applyRateLimitNetworkFilter(0.5 * MESSAGE_COUNT);

      broker1 = "BrokerA";
      broker2 = "BrokerB";

      // Bridge brokers
      bridgeBrokers(broker1, broker2);
      bridgeBrokers(broker2, broker1);

      // Run brokers
      startAllBrokers();

      // Create queue
      Destination dest = createDestination("TEST.FOO", false);

      // Create the first client
      MessageConsumer client1 = createConsumer(broker1, dest);
      MessageConsumer client2 = createConsumer(broker2, dest);

      // Give clients time to register with broker
      Thread.sleep(500);

      // Always send messages to broker A
      sendMessages("BrokerA", dest, MESSAGE_COUNT);

      // Let each client receive 20% of the messages - 40% total
      msgsClient1 += receiveExactMessages(client1, (int) (MESSAGE_COUNT * 0.20));
      msgsClient2 += receiveExactMessages(client2, (int) (MESSAGE_COUNT * 0.20));

      LOG.info("Disconnect both clients");
      client1.close();
      client2.close();

      // Let each client receive 30% more of the total messages - 60% total
      LOG.info("Serially create another two clients for each broker and consume in turn");
      client1 = createConsumer(broker1, dest);
      msgsClient1 += receiveExactMessages(client1, (int) (MESSAGE_COUNT * 0.30));
      client1.close();

      // the close will allow replay or the replay of the remaining messages
      client2 = createConsumer(broker2, dest);
      msgsClient2 += receiveExactMessages(client2, (int) (MESSAGE_COUNT * 0.30));
      client2.close();

      // First client should have received 50% of the messages
      assertEquals("Client for " + broker1 + " should have received 50% of the messages.", (int) (MESSAGE_COUNT * 0.50), msgsClient1);

      // Second client should have received 50% of the messages
      assertEquals("Client for " + broker2 + " should have received 50% of the messages.", (int) (MESSAGE_COUNT * 0.50), msgsClient2);
   }

   public void testDuplicateSend() throws Exception {
      broker1 = "BrokerA";
      broker2 = "BrokerB";

      // enable producer audit for the network connector, off by default b/c of interference with composite
      // dests and virtual topics
      brokers.get(broker2).broker.getTransportConnectors().get(0).setAuditNetworkProducers(true);
      bridgeBrokers(broker1, broker2);

      final AtomicBoolean first = new AtomicBoolean();
      final CountDownLatch gotMessageLatch = new CountDownLatch(1);

      BrokerService brokerService = brokers.get(broker2).broker;
      brokerService.setPersistent(true);
      brokerService.setDeleteAllMessagesOnStartup(true);
      brokerService.setPlugins(new BrokerPlugin[]{new BrokerPluginSupport() {
         @Override
         public void send(final ProducerBrokerExchange producerExchange,
                          org.apache.activemq.command.Message messageSend) throws Exception {
            super.send(producerExchange, messageSend);
            if (first.compareAndSet(false, true)) {
               producerExchange.getConnectionContext().setDontSendReponse(true);
               new Thread() {
                  @Override
                  public void run() {
                     try {
                        LOG.info("Waiting for recepit");
                        assertTrue("message received on time", gotMessageLatch.await(60, TimeUnit.SECONDS));
                        LOG.info("Stopping connection post send and receive and multiple producers");
                        producerExchange.getConnectionContext().getConnection().stop();
                     } catch (Exception e) {
                        e.printStackTrace();
                     }
                  }
               }.start();
            }
         }
      }});

      // Run brokers
      startAllBrokers();

      waitForBridgeFormation();

      // Create queue
      Destination dest = createDestination("TEST.FOO", false);

      MessageConsumer client2 = createConsumer(broker2, dest);

      sendMessages("BrokerA", dest, 1);

      assertEquals("Client got message", 1, receiveExactMessages(client2, 1));
      client2.close();
      gotMessageLatch.countDown();

      // message still pending on broker1
      assertEquals("messages message still there", 1, brokers.get(broker1).broker.getAdminView().getTotalMessageCount());

      client2 = createConsumer(broker2, dest);

      LOG.info("Let the second client receive the rest of the messages");
      assertEquals("no duplicate message", 0, receiveAllMessages(client2));
      assertEquals("no duplicate message", 0, receiveAllMessages(client2));

      assertEquals("no messages enqueued", 0, brokers.get(broker2).broker.getAdminView().getTotalMessageCount());
      assertTrue("no messages enqueued on origin", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return 0 == brokers.get(broker1).broker.getAdminView().getTotalMessageCount();
         }
      }));
   }

   public void testDuplicateSendWithNoAuditEnqueueCountStat() throws Exception {
      broker1 = "BrokerA";
      broker2 = "BrokerB";

      NetworkConnector networkConnector = bridgeBrokers(broker1, broker2);

      final AtomicBoolean first = new AtomicBoolean();
      final CountDownLatch gotMessageLatch = new CountDownLatch(1);

      BrokerService brokerService = brokers.get(broker2).broker;
      brokerService.setPersistent(true);
      brokerService.setDeleteAllMessagesOnStartup(true);
      // disable concurrent dispatch otherwise store duplicate suppression will be skipped b/c cursor audit is already
      // disabled so verification of stats will fail - ie: duplicate will be dispatched
      ((KahaDBPersistenceAdapter) brokerService.getPersistenceAdapter()).setConcurrentStoreAndDispatchQueues(false);
      brokerService.setPlugins(new BrokerPlugin[]{new BrokerPluginSupport() {
         @Override
         public void send(final ProducerBrokerExchange producerExchange,
                          org.apache.activemq.command.Message messageSend) throws Exception {
            super.send(producerExchange, messageSend);
            if (first.compareAndSet(false, true)) {
               producerExchange.getConnectionContext().setDontSendReponse(true);
               new Thread() {
                  @Override
                  public void run() {
                     try {
                        LOG.info("Waiting for recepit");
                        assertTrue("message received on time", gotMessageLatch.await(60, TimeUnit.SECONDS));
                        LOG.info("Stopping connection post send and receive and multiple producers");
                        producerExchange.getConnectionContext().getConnection().stop();
                     } catch (Exception e) {
                        e.printStackTrace();
                     }
                  }
               }.start();
            }
         }
      }});

      // Create queue
      ActiveMQDestination dest = createDestination("TEST.FOO", false);

      // statically include our destination
      networkConnector.addStaticallyIncludedDestination(dest);

      // Run brokers
      startAllBrokers();

      waitForBridgeFormation();

      sendMessages("BrokerA", dest, 1);

      // wait for broker2 to get the initial forward
      Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return brokers.get(broker2).broker.getAdminView().getTotalMessageCount() == 1;
         }
      });

      // message still pending on broker1
      assertEquals("messages message still there", 1, brokers.get(broker1).broker.getAdminView().getTotalMessageCount());

      // allow the bridge to be shutdown and restarted
      gotMessageLatch.countDown();

      // verify message is forwarded after restart
      assertTrue("no messages enqueued on origin", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return 0 == brokers.get(broker1).broker.getAdminView().getTotalMessageCount();
         }
      }));

      assertEquals("one messages pending", 1, brokers.get(broker2).broker.getAdminView().getTotalMessageCount());
      assertEquals("one messages enqueued", 1, brokers.get(broker2).broker.getDestination(dest).getDestinationStatistics().getEnqueues().getCount());
   }

   protected int receiveExactMessages(MessageConsumer consumer, int msgCount) throws Exception {
      Message msg;
      int i;
      for (i = 0; i < msgCount; i++) {
         msg = consumer.receive(4000);
         if (msg == null) {
            LOG.error("Consumer failed to receive exactly " + msgCount + " messages. Actual messages received is: " + i);
            break;
         }
      }

      return i;
   }

   protected int receiveAllMessages(MessageConsumer consumer) throws Exception {
      int msgsReceived = 0;

      Message msg;
      do {
         msg = consumer.receive(1000);
         if (msg != null) {
            msgsReceived++;
         }
      } while (msg != null);

      return msgsReceived;
   }

   @Override
   protected MessageConsumer createConsumer(String brokerName, Destination dest) throws Exception {
      Connection conn = createConnection(brokerName);
      conn.start();
      Session sess = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
      return sess.createConsumer(dest);
   }

   @Override
   protected void configureBroker(BrokerService broker) {
      PolicyMap policyMap = new PolicyMap();
      PolicyEntry defaultEntry = new PolicyEntry();
      defaultEntry.setEnableAudit(false);
      policyMap.setDefaultEntry(defaultEntry);
      broker.setDestinationPolicy(policyMap);
   }

   @Override
   protected NetworkConnector bridgeBrokers(BrokerService localBroker,
                                            BrokerService remoteBroker,
                                            boolean dynamicOnly,
                                            int networkTTL,
                                            boolean conduit,
                                            boolean failover) throws Exception {
      NetworkConnector nc = super.bridgeBrokers(localBroker, remoteBroker, dynamicOnly, networkTTL, conduit, failover);
      nc.setPrefetchSize(NETWORK_PREFETCH);
      nc.setDecreaseNetworkConsumerPriority(true);
      return nc;
   }

   @Override
   public void setUp() throws Exception {
      super.setAutoFail(true);
      super.setUp();
      createBroker(new URI("broker:(tcp://localhost:61616)/BrokerA?persistent=false&useJmx=true"));
      createBroker(new URI("broker:(tcp://localhost:61617)/BrokerB?persistent=false&useJmx=true"));

      // Configure broker connection factory
      ActiveMQConnectionFactory factoryA;
      ActiveMQConnectionFactory factoryB;
      factoryA = (ActiveMQConnectionFactory) getConnectionFactory("BrokerA");
      factoryB = (ActiveMQConnectionFactory) getConnectionFactory("BrokerB");

      // Set prefetch policy
      ActiveMQPrefetchPolicy policy = new ActiveMQPrefetchPolicy();
      policy.setAll(PREFETCH_COUNT);

      factoryA.setPrefetchPolicy(policy);
      factoryB.setPrefetchPolicy(policy);

      msgsClient1 = 0;
      msgsClient2 = 0;
   }
}
