/**
 * 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.DeliveryMode;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import java.net.URI;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

import junit.framework.Test;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.JmsMultipleBrokersTestSupport;
import org.apache.activemq.advisory.AdvisorySupport;
import org.apache.activemq.broker.region.policy.DeadLetterStrategy;
import org.apache.activemq.broker.region.policy.PolicyEntry;
import org.apache.activemq.broker.region.policy.PolicyMap;
import org.apache.activemq.broker.region.policy.PriorityDispatchPolicy;
import org.apache.activemq.broker.region.policy.SharedDeadLetterStrategy;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ActiveMQTempTopic;
import org.apache.activemq.command.ActiveMQTextMessage;
import org.apache.activemq.network.DemandForwardingBridgeSupport;
import org.apache.activemq.network.NetworkBridge;
import org.apache.activemq.network.NetworkConnector;
import org.apache.activemq.util.DefaultTestAppender;
import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.spi.LoggingEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author <a href="http://www.christianposta.com/blog">Christian Posta</a>
 */
public class RequestReplyTempDestRemovalAdvisoryRaceTest extends JmsMultipleBrokersTestSupport {

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

   private static final String BROKER_A = "BrokerA";
   private static final String BROKER_B = "BrokerB";
   private static final String BROKER_C = "BrokerC";

   private static final int NUM_RESPONDENTS = 1;
   private static final int NUM_SENDS = 1;
   private static final int RANDOM_SLEEP_FOR_RESPONDENT_MS = 0;
   private static final int RANDOM_SLEEP_FOR_SENDER_MS = 1;
   private static final String QUEUE_NAME = "foo.queue";
   private static String[] TEST_ITERATIONS = new String[]{QUEUE_NAME + "0", QUEUE_NAME + "1", QUEUE_NAME + "2", QUEUE_NAME + "3"};

   final AtomicLong messageCount = new AtomicLong(0);
   final AtomicLong respondentSendError = new AtomicLong(0);
   final AtomicLong responseReceived = new AtomicLong(0);
   final AtomicLong sendsWithNoConsumers = new AtomicLong(0);
   final AtomicLong forwardFailures = new AtomicLong(0);

   protected final AtomicBoolean shutdown = new AtomicBoolean(false);
   HashSet<NetworkConnector> networkConnectors = new HashSet<>();
   HashSet<Connection> advisoryConsumerConnections = new HashSet<>();
   Appender slowDownAppender;

   CountDownLatch consumerDemandExists;

   protected boolean useDuplex = false;

   public static Test suite() {
      return suite(RequestReplyTempDestRemovalAdvisoryRaceTest.class);
   }

   /**
    * Notes: to reliably reproduce use debugger... set a "thread" breakpoint at line 679 in DemandForwardingBridgeSupport,
    * and only break on the "2nd" pass (broker C's bridge). Allow debugging to continue shortly after hitting
    * the breakpoint, for this test we use a logging appender to implement the pause,
    * it fails most of the time, hence the combos
    */
   public void initCombos() {
      addCombinationValues("QUEUE_NAME", TEST_ITERATIONS);
   }

   public void testTempDestRaceDuplex() throws Exception {
      // duplex
      useDuplex = true;
      bridgeBrokers(BROKER_A, BROKER_B, false, 3);
      bridgeBrokers(BROKER_B, BROKER_C, false, 3);

      startAllBrokers();

      waitForBridgeFormation(1);

      HashSet<NetworkBridge> bridgesStart = new HashSet<>();
      for (NetworkConnector networkConnector : networkConnectors) {
         bridgesStart.addAll(networkConnector.activeBridges());
      }
      LOG.info("Bridges start:" + bridgesStart);

      slowDownAdvisoryDispatch();
      noConsumerAdvisory();
      forwardFailureAdvisory();

      // set up respondents
      ExecutorService respondentThreadPool = Executors.newFixedThreadPool(50);
      BrokerItem brokerA = brokers.get(BROKER_A);
      ActiveMQConnectionFactory brokerAFactory = new ActiveMQConnectionFactory(brokerA.broker.getTransportConnectorByScheme("tcp").getName() + "?jms.watchTopicAdvisories=false");
      brokerAFactory.setAlwaysSyncSend(true);
      for (int i = 0; i < NUM_RESPONDENTS; i++) {
         respondentThreadPool.execute(new EchoRespondent(brokerAFactory));
      }

      // fire off sends
      ExecutorService senderThreadPool = Executors.newCachedThreadPool();
      BrokerItem brokerC = brokers.get(BROKER_C);
      ActiveMQConnectionFactory brokerCFactory = new ActiveMQConnectionFactory(brokerC.broker.getTransportConnectorByScheme("tcp").getName() + "?jms.watchTopicAdvisories=false");
      for (int i = 0; i < NUM_SENDS; i++) {
         senderThreadPool.execute(new MessageSender(brokerCFactory));
      }

      senderThreadPool.shutdown();
      senderThreadPool.awaitTermination(30, TimeUnit.SECONDS);
      TimeUnit.SECONDS.sleep(15);
      LOG.info("shutting down");
      shutdown.compareAndSet(false, true);

      HashSet<NetworkBridge> bridgesEnd = new HashSet<>();
      for (NetworkConnector networkConnector : networkConnectors) {
         bridgesEnd.addAll(networkConnector.activeBridges());
      }
      LOG.info("Bridges end:" + bridgesEnd);

      assertEquals("no new bridges created", bridgesStart, bridgesEnd);

      // validate success or error of dlq
      LOG.info("received: " + responseReceived.get() + ", respondent error: " + respondentSendError.get() + ", noConsumerCount: " + sendsWithNoConsumers.get() + ", forwardFailures: " + forwardFailures.get());
      assertEquals("success or error", NUM_SENDS, respondentSendError.get() + forwardFailures.get() + responseReceived.get() + sendsWithNoConsumers.get());

   }

   private void forwardFailureAdvisory() throws JMSException {
      for (BrokerItem item : brokers.values()) {
         ActiveMQConnectionFactory brokerAFactory = new ActiveMQConnectionFactory(item.broker.getTransportConnectorByScheme("tcp").getName() + "?jms.watchTopicAdvisories=false");
         Connection connection = brokerAFactory.createConnection();
         connection.start();
         connection.createSession(false, Session.AUTO_ACKNOWLEDGE).createConsumer(AdvisorySupport.getNetworkBridgeForwardFailureAdvisoryTopic()).setMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message) {
               forwardFailures.incrementAndGet();
            }
         });
      }
   }

   private void noConsumerAdvisory() throws JMSException {
      for (BrokerItem item : brokers.values()) {
         ActiveMQConnectionFactory brokerAFactory = new ActiveMQConnectionFactory(item.broker.getTransportConnectorByScheme("tcp").getName() + "?jms.watchTopicAdvisories=false");
         Connection connection = brokerAFactory.createConnection();
         connection.start();
         connection.createSession(false, Session.AUTO_ACKNOWLEDGE).createConsumer(AdvisorySupport.getNoTopicConsumersAdvisoryTopic(new ActiveMQTempTopic(">"))).setMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message) {
               sendsWithNoConsumers.incrementAndGet();
            }
         });
      }
   }

   public void testTempDestRace() throws Exception {
      // non duplex
      bridgeBrokers(BROKER_A, BROKER_B, false, 3);
      bridgeBrokers(BROKER_B, BROKER_A, false, 3);
      bridgeBrokers(BROKER_B, BROKER_C, false, 3);
      bridgeBrokers(BROKER_C, BROKER_B, false, 3);

      startAllBrokers();

      waitForBridgeFormation(1);

      HashSet<NetworkBridge> bridgesStart = new HashSet<>();
      for (NetworkConnector networkConnector : networkConnectors) {
         bridgesStart.addAll(networkConnector.activeBridges());
      }

      slowDownAdvisoryDispatch();
      noConsumerAdvisory();
      forwardFailureAdvisory();

      // set up respondents
      ExecutorService respondentThreadPool = Executors.newFixedThreadPool(50);
      BrokerItem brokerA = brokers.get(BROKER_A);
      ActiveMQConnectionFactory brokerAFactory = new ActiveMQConnectionFactory(brokerA.broker.getTransportConnectorByScheme("tcp").getName() + "?jms.watchTopicAdvisories=false");
      brokerAFactory.setAlwaysSyncSend(true);
      for (int i = 0; i < NUM_RESPONDENTS; i++) {
         respondentThreadPool.execute(new EchoRespondent(brokerAFactory));
      }

      // fire off sends
      ExecutorService senderThreadPool = Executors.newCachedThreadPool();
      BrokerItem brokerC = brokers.get(BROKER_C);
      ActiveMQConnectionFactory brokerCFactory = new ActiveMQConnectionFactory(brokerC.broker.getTransportConnectorByScheme("tcp").getName() + "?jms.watchTopicAdvisories=false");
      for (int i = 0; i < NUM_SENDS; i++) {
         senderThreadPool.execute(new MessageSender(brokerCFactory));
      }

      senderThreadPool.shutdown();
      senderThreadPool.awaitTermination(30, TimeUnit.SECONDS);
      TimeUnit.SECONDS.sleep(10);
      LOG.info("shutting down");
      shutdown.compareAndSet(false, true);

      HashSet<NetworkBridge> bridgesEnd = new HashSet<>();
      for (NetworkConnector networkConnector : networkConnectors) {
         bridgesEnd.addAll(networkConnector.activeBridges());
      }
      assertEquals("no new bridges created", bridgesStart, bridgesEnd);

      // validate success or error or dlq
      LOG.info("received: " + responseReceived.get() + ", respondent error: " + respondentSendError.get() + ", noConsumerCount: " + sendsWithNoConsumers.get() + ", forwardFailures: " + forwardFailures.get());
      assertEquals("success or error", NUM_SENDS, respondentSendError.get() + forwardFailures.get() + responseReceived.get() + sendsWithNoConsumers.get());

   }

   private void slowDownAdvisoryDispatch() throws Exception {

      org.apache.log4j.Logger.getLogger(DemandForwardingBridgeSupport.class).setLevel(Level.DEBUG);

      // instrument a logger to block the processing of a remove sub advisory
      // simulate a slow thread
      slowDownAppender = new DefaultTestAppender() {
         @Override
         public void doAppend(LoggingEvent loggingEvent) {
            if (Level.DEBUG.equals(loggingEvent.getLevel())) {
               String message = loggingEvent.getMessage().toString();
               if (message.startsWith("BrokerB") && message.contains("remove local subscription")) {
                  // sleep for a bit
                  try {
                     consumerDemandExists.countDown();
                     System.err.println("Sleeping on receipt of remove info debug message: " + message);
                     TimeUnit.SECONDS.sleep(2);
                  } catch (Exception ignored) {
                  }
               }

            }
         }
      };

      org.apache.log4j.Logger.getRootLogger().addAppender(slowDownAppender);
   }

   @Override
   protected void setUp() throws Exception {
      super.setUp();
      responseReceived.set(0);
      respondentSendError.set(0);
      forwardFailures.set(0);
      sendsWithNoConsumers.set(0);
      networkConnectors.clear();
      advisoryConsumerConnections.clear();
      consumerDemandExists = new CountDownLatch(1);
      createBroker(new URI("broker:(tcp://localhost:0)/" + BROKER_A + "?persistent=false&useJmx=false")).setDedicatedTaskRunner(false);
      createBroker(new URI("broker:(tcp://localhost:0)/" + BROKER_B + "?persistent=false&useJmx=false")).setDedicatedTaskRunner(false);
      createBroker(new URI("broker:(tcp://localhost:0)/" + BROKER_C + "?persistent=false&useJmx=false")).setDedicatedTaskRunner(false);

      PolicyMap map = new PolicyMap();
      PolicyEntry defaultEntry = new PolicyEntry();
      defaultEntry.setSendAdvisoryIfNoConsumers(true);
      DeadLetterStrategy deadletterStrategy = new SharedDeadLetterStrategy();
      deadletterStrategy.setProcessNonPersistent(true);
      defaultEntry.setDeadLetterStrategy(deadletterStrategy);
      defaultEntry.setDispatchPolicy(new PriorityDispatchPolicy());
      map.put(new ActiveMQTempTopic(">"), defaultEntry);

      for (BrokerItem item : brokers.values()) {
         item.broker.setDestinationPolicy(map);
      }
   }

   @Override
   protected void tearDown() throws Exception {
      if (slowDownAppender != null) {
         org.apache.log4j.Logger.getRootLogger().removeAppender(slowDownAppender);
      }
      for (Connection connection : advisoryConsumerConnections) {
         connection.close();
      }
      super.tearDown();
   }

   protected NetworkConnector bridgeBrokers(String localBrokerName,
                                            String remoteBrokerName,
                                            boolean dynamicOnly,
                                            int networkTTL) throws Exception {
      NetworkConnector connector = super.bridgeBrokers(localBrokerName, remoteBrokerName, dynamicOnly, networkTTL, true);
      connector.setBridgeTempDestinations(true);
      connector.setAdvisoryForFailedForward(true);
      connector.setDuplex(useDuplex);
      connector.setAlwaysSyncSend(true);
      networkConnectors.add(connector);
      return connector;
   }

   abstract class MessageClient {

      protected Connection connection;
      protected Session session;
      protected MessageConsumer consumer;
      protected MessageProducer producer;
      protected Random random;
      protected int timeToSleep;

      // set up the connection and session
      public MessageClient(ActiveMQConnectionFactory factory, int timeToSleep) throws Exception {
         this.connection = factory.createConnection();
         this.session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
         this.timeToSleep = timeToSleep;
         this.random = new Random(System.currentTimeMillis());
         preInit();
         initProducer();
         initConsumer();
         this.connection.start();
      }

      protected void preInit() throws JMSException {

      }

      abstract protected void initProducer() throws JMSException;

      abstract protected void initConsumer() throws JMSException;
   }

   class MessageSender extends MessageClient implements Runnable {

      protected Destination tempDest;

      public MessageSender(ActiveMQConnectionFactory factory) throws Exception {
         super(factory, RANDOM_SLEEP_FOR_SENDER_MS);
      }

      @Override
      public void run() {
         // create a message
         try {
            TextMessage message = session.createTextMessage("request: message #" + messageCount.getAndIncrement());
            message.setJMSReplyTo(tempDest);
            producer.send(message);
            LOG.info("SENDER: Message [" + message.getText() + "] has been sent.");

            Message incomingMessage = consumer.receive(timeToSleep);
            if (incomingMessage instanceof TextMessage) {
               try {
                  LOG.info("SENDER: Got a response from echo service!" + ((TextMessage) incomingMessage).getText());
                  responseReceived.incrementAndGet();
               } catch (JMSException e) {
                  LOG.error("SENDER: might want to see why i'm getting non-text messages..." + incomingMessage, e);
               }
            } else {
               LOG.info("SENDER: Did not get a response this time");
            }

         } catch (JMSException e) {
            LOG.error("SENDER: Could not complete message sending properly: " + e.getMessage());
         } finally {
            try {
               producer.close();
               consumer.close();
               session.close();
               connection.close();
            } catch (JMSException e) {
               e.printStackTrace();
            }
         }
      }

      @Override
      protected void preInit() throws JMSException {
         this.tempDest = session.createTemporaryTopic();

      }

      @Override
      protected void initProducer() throws JMSException {
         this.producer = session.createProducer(new ActiveMQQueue(QUEUE_NAME));
      }

      @Override
      protected void initConsumer() throws JMSException {
         this.consumer = session.createConsumer(tempDest);
         LOG.info("consumer for: " + tempDest + ", " + consumer);

      }

   }

   class EchoRespondent extends MessageClient implements Runnable {

      public EchoRespondent(ActiveMQConnectionFactory factory) throws Exception {
         super(factory, RANDOM_SLEEP_FOR_RESPONDENT_MS);
      }

      @Override
      public void run() {
         try {
            LOG.info("RESPONDENT LISTENING");
            while (!shutdown.get()) {
               Message incomingMessage = consumer.receive(1000);
               if (incomingMessage instanceof TextMessage) {
                  ActiveMQTextMessage textMessage = (ActiveMQTextMessage) incomingMessage;
                  try {
                     LOG.info("RESPONDENT: Received a message: [" + textMessage.getText() + "]" + Arrays.asList(textMessage.getBrokerPath()));
                     Message message = session.createTextMessage("reply: " + textMessage.getText());
                     Destination replyTo = incomingMessage.getJMSReplyTo();
                     TimeUnit.MILLISECONDS.sleep(timeToSleep);
                     consumerDemandExists.await(5, TimeUnit.SECONDS);
                     try {
                        producer.send(replyTo, message);
                        LOG.info("RESPONDENT: sent reply:" + message.getJMSMessageID() + " back to: " + replyTo);
                     } catch (JMSException e) {
                        LOG.error("RESPONDENT: could not send reply message: " + e.getLocalizedMessage(), e);
                        respondentSendError.incrementAndGet();
                     }
                  } catch (JMSException e) {
                     LOG.error("RESPONDENT: could not create the reply message: " + e.getLocalizedMessage(), e);
                  } catch (InterruptedException e) {
                     LOG.info("RESPONDENT could not generate a random number");
                  }
               }
            }
         } catch (JMSException e) {
            LOG.info("RESPONDENT: Could not set the message listener on the respondent");
         }
      }

      @Override
      protected void initProducer() throws JMSException {
         this.producer = session.createProducer(null);
         // so that we can get an advisory on sending with no consumers
         this.producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
      }

      @Override
      protected void initConsumer() throws JMSException {
         this.consumer = session.createConsumer(new ActiveMQQueue(QUEUE_NAME));
      }
   }
}
