/**
 * 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 static org.junit.Assert.fail;

import java.util.ArrayList;

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

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.broker.BrokerService;
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.command.ActiveMQMessage;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * Test the loss of messages detected during testing with ActiveMQ 5.4.1 and 5.4.2.
 * <br>
 * Symptoms: - 1 record is lost "early" in the stream. - no more records lost.
 * <br>
 * Test Configuration: - Broker Settings: - Destination Policy - Occurs with "Destination Policy" using Store Cursor and
 * a memory limit - Not reproduced without "Destination Policy" defined - Persistence Adapter - Memory: Does not occur.
 * - KahaDB: Occurs. - Messages - Occurs with TextMessage and BinaryMessage - Persistent messages.
 * <br>
 * Notes: - Lower memory limits increase the rate of occurrence. - Higher memory limits may prevent the problem
 * (probably because memory limits not reached). - Producers sending a number of messages before consumers come online
 * increases rate of occurrence.
 */

public class AMQ3167Test {

   protected BrokerService embeddedBroker;

   protected static final int MEMORY_LIMIT = 16 * 1024;

   protected static boolean Debug_f = false;

   protected long Producer_stop_time = 0;
   protected long Consumer_stop_time = 0;
   protected long Consumer_startup_delay_ms = 2000;
   protected boolean Stop_after_error = true;

   protected Connection JMS_conn;
   protected long Num_error = 0;

   // // ////
   // // UTILITIES ////
   // // ////

   /**
    * Create a new, unsecured, client connection to the test broker using the given username and password. This
    * connection bypasses all security.
    * <br>
    * Don't forget to start the connection or no messages will be received by consumers even though producers will work
    * fine.
    *
    * @username name of the JMS user for the connection; may be null.
    * @password Password for the JMS user; may be null.
    */

   protected Connection createUnsecuredConnection(String username, String password) throws javax.jms.JMSException {
      ActiveMQConnectionFactory conn_fact;

      conn_fact = new ActiveMQConnectionFactory(embeddedBroker.getVmConnectorURI());

      return conn_fact.createConnection(username, password);
   }

   // // ////
   // // TEST FUNCTIONALITY ////
   // // ////

   @Before
   public void testPrep() throws Exception {
      embeddedBroker = new BrokerService();
      configureBroker(embeddedBroker);
      embeddedBroker.start();
      embeddedBroker.waitUntilStarted();

      // Prepare the connection
      JMS_conn = createUnsecuredConnection(null, null);
      JMS_conn.start();
   }

   @After
   public void testCleanup() throws java.lang.Exception {
      JMS_conn.stop();
      embeddedBroker.stop();
   }

   protected void configureBroker(BrokerService broker_svc) throws Exception {

      broker_svc.setBrokerName("testbroker1");

      broker_svc.setUseJmx(false);
      broker_svc.setPersistent(true);
      broker_svc.setDataDirectory("target/AMQ3167Test");
      configureDestinationPolicy(broker_svc);
   }

   /**
    * NOTE: overrides any prior policy map defined for the broker service.
    */

   protected void configureDestinationPolicy(BrokerService broker_svc) {
      PolicyMap pol_map;
      PolicyEntry pol_ent;
      ArrayList<PolicyEntry> ent_list;

      ent_list = new ArrayList<PolicyEntry>();

      //
      // QUEUES
      //

      pol_ent = new PolicyEntry();
      pol_ent.setQueue(">");
      pol_ent.setMemoryLimit(MEMORY_LIMIT);
      pol_ent.setProducerFlowControl(false);
      ent_list.add(pol_ent);

      //
      // COMPLETE POLICY MAP
      //

      pol_map = new PolicyMap();
      pol_map.setPolicyEntries(ent_list);

      broker_svc.setDestinationPolicy(pol_map);
   }

   // // ////
   // // TEST ////
   // // ////

   @Test
   public void testQueueLostMessage() throws Exception {
      Destination dest;

      dest = ActiveMQDestination.createDestination("lostmsgtest.queue", ActiveMQDestination.QUEUE_TYPE);

      // 10 seconds from now
      Producer_stop_time = java.lang.System.nanoTime() + (10L * 1000000000L);

      // 15 seconds from now
      Consumer_stop_time = Producer_stop_time + (5L * 1000000000L);

      runLostMsgTest(dest, 1000000, 1, 1, false);

      // Make sure failures in the threads are thoroughly reported in the JUnit framework.
      assertTrue(Num_error == 0);
   }

   /**
    *
    */

   protected static void log(String msg) {
      if (Debug_f)
         java.lang.System.err.println(msg);
   }

   /**
    * Main body of the lost-message test.
    */

   protected void runLostMsgTest(Destination dest,
                                 int num_msg,
                                 int num_send_per_sess,
                                 int num_recv_per_sess,
                                 boolean topic_f) throws Exception {
      Thread prod_thread;
      Thread cons_thread;
      String tag;
      Session sess;
      MessageProducer prod;
      MessageConsumer cons;
      int ack_mode;

      //
      // Start the producer
      //

      tag = "prod";
      log(">> Starting producer " + tag);

      sess = JMS_conn.createSession((num_send_per_sess > 1), Session.AUTO_ACKNOWLEDGE);
      prod = sess.createProducer(dest);

      prod_thread = new producerThread(sess, prod, tag, num_msg, num_send_per_sess);
      prod_thread.start();
      log("Started producer " + tag);

      //
      // Delay before starting consumers
      //

      log("Waiting before starting consumers");
      java.lang.Thread.sleep(Consumer_startup_delay_ms);

      //
      // Now create and start the consumer
      //

      tag = "cons";
      log(">> Starting consumer");

      if (num_recv_per_sess > 1)
         ack_mode = Session.CLIENT_ACKNOWLEDGE;
      else
         ack_mode = Session.AUTO_ACKNOWLEDGE;

      sess = JMS_conn.createSession(false, ack_mode);
      cons = sess.createConsumer(dest);

      cons_thread = new consumerThread(sess, cons, tag, num_msg, num_recv_per_sess);
      cons_thread.start();
      log("Started consumer " + tag);

      //
      // Wait for the producer and consumer to finish.
      //

      log("< waiting for producer.");
      prod_thread.join();

      log("< waiting for consumer.");
      cons_thread.join();

      log("Shutting down");
   }

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

   /**
    * Producer thread - runs a single producer until the maximum number of messages is sent, the producer stop time is
    * reached, or a test error is detected.
    */

   protected class producerThread extends Thread {

      protected Session msgSess;
      protected MessageProducer msgProd;
      protected String producerTag;
      protected int numMsg;
      protected int numPerSess;
      protected long producer_stop_time;

      producerThread(Session sess, MessageProducer prod, String tag, int num_msg, int sess_size) {
         super();

         producer_stop_time = 0;
         msgSess = sess;
         msgProd = prod;
         producerTag = tag;
         numMsg = num_msg;
         numPerSess = sess_size;
      }

      public void execTest() throws Exception {
         Message msg;
         int sess_start;
         int cur;

         sess_start = 0;
         cur = 0;
         while ((cur < numMsg) && (!didTimeOut()) && ((!Stop_after_error) || (Num_error == 0))) {
            msg = msgSess.createTextMessage("test message from " + producerTag);
            msg.setStringProperty("testprodtag", producerTag);
            msg.setIntProperty("seq", cur);

            if (msg instanceof ActiveMQMessage) {
               ((ActiveMQMessage) msg).setResponseRequired(true);
            }

            //
            // Send the message.
            //

            msgProd.send(msg);
            cur++;

            //
            // Commit if the number of messages per session has been reached, and
            // transactions are being used (only when > 1 msg per sess).
            //

            if ((numPerSess > 1) && ((cur - sess_start) >= numPerSess)) {
               msgSess.commit();
               sess_start = cur;
            }
         }

         // Make sure to send the final commit, if there were sends since the last commit.
         if ((numPerSess > 1) && ((cur - sess_start) > 0))
            msgSess.commit();

         if (cur < numMsg)
            log("* Producer " + producerTag + " timed out at " + java.lang.System.nanoTime() + " (stop time " + producer_stop_time + ")");
      }

      /**
       * Check whether it is time for the producer to terminate.
       */

      protected boolean didTimeOut() {
         if ((Producer_stop_time > 0) && (java.lang.System.nanoTime() >= Producer_stop_time))
            return true;

         return false;
      }

      /**
       * Run the producer.
       */

      @Override
      public void run() {
         try {
            log("- running producer " + producerTag);
            execTest();
            log("- finished running producer " + producerTag);
         }
         catch (Throwable thrown) {
            Num_error++;
            fail("producer " + producerTag + " failed: " + thrown.getMessage());
            throw new Error("producer " + producerTag + " failed", thrown);
         }
      }

      @Override
      public String toString() {
         return producerTag;
      }
   }

   /**
    * Producer thread - runs a single consumer until the maximum number of messages is received, the consumer stop time
    * is reached, or a test error is detected.
    */

   protected class consumerThread extends Thread {

      protected Session msgSess;
      protected MessageConsumer msgCons;
      protected String consumerTag;
      protected int numMsg;
      protected int numPerSess;

      consumerThread(Session sess, MessageConsumer cons, String tag, int num_msg, int sess_size) {
         super();

         msgSess = sess;
         msgCons = cons;
         consumerTag = tag;
         numMsg = num_msg;
         numPerSess = sess_size;
      }

      public void execTest() throws Exception {
         Message msg;
         int sess_start;
         int cur;

         msg = null;
         sess_start = 0;
         cur = 0;

         while ((cur < numMsg) && (!didTimeOut()) && ((!Stop_after_error) || (Num_error == 0))) {
            //
            // Use a timeout of 1 second to periodically check the consumer timeout.
            //
            msg = msgCons.receive(1000);
            if (msg != null) {
               checkMessage(msg, cur);
               cur++;

               if ((numPerSess > 1) && ((cur - sess_start) >= numPerSess)) {
                  msg.acknowledge();
                  sess_start = cur;
               }
            }
         }

         // Acknowledge the last messages, if they were not yet acknowledged.
         if ((numPerSess > 1) && ((cur - sess_start) > 0))
            msg.acknowledge();

         if (cur < numMsg)
            log("* Consumer " + consumerTag + " timed out");
      }

      /**
       * Check whether it is time for the consumer to terminate.
       */

      protected boolean didTimeOut() {
         if ((Consumer_stop_time > 0) && (java.lang.System.nanoTime() >= Consumer_stop_time))
            return true;

         return false;
      }

      /**
       * Verify the message received. Sequence numbers are checked and are expected to exactly match the message
       * number (starting at 0).
       */

      protected void checkMessage(Message msg, int exp_seq) throws javax.jms.JMSException {
         int seq;

         seq = msg.getIntProperty("seq");

         if (exp_seq != seq) {
            Num_error++;
            fail("*** Consumer " + consumerTag + " expected seq " + exp_seq + "; received " + seq);
         }
      }

      /**
       * Run the consumer.
       */

      @Override
      public void run() {
         try {
            log("- running consumer " + consumerTag);
            execTest();
            log("- running consumer " + consumerTag);
         }
         catch (Throwable thrown) {
            Num_error++;
            fail("consumer " + consumerTag + " failed: " + thrown.getMessage());
            throw new Error("consumer " + consumerTag + " failed", thrown);
         }
      }

      @Override
      public String toString() {
         return consumerTag;
      }
   }
}