/**
 * 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 java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import javax.jms.Connection;
import javax.jms.DeliveryMode;
import javax.jms.ExceptionListener;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;

import org.apache.activemq.ActiveMQSession;
import org.apache.activemq.TestSupport;
import org.apache.activemq.command.ActiveMQQueue;

/**
 * In CLIENT_ACKNOWLEDGE and INDIVIDUAL_ACKNOWLEDGE modes following exception
 * occurs when ASYNCH consumers acknowledges messages in not in order they
 * received the messages.
 * <p>
 * Exception thrown on broker side:
 * <p>
 * {@code javax.jms.JMSException: Could not correlate acknowledgment with
 * dispatched message: MessageAck}
 *
 * @author daroo
 */
public class AMQ2489Test extends TestSupport {

   private final static String SEQ_NUM_PROPERTY = "seqNum";

   private final static int TOTAL_MESSAGES_CNT = 2;
   private final static int CONSUMERS_CNT = 2;

   private final CountDownLatch LATCH = new CountDownLatch(TOTAL_MESSAGES_CNT);

   private Connection connection;

   protected void setUp() throws Exception {
      super.setUp();
      connection = createConnection();
   }

   protected void tearDown() throws Exception {
      if (connection != null) {
         connection.close();
         connection = null;
      }
      super.tearDown();
   }

   public void testUnorderedClientAcknowledge() throws Exception {
      doUnorderedAck(Session.CLIENT_ACKNOWLEDGE);
   }

   public void testUnorderedIndividualAcknowledge() throws Exception {
      doUnorderedAck(ActiveMQSession.INDIVIDUAL_ACKNOWLEDGE);
   }

   /**
    * Main test method
    *
    * @param acknowledgmentMode - ACK mode to be used by consumers
    * @throws Exception
    */
   protected void doUnorderedAck(int acknowledgmentMode) throws Exception {
      List<Consumer> consumers = null;
      Session producerSession = null;

      connection.start();
      // Because exception is thrown on broker side only, let's set up
      // exception listener to get it
      final TestExceptionListener exceptionListener = new TestExceptionListener();
      connection.setExceptionListener(exceptionListener);
      try {
         consumers = new ArrayList<Consumer>();
         // start customers
         for (int i = 0; i < CONSUMERS_CNT; i++) {
            consumers.add(new Consumer(acknowledgmentMode));
         }

         // produce few test messages
         producerSession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
         final MessageProducer producer = producerSession.createProducer(new ActiveMQQueue(getQueueName()));
         producer.setDeliveryMode(DeliveryMode.PERSISTENT);
         for (int i = 0; i < TOTAL_MESSAGES_CNT; i++) {
            final Message message = producerSession.createTextMessage("test");
            // assign each message sequence number
            message.setIntProperty(SEQ_NUM_PROPERTY, i);
            producer.send(message);
         }

         // during each onMessage() calls consumers decreases the LATCH
         // counter.
         //
         // so, let's wait till all messages are consumed.
         //
         LATCH.await();

         // wait a bit more to give exception listener a chance be populated
         // with
         // broker's error
         TimeUnit.SECONDS.sleep(1);

         assertFalse(exceptionListener.getStatusText(), exceptionListener.hasExceptions());

      }
      finally {
         if (producerSession != null)
            producerSession.close();

         if (consumers != null) {
            for (Consumer c : consumers) {
               c.close();
            }
         }
      }
   }

   protected String getQueueName() {
      return getClass().getName() + "." + getName();
   }

   public final class Consumer implements MessageListener {

      final Session session;

      private Consumer(int acknowledgmentMode) {
         try {
            session = connection.createSession(false, acknowledgmentMode);
            final Queue queue = session.createQueue(getQueueName() + "?consumer.prefetchSize=1");
            final MessageConsumer consumer = session.createConsumer(queue);
            consumer.setMessageListener(this);
         }
         catch (JMSException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
         }
      }

      public void onMessage(Message message) {
         try {
            // retrieve sequence number assigned by producer...
            final int seqNum = message.getIntProperty(SEQ_NUM_PROPERTY);

            // ...and let's delay every second message a little bit before
            // acknowledgment
            if ((seqNum % 2) == 0) {
               System.out.println("Delayed message sequence numeber: " + seqNum);
               try {
                  TimeUnit.SECONDS.sleep(1);
               }
               catch (InterruptedException e) {
                  Thread.currentThread().interrupt();
               }
            }

            message.acknowledge();
         }
         catch (JMSException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
         }
         finally {
            // decrease LATCH counter in the main test method.
            LATCH.countDown();
         }
      }

      private void close() {
         if (session != null) {
            try {
               session.close();
            }
            catch (JMSException e) {
               e.printStackTrace();
               throw new RuntimeException(e);
            }
         }
      }
   }

   public final class TestExceptionListener implements ExceptionListener {

      private final java.util.Queue<Exception> exceptions = new ConcurrentLinkedQueue<Exception>();

      public void onException(JMSException e) {
         exceptions.add(e);
      }

      public boolean hasExceptions() {
         return exceptions.isEmpty() == false;
      }

      public String getStatusText() {
         final StringBuilder str = new StringBuilder();
         str.append("Exceptions count on broker side: " + exceptions.size() + ".\nMessages:\n");
         for (Exception e : exceptions) {
            str.append(e.getMessage() + "\n\n");
         }
         return str.toString();
      }
   }
}
