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

import static org.junit.Assert.assertTrue;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import javax.jms.Connection;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;

import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.command.ActiveMQDestination;
import org.apache.activemq.network.DiscoveryNetworkConnector;
import org.apache.activemq.network.NetworkConnector;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AMQ3274Test {

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

   protected static int Next_broker_num = 0;
   protected EmbeddedTcpBroker broker1;
   protected EmbeddedTcpBroker broker2;

   protected int nextEchoId = 0;
   protected boolean testError = false;

   protected int echoResponseFill = 0; // Number of "filler" response messages per request

   public AMQ3274Test() throws Exception {
      broker1 = new EmbeddedTcpBroker();
      broker2 = new EmbeddedTcpBroker();

      broker1.coreConnectTo(broker2, true);
      broker2.coreConnectTo(broker1, true);
   }

   public void logMessage(String msg) {
      System.out.println(msg);
      System.out.flush();
   }

   public void testMessages(Session sess,
                            MessageProducer req_prod,
                            Destination resp_dest,
                            int num_msg) throws Exception {
      MessageConsumer resp_cons;
      TextMessage msg;
      MessageClient cons_client;
      int cur;
      int tot_expected;

      resp_cons = sess.createConsumer(resp_dest);

      cons_client = new MessageClient(resp_cons, num_msg);
      cons_client.start();

      cur = 0;
      while ((cur < num_msg) && (!testError)) {
         msg = sess.createTextMessage("MSG AAAA " + cur);
         msg.setIntProperty("SEQ", 100 + cur);
         msg.setStringProperty("TEST", "TOPO");
         msg.setJMSReplyTo(resp_dest);

         if (cur == (num_msg - 1))
            msg.setBooleanProperty("end-of-response", true);

         req_prod.send(msg);

         cur++;
      }

      cons_client.waitShutdown(5000);

      if (cons_client.shutdown()) {
         LOG.debug("Consumer client shutdown complete");
      }
      else {
         LOG.debug("Consumer client shutdown incomplete!!!");
      }

      tot_expected = num_msg * (echoResponseFill + 1);

      if (cons_client.getNumMsgReceived() == tot_expected) {
         LOG.info("Have " + tot_expected + " messages, as-expected");
      }
      else {
         LOG.error("Have " + cons_client.getNumMsgReceived() + " messages; expected " + tot_expected);
         testError = true;
      }

      resp_cons.close();
   }

   /**
    * Test one destination between the given "producer broker" and
    * "consumer broker" specified.
    */
   public void testOneDest(Connection conn,
                           Session sess,
                           Destination cons_dest,
                           String prod_broker_url,
                           String cons_broker_url,
                           int num_msg) throws Exception {
      int echo_id;

      EchoService echo_svc;
      String echo_queue_name;
      Destination prod_dest;
      MessageProducer msg_prod;

      synchronized (this) {
         echo_id = this.nextEchoId;
         this.nextEchoId++;
      }

      echo_queue_name = "echo.queue." + echo_id;

      LOG.trace("destroying the echo queue in case an old one exists");
      removeQueue(conn, echo_queue_name);

      echo_svc = new EchoService(echo_queue_name, prod_broker_url);
      echo_svc.start();

      LOG.trace("Creating echo queue and producer");
      prod_dest = sess.createQueue(echo_queue_name);
      msg_prod = sess.createProducer(prod_dest);

      testMessages(sess, msg_prod, cons_dest, num_msg);

      echo_svc.shutdown();
      msg_prod.close();
   }

   /**
    * TEST TEMPORARY TOPICS
    */
   public void testTempTopic(String prod_broker_url, String cons_broker_url) throws Exception {
      Connection conn;
      Session sess;
      Destination cons_dest;
      int num_msg;

      num_msg = 5;

      LOG.info("TESTING TEMP TOPICS " + prod_broker_url + " -> " + cons_broker_url + " (" + num_msg + " messages)");

      conn = createConnection(cons_broker_url);
      conn.start();
      sess = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);

      LOG.trace("Creating destination");
      cons_dest = sess.createTemporaryTopic();

      testOneDest(conn, sess, cons_dest, prod_broker_url, cons_broker_url, num_msg);

      sess.close();
      conn.close();
   }

   /**
    * TEST TOPICS
    */
   public void testTopic(String prod_broker_url, String cons_broker_url) throws Exception {
      int num_msg;

      Connection conn;
      Session sess;
      String topic_name;

      Destination cons_dest;

      num_msg = 5;

      LOG.info("TESTING TOPICS " + prod_broker_url + " -> " + cons_broker_url + " (" + num_msg + " messages)");

      conn = createConnection(cons_broker_url);
      conn.start();
      sess = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);

      topic_name = "topotest2.perm.topic";
      LOG.trace("Removing existing Topic");
      removeTopic(conn, topic_name);
      LOG.trace("Creating Topic, " + topic_name);
      cons_dest = sess.createTopic(topic_name);

      testOneDest(conn, sess, cons_dest, prod_broker_url, cons_broker_url, num_msg);

      removeTopic(conn, topic_name);
      sess.close();
      conn.close();
   }

   /**
    * TEST TEMPORARY QUEUES
    */
   public void testTempQueue(String prod_broker_url, String cons_broker_url) throws Exception {
      int num_msg;

      Connection conn;
      Session sess;

      Destination cons_dest;

      num_msg = 5;

      LOG.info("TESTING TEMP QUEUES " + prod_broker_url + " -> " + cons_broker_url + " (" + num_msg + " messages)");

      conn = createConnection(cons_broker_url);
      conn.start();
      sess = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);

      LOG.trace("Creating destination");
      cons_dest = sess.createTemporaryQueue();

      testOneDest(conn, sess, cons_dest, prod_broker_url, cons_broker_url, num_msg);

      sess.close();
      conn.close();
   }

   /**
    * TEST QUEUES
    */
   public void testQueue(String prod_broker_url, String cons_broker_url) throws Exception {
      int num_msg;

      Connection conn;
      Session sess;
      String queue_name;

      Destination cons_dest;

      num_msg = 5;

      LOG.info("TESTING QUEUES " + prod_broker_url + " -> " + cons_broker_url + " (" + num_msg + " messages)");

      conn = createConnection(cons_broker_url);
      conn.start();
      sess = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);

      queue_name = "topotest2.perm.queue";
      LOG.trace("Removing existing Queue");
      removeQueue(conn, queue_name);
      LOG.trace("Creating Queue, " + queue_name);
      cons_dest = sess.createQueue(queue_name);

      testOneDest(conn, sess, cons_dest, prod_broker_url, cons_broker_url, num_msg);

      removeQueue(conn, queue_name);
      sess.close();
      conn.close();
   }

   @Test
   public void run() throws Exception {
      Thread start1;
      Thread start2;

      testError = false;

      // Use threads to avoid startup deadlock since the first broker started waits until
      // it knows the name of the remote broker before finishing its startup, which means
      // the remote must already be running.

      start1 = new Thread() {
         public void run() {
            try {
               broker1.start();
            }
            catch (Exception ex) {
               LOG.error(null, ex);
            }
         }
      };

      start2 = new Thread() {
         public void run() {
            try {
               broker2.start();
            }
            catch (Exception ex) {
               LOG.error(null, ex);
            }
         }
      };

      start1.start();
      start2.start();

      start1.join();
      start2.join();

      if (!testError) {
         this.testTempTopic(broker1.getConnectionUrl(), broker2.getConnectionUrl());
      }
      if (!testError) {
         this.testTempQueue(broker1.getConnectionUrl(), broker2.getConnectionUrl());
      }
      if (!testError) {
         this.testTopic(broker1.getConnectionUrl(), broker2.getConnectionUrl());
      }
      if (!testError) {
         this.testQueue(broker1.getConnectionUrl(), broker2.getConnectionUrl());
      }
      Thread.sleep(100);

      shutdown();

      assertTrue(!testError);
   }

   public void shutdown() throws Exception {
      broker1.stop();
      broker2.stop();
   }

   /**
    * @param args the command line arguments
    */
   public static void main(String[] args) {
      AMQ3274Test main_obj;

      try {
         main_obj = new AMQ3274Test();
         main_obj.run();
      }
      catch (Exception ex) {
         ex.printStackTrace();
         LOG.error(null, ex);
         System.exit(0);
      }
   }

   protected Connection createConnection(String url) throws Exception {
      return org.apache.activemq.ActiveMQConnection.makeConnection(url);
   }

   protected static void removeQueue(Connection conn, String dest_name) throws java.lang.Exception {
      org.apache.activemq.command.ActiveMQDestination dest;

      if (conn instanceof org.apache.activemq.ActiveMQConnection) {
         dest = org.apache.activemq.command.ActiveMQDestination.createDestination(dest_name, (byte) org.apache.activemq.command.ActiveMQDestination.QUEUE_TYPE);
         ((org.apache.activemq.ActiveMQConnection) conn).destroyDestination(dest);
      }
   }

   protected static void removeTopic(Connection conn, String dest_name) throws java.lang.Exception {
      org.apache.activemq.command.ActiveMQDestination dest;

      if (conn instanceof org.apache.activemq.ActiveMQConnection) {
         dest = org.apache.activemq.command.ActiveMQDestination.createDestination(dest_name, (byte) org.apache.activemq.command.ActiveMQDestination.TOPIC_TYPE);
         ((org.apache.activemq.ActiveMQConnection) conn).destroyDestination(dest);
      }
   }

   @SuppressWarnings("rawtypes")
   public static String fmtMsgInfo(Message msg) throws Exception {
      StringBuilder msg_desc;
      String prop;
      Enumeration prop_enum;

      msg_desc = new StringBuilder();
      msg_desc = new StringBuilder();

      if (msg instanceof TextMessage) {
         msg_desc.append(((TextMessage) msg).getText());
      }
      else {
         msg_desc.append("[");
         msg_desc.append(msg.getClass().getName());
         msg_desc.append("]");
      }

      prop_enum = msg.getPropertyNames();
      while (prop_enum.hasMoreElements()) {
         prop = (String) prop_enum.nextElement();
         msg_desc.append("; ");
         msg_desc.append(prop);
         msg_desc.append("=");
         msg_desc.append(msg.getStringProperty(prop));
      }

      return msg_desc.toString();
   }

   // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
   // /////////////////////////////////////////////// INTERNAL CLASSES
   // /////////////////////////////////////////////////
   // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

   protected class EmbeddedTcpBroker {

      protected BrokerService brokerSvc;
      protected int brokerNum;
      protected String brokerName;
      protected String brokerId;
      protected int port;
      protected String tcpUrl;

      public EmbeddedTcpBroker() throws Exception {
         brokerSvc = new BrokerService();

         synchronized (this.getClass()) {
            brokerNum = Next_broker_num;
            Next_broker_num++;
         }

         brokerName = "broker" + brokerNum;
         brokerId = "b" + brokerNum;

         brokerSvc.setBrokerName(brokerName);
         brokerSvc.setBrokerId(brokerId);
         brokerSvc.setPersistent(false);
         brokerSvc.setUseJmx(false);
         tcpUrl = brokerSvc.addConnector("tcp://localhost:0").getPublishableConnectString();
      }

      public Connection createConnection() throws URISyntaxException, JMSException {
         Connection result;

         result = org.apache.activemq.ActiveMQConnection.makeConnection(this.tcpUrl);

         return result;
      }

      public String getConnectionUrl() {
         return this.tcpUrl;
      }

      /**
       * Create network connections to the given broker using the
       * network-connector configuration of CORE brokers (e.g.
       * core1.bus.dev1.coresys.tmcs)
       *
       * @param other
       * @param duplex_f
       */
      public void coreConnectTo(EmbeddedTcpBroker other, boolean duplex_f) throws Exception {
         this.makeConnectionTo(other, duplex_f, true);
         this.makeConnectionTo(other, duplex_f, false);
      }

      public void start() throws Exception {
         brokerSvc.start();
      }

      public void stop() throws Exception {
         brokerSvc.stop();
      }

      /**
       * Make one connection to the other embedded broker, of the specified
       * type (queue or topic) using the standard CORE broker networking.
       *
       * @param other
       * @param duplex_f
       * @param queue_f
       * @throws Exception
       */
      protected void makeConnectionTo(EmbeddedTcpBroker other, boolean duplex_f, boolean queue_f) throws Exception {
         NetworkConnector nw_conn;
         String prefix;
         ActiveMQDestination excl_dest;
         ArrayList<ActiveMQDestination> excludes;

         nw_conn = new DiscoveryNetworkConnector(new URI("static:(" + other.tcpUrl + ")"));
         nw_conn.setDuplex(duplex_f);

         if (queue_f)
            nw_conn.setConduitSubscriptions(false);
         else
            nw_conn.setConduitSubscriptions(true);

         nw_conn.setNetworkTTL(5);
         nw_conn.setSuppressDuplicateQueueSubscriptions(true);
         nw_conn.setDecreaseNetworkConsumerPriority(true);
         nw_conn.setBridgeTempDestinations(true);

         if (queue_f) {
            prefix = "queue";
            excl_dest = ActiveMQDestination.createDestination(">", ActiveMQDestination.QUEUE_TYPE);
         }
         else {
            prefix = "topic";
            excl_dest = ActiveMQDestination.createDestination(">", ActiveMQDestination.TOPIC_TYPE);
         }

         excludes = new ArrayList<ActiveMQDestination>();
         excludes.add(excl_dest);
         nw_conn.setExcludedDestinations(excludes);

         if (duplex_f)
            nw_conn.setName(this.brokerId + "<-" + prefix + "->" + other.brokerId);
         else
            nw_conn.setName(this.brokerId + "-" + prefix + "->" + other.brokerId);

         brokerSvc.addNetworkConnector(nw_conn);
      }
   }

   protected class MessageClient extends java.lang.Thread {

      protected MessageConsumer msgCons;
      protected boolean shutdownInd;
      protected int expectedCount;
      protected int lastSeq = 0;
      protected int msgCount = 0;
      protected boolean haveFirstSeq;
      protected CountDownLatch shutdownLatch;

      public MessageClient(MessageConsumer cons, int num_to_expect) {
         msgCons = cons;
         expectedCount = (num_to_expect * (echoResponseFill + 1));
         shutdownLatch = new CountDownLatch(1);
      }

      public void run() {
         CountDownLatch latch;

         try {
            synchronized (this) {
               latch = shutdownLatch;
            }

            shutdownInd = false;
            processMessages();

            latch.countDown();
         }
         catch (Exception exc) {
            LOG.error("message client error", exc);
         }
      }

      public void waitShutdown(long timeout) {
         CountDownLatch latch;

         try {
            synchronized (this) {
               latch = shutdownLatch;
            }

            if (latch != null)
               latch.await(timeout, TimeUnit.MILLISECONDS);
            else
               LOG.info("echo client shutdown: client does not appear to be active");
         }
         catch (InterruptedException int_exc) {
            LOG.warn("wait for message client shutdown interrupted", int_exc);
         }
      }

      public boolean shutdown() {
         boolean down_ind;

         if (!shutdownInd) {
            shutdownInd = true;
         }

         waitShutdown(200);

         synchronized (this) {
            if ((shutdownLatch == null) || (shutdownLatch.getCount() == 0))
               down_ind = true;
            else
               down_ind = false;
         }

         return down_ind;
      }

      public int getNumMsgReceived() {
         return msgCount;
      }

      protected void processMessages() throws Exception {
         Message in_msg;

         haveFirstSeq = false;
         while ((!shutdownInd) && (!testError)) {
            in_msg = msgCons.receive(100);

            if (in_msg != null) {
               msgCount++;
               checkMessage(in_msg);
            }
         }
      }

      protected void checkMessage(Message in_msg) throws Exception {
         int seq;

         LOG.debug("received message " + fmtMsgInfo(in_msg));

         if (in_msg.propertyExists("SEQ")) {
            seq = in_msg.getIntProperty("SEQ");

            if ((haveFirstSeq) && (seq != (lastSeq + 1))) {
               LOG.error("***ERROR*** incorrect sequence number; expected " + Integer.toString(lastSeq + 1) + " but have " + Integer.toString(seq));

               testError = true;
            }

            lastSeq = seq;

            if (msgCount > expectedCount) {
               LOG.warn("*** have more messages than expected; have " + msgCount + "; expect " + expectedCount);

               testError = true;
            }
         }

         if (in_msg.propertyExists("end-of-response")) {
            LOG.trace("received end-of-response message");
            shutdownInd = true;
         }
      }
   }

   protected class EchoService extends java.lang.Thread {

      protected String destName;
      protected Connection jmsConn;
      protected Session sess;
      protected MessageConsumer msg_cons;
      protected boolean Shutdown_ind;

      protected Destination req_dest;
      protected Destination resp_dest;
      protected MessageProducer msg_prod;

      protected CountDownLatch waitShutdown;

      public EchoService(String dest, Connection broker_conn) throws Exception {
         destName = dest;
         jmsConn = broker_conn;

         Shutdown_ind = false;

         sess = jmsConn.createSession(false, Session.AUTO_ACKNOWLEDGE);
         req_dest = sess.createQueue(destName);
         msg_cons = sess.createConsumer(req_dest);

         jmsConn.start();

         waitShutdown = new CountDownLatch(1);
      }

      public EchoService(String dest, String broker_url) throws Exception {
         this(dest, ActiveMQConnection.makeConnection(broker_url));
      }

      public void run() {
         Message req;

         try {
            LOG.info("STARTING ECHO SERVICE");

            while (!Shutdown_ind) {
               req = msg_cons.receive(100);
               if (req != null) {
                  if (LOG.isDebugEnabled())
                     LOG.debug("ECHO request message " + req.toString());

                  resp_dest = req.getJMSReplyTo();
                  if (resp_dest != null) {
                     msg_prod = sess.createProducer(resp_dest);
                     msg_prod.send(req);
                     msg_prod.close();
                     msg_prod = null;
                  }
                  else {
                     LOG.warn("invalid request: no reply-to destination given");
                  }
               }
            }
         }
         catch (Exception ex) {
            LOG.error(null, ex);
         }
         finally {
            LOG.info("shutting down test echo service");

            try {
               jmsConn.stop();
            }
            catch (javax.jms.JMSException jms_exc) {
               LOG.warn("error on shutting down JMS connection", jms_exc);
            }

            synchronized (this) {
               waitShutdown.countDown();
            }
         }
      }

      /**
       * Shut down the service, waiting up to 3 seconds for the service to
       * terminate.
       */
      public void shutdown() {
         CountDownLatch wait_l;

         synchronized (this) {
            wait_l = waitShutdown;
         }

         Shutdown_ind = true;

         try {
            if (wait_l != null) {
               if (wait_l.await(3000, TimeUnit.MILLISECONDS)) {
                  LOG.info("echo service shutdown complete");
               }
               else {
                  LOG.warn("timeout waiting for echo service shutdown");
               }
            }
            else {
               LOG.info("echo service shutdown: service does not appear to be active");
            }
         }
         catch (InterruptedException int_exc) {
            LOG.warn("interrupted while waiting for echo service shutdown");
         }
      }
   }
}
