/**
 * 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.Destination;
import javax.jms.MessageConsumer;
import javax.management.ObjectName;
import java.lang.Thread.UncaughtExceptionHandler;
import java.net.URI;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.activemq.JmsMultipleBrokersTestSupport;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.jmx.ManagementContext;
import org.apache.activemq.broker.jmx.QueueViewMBean;
import org.apache.activemq.broker.region.policy.PolicyEntry;
import org.apache.activemq.broker.region.policy.PolicyMap;
import org.apache.activemq.network.NetworkConnector;
import org.apache.activemq.util.Wait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class VerifyNetworkConsumersDisconnectTest extends JmsMultipleBrokersTestSupport implements UncaughtExceptionHandler {

   public static final int BROKER_COUNT = 3;
   public static final int CONSUMER_COUNT = 5;
   public static final int MESSAGE_COUNT = 0;
   public static final boolean DUPLEX = false;
   public static final boolean CONDUIT = true;

   public static final int NETWORK_TTL = 6;
   private static final Logger LOG = LoggerFactory.getLogger(VerifyNetworkConsumersDisconnectTest.class);
   public static final int TIMEOUT = 30000;

   protected Map<String, MessageConsumer> consumerMap;
   final Map<Thread, Throwable> unhandledExceptions = new HashMap<>();

   private void assertNoUnhandledExceptions() {
      for (Entry<Thread, Throwable> e : unhandledExceptions.entrySet()) {
         LOG.error("Thread:" + e.getKey() + " Had unexpected: " + e.getValue());
      }
      assertTrue("There are no unhandled exceptions, see: log for detail on: " + unhandledExceptions, unhandledExceptions.isEmpty());
   }

   public NetworkConnector bridge(String from, String to) throws Exception {
      NetworkConnector networkConnector = bridgeBrokers(from, to, true, NETWORK_TTL, CONDUIT);
      networkConnector.setSuppressDuplicateQueueSubscriptions(true);
      networkConnector.setDecreaseNetworkConsumerPriority(true);
      networkConnector.setDuplex(DUPLEX);
      return networkConnector;
   }

   /*why conduit proxy proxy consumers gets us in a knot w.r.t removal
   DC-7 for CA-9, add DB-15, remove CA-9, add CB-8
   CB-8 add DC-7
   CB-8 - why not dead?
   CB-8 for BA-6, add BD-15, remove BA-6
   BD-15 for DA-11, add DC-7
   */
   public void testConsumerOnEachBroker() throws Exception {
      bridge("Broker0", "Broker1");
      if (!DUPLEX)
         bridge("Broker1", "Broker0");

      bridge("Broker1", "Broker2");
      if (!DUPLEX)
         bridge("Broker2", "Broker1");

      startAllBrokers();
      waitForBridgeFormation(brokers.get("Broker0").broker, 1, 0);
      waitForBridgeFormation(brokers.get("Broker2").broker, 1, 0);
      waitForBridgeFormation(brokers.get("Broker1").broker, 1, 0);
      waitForBridgeFormation(brokers.get("Broker1").broker, 1, 1);

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

      // Setup consumers
      for (int i = 0; i < BROKER_COUNT; i++) {
         consumerMap.put("Consumer:" + i + ":0", createConsumer("Broker" + i, dest));
      }

      assertExactConsumersConnect("Broker0", 3, 1, TIMEOUT);
      assertExactConsumersConnect("Broker2", 3, 1, TIMEOUT);
      // piggy in the middle
      assertExactConsumersConnect("Broker1", 3, 1, TIMEOUT);

      assertNoUnhandledExceptions();

      LOG.info("Complete the mesh - 0->2");

      // shorter route
      NetworkConnector nc = bridge("Broker0", "Broker2");
      nc.setBrokerName("Broker0");
      nc.start();

      if (!DUPLEX) {
         LOG.info("... complete the mesh - 2->0");
         nc = bridge("Broker2", "Broker0");
         nc.setBrokerName("Broker2");
         nc.start();
      }

      // wait for consumers to get propagated
      for (int i = 0; i < BROKER_COUNT; i++) {
         assertExactConsumersConnect("Broker" + i, 3, 1, TIMEOUT);
      }

      // reverse order close
      consumerMap.get("Consumer:" + 2 + ":0").close();
      TimeUnit.SECONDS.sleep(1);
      consumerMap.get("Consumer:" + 1 + ":0").close();
      TimeUnit.SECONDS.sleep(1);
      consumerMap.get("Consumer:" + 0 + ":0").close();

      LOG.info("Check for no consumers..");
      for (int i = 0; i < BROKER_COUNT; i++) {
         assertExactConsumersConnect("Broker" + i, 0, 0, TIMEOUT);
      }

   }

   public void testXConsumerOnEachBroker() throws Exception {
      bridge("Broker0", "Broker1");
      if (!DUPLEX)
         bridge("Broker1", "Broker0");

      bridge("Broker1", "Broker2");
      if (!DUPLEX)
         bridge("Broker2", "Broker1");

      startAllBrokers();

      waitForBridgeFormation(brokers.get("Broker0").broker, 1, 0);
      waitForBridgeFormation(brokers.get("Broker2").broker, 1, 0);
      waitForBridgeFormation(brokers.get("Broker1").broker, 1, 0);
      waitForBridgeFormation(brokers.get("Broker1").broker, 1, 1);

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

      // Setup consumers
      for (int i = 0; i < BROKER_COUNT; i++) {
         for (int j = 0; j < CONSUMER_COUNT; j++)
            consumerMap.put("Consumer:" + i + ":" + j, createConsumer("Broker" + i, dest));
      }

      for (int i = 0; i < BROKER_COUNT; i++) {
         assertExactConsumersConnect("Broker" + i, CONSUMER_COUNT + (BROKER_COUNT - 1), 1, TIMEOUT);
      }

      assertNoUnhandledExceptions();

      LOG.info("Complete the mesh - 0->2");

      // shorter route
      NetworkConnector nc = bridge("Broker0", "Broker2");
      nc.setBrokerName("Broker0");
      nc.start();

      waitForBridgeFormation(brokers.get("Broker0").broker, 1, 1);

      if (!DUPLEX) {
         LOG.info("... complete the mesh - 2->0");
         nc = bridge("Broker2", "Broker0");
         nc.setBrokerName("Broker2");
         nc.start();
      }

      waitForBridgeFormation(brokers.get("Broker2").broker, 1, 1);

      for (int i = 0; i < BROKER_COUNT; i++) {
         assertExactConsumersConnect("Broker" + i, CONSUMER_COUNT + (BROKER_COUNT - 1), 1, TIMEOUT);
      }

      // reverse order close
      for (int i = 0; i < CONSUMER_COUNT; i++) {
         consumerMap.get("Consumer:" + 2 + ":" + i).close();
         TimeUnit.SECONDS.sleep(1);
         consumerMap.get("Consumer:" + 1 + ":" + i).close();
         TimeUnit.SECONDS.sleep(1);
         consumerMap.get("Consumer:" + 0 + ":" + i).close();
      }

      LOG.info("Check for no consumers..");
      for (int i = 0; i < BROKER_COUNT; i++) {
         assertExactConsumersConnect("Broker" + i, 0, 0, TIMEOUT);
      }

   }

   protected void assertExactConsumersConnect(final String brokerName,
                                              final int count,
                                              final int numChecks,
                                              long timeout) throws Exception {
      final ManagementContext context = brokers.get(brokerName).broker.getManagementContext();
      final AtomicInteger stability = new AtomicInteger(0);
      assertTrue("Expected consumers count: " + count + " on: " + brokerName, Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            try {
               QueueViewMBean queueViewMBean = (QueueViewMBean) context.newProxyInstance(brokers.get(brokerName).broker.getAdminView().getQueues()[0], QueueViewMBean.class, false);
               long currentCount = queueViewMBean.getConsumerCount();
               LOG.info("On " + brokerName + " current consumer count for " + queueViewMBean + ", " + currentCount);
               LinkedList<String> consumerIds = new LinkedList<>();
               for (ObjectName objectName : queueViewMBean.getSubscriptions()) {
                  consumerIds.add(objectName.getKeyProperty("consumerId"));
               }
               LOG.info("Sub IDs: " + consumerIds);
               if (currentCount == count) {
                  stability.incrementAndGet();
               } else {
                  stability.set(0);
               }
               return stability.get() > numChecks;
            } catch (Exception e) {
               LOG.warn(": ", e);
               return false;
            }
         }
      }, timeout));
   }

   @Override
   public void setUp() throws Exception {
      super.setAutoFail(true);
      super.setUp();

      unhandledExceptions.clear();
      Thread.setDefaultUncaughtExceptionHandler(this);

      // Setup n brokers
      for (int i = 0; i < BROKER_COUNT; i++) {
         createBroker(new URI("broker:(tcp://localhost:6161" + i + ")/Broker" + i + "?persistent=false&useJmx=true&brokerId=Broker" + i));
      }

      consumerMap = new LinkedHashMap<>();
   }

   @Override
   protected void configureBroker(BrokerService brokerService) {
      PolicyEntry policyEntry = new PolicyEntry();
      policyEntry.setExpireMessagesPeriod(0);
      PolicyMap policyMap = new PolicyMap();
      policyMap.setDefaultEntry(policyEntry);
      brokerService.setDestinationPolicy(policyMap);
   }

   @Override
   public void uncaughtException(Thread t, Throwable e) {
      synchronized (unhandledExceptions) {
         unhandledExceptions.put(t, e);
      }
   }
}
