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

import java.util.concurrent.atomic.AtomicInteger;

import javax.jms.Connection;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
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.Session;
import javax.jms.TextMessage;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.util.Wait;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Test for AMQ-3965.
 * A consumer may be stalled in case it uses optimizeAcknowledge and receives
 * a number of messages that expire before being dispatched to application code.
 * See for more details.
 */
public class OptimizeAcknowledgeWithExpiredMsgsTest {

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

   private BrokerService broker = null;

   private String connectionUri;

   /**
    * Creates a broker instance but does not start it.
    *
    * @param brokerUri  - transport uri of broker
    * @param brokerName - name for the broker
    * @return a BrokerService instance with transport uri and broker name set
    * @throws Exception
    */
   protected BrokerService createBroker() throws Exception {
      BrokerService broker = new BrokerService();
      broker.setPersistent(false);
      broker.setDeleteAllMessagesOnStartup(true);
      broker.setUseJmx(false);
      connectionUri = broker.addConnector("tcp://localhost:0").getPublishableConnectString();
      return broker;
   }

   @Before
   public void setUp() throws Exception {
      broker = createBroker();
      broker.start();
      broker.waitUntilStarted();
   }

   @After
   public void tearDown() throws Exception {
      if (broker != null) {
         broker.stop();
         broker.waitUntilStopped();
         broker = null;
      }
   }

   /**
    * Tests for AMQ-3965
    * Creates connection into broker using optimzeAcknowledge and prefetch=100
    * Creates producer and consumer. Producer sends 45 msgs that will expire
    * at consumer (but before being dispatched to app code).
    * Producer then sends 60 msgs without expiry.
    *
    * Consumer receives msgs using a MessageListener and increments a counter.
    * Main thread sleeps for 5 seconds and checks the counter value.
    * If counter != 60 msgs (the number of msgs that should get dispatched
    * to consumer) the test fails.
    */
   @Test
   public void testOptimizedAckWithExpiredMsgs() throws Exception {
      ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(connectionUri + "?jms.optimizeAcknowledge=true&jms.prefetchPolicy.all=100");

      // Create JMS resources
      Connection connection = connectionFactory.createConnection();
      Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
      Destination destination = session.createQueue("TEST.FOO");

      // ***** Consumer code *****
      MessageConsumer consumer = session.createConsumer(destination);

      final MyMessageListener listener = new MyMessageListener();
      connection.setExceptionListener((ExceptionListener) listener);

      // ***** Producer Code *****
      MessageProducer producer = session.createProducer(destination);
      producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

      String text = "Hello world! From: " + Thread.currentThread().getName() + " : " + this.hashCode();
      TextMessage message;

      // Produce msgs that will expire quickly
      for (int i = 0; i < 45; i++) {
         message = session.createTextMessage(text);
         producer.send(message, 1, 1, 100);
         LOG.trace("Sent message: " + message.getJMSMessageID() +
                      " with expiry 10 msec");
      }
      // Produce msgs that don't expire
      for (int i = 0; i < 60; i++) {
         message = session.createTextMessage(text);
         producer.send(message, 1, 1, 60000);
         // producer.send(message);
         LOG.trace("Sent message: " + message.getJMSMessageID() +
                      " with expiry 30 sec");
      }
      consumer.setMessageListener(listener);

      sleep(1000);  // let the batch of 45 expire.

      connection.start();

      assertTrue("Should receive all expected messages, counter at " + listener.getCounter(), Wait.waitFor(new Wait.Condition() {

         @Override
         public boolean isSatisified() throws Exception {
            return listener.getCounter() == 60;
         }
      }));

      LOG.info("Received all expected messages with counter at: " + listener.getCounter());

      // Cleanup
      producer.close();
      consumer.close();
      session.close();
      connection.close();
   }

   @Test
   public void testOptimizedAckWithExpiredMsgsSync() throws Exception {
      ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(connectionUri + "?jms.optimizeAcknowledge=true&jms.prefetchPolicy.all=100");

      // Create JMS resources
      Connection connection = connectionFactory.createConnection();
      connection.start();
      Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
      Destination destination = session.createQueue("TEST.FOO");

      // ***** Consumer code *****
      MessageConsumer consumer = session.createConsumer(destination);

      // ***** Producer Code *****
      MessageProducer producer = session.createProducer(destination);
      producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

      String text = "Hello world! From: " + Thread.currentThread().getName() + " : " + this.hashCode();
      TextMessage message;

      // Produce msgs that will expire quickly
      for (int i = 0; i < 45; i++) {
         message = session.createTextMessage(text);
         producer.send(message, 1, 1, 10);
         LOG.trace("Sent message: " + message.getJMSMessageID() +
                      " with expiry 10 msec");
      }
      // Produce msgs that don't expire
      for (int i = 0; i < 60; i++) {
         message = session.createTextMessage(text);
         producer.send(message, 1, 1, 30000);
         // producer.send(message);
         LOG.trace("Sent message: " + message.getJMSMessageID() +
                      " with expiry 30 sec");
      }
      sleep(200);

      int counter = 1;
      for (; counter <= 60; ++counter) {
         assertNotNull(consumer.receive(2000));
         LOG.info("counter at " + counter);
      }
      LOG.info("Received all expected messages with counter at: " + counter);

      // Cleanup
      producer.close();
      consumer.close();
      session.close();
      connection.close();
   }

   @Test
   public void testOptimizedAckWithExpiredMsgsSync2() throws Exception {
      ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(connectionUri + "?jms.optimizeAcknowledge=true&jms.prefetchPolicy.all=100");

      // Create JMS resources
      Connection connection = connectionFactory.createConnection();
      connection.start();
      Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
      Destination destination = session.createQueue("TEST.FOO");

      // ***** Consumer code *****
      MessageConsumer consumer = session.createConsumer(destination);

      // ***** Producer Code *****
      MessageProducer producer = session.createProducer(destination);
      producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

      String text = "Hello world! From: " + Thread.currentThread().getName() + " : " + this.hashCode();
      TextMessage message;

      // Produce msgs that don't expire
      for (int i = 0; i < 56; i++) {
         message = session.createTextMessage(text);
         producer.send(message, 1, 1, 30000);
         // producer.send(message);
         LOG.trace("Sent message: " + message.getJMSMessageID() +
                      " with expiry 30 sec");
      }
      // Produce msgs that will expire quickly
      for (int i = 0; i < 44; i++) {
         message = session.createTextMessage(text);
         producer.send(message, 1, 1, 10);
         LOG.trace("Sent message: " + message.getJMSMessageID() +
                      " with expiry 10 msec");
      }
      // Produce some moremsgs that don't expire
      for (int i = 0; i < 4; i++) {
         message = session.createTextMessage(text);
         producer.send(message, 1, 1, 30000);
         // producer.send(message);
         LOG.trace("Sent message: " + message.getJMSMessageID() +
                      " with expiry 30 sec");
      }

      sleep(200);

      int counter = 1;
      for (; counter <= 60; ++counter) {
         assertNotNull(consumer.receive(2000));
         LOG.info("counter at " + counter);
      }
      LOG.info("Received all expected messages with counter at: " + counter);

      // Cleanup
      producer.close();
      consumer.close();
      session.close();
      connection.close();
   }

   private void sleep(int milliSecondTime) {
      try {
         Thread.sleep(milliSecondTime);
      }
      catch (InterruptedException igonred) {
      }
   }

   /**
    * Standard JMS MessageListener
    */
   private class MyMessageListener implements MessageListener, ExceptionListener {

      private AtomicInteger counter = new AtomicInteger(0);

      public void onMessage(final Message message) {
         try {
            LOG.trace("Got Message " + message.getJMSMessageID());
            LOG.info("counter at " + counter.incrementAndGet());
         }
         catch (final Exception e) {
         }
      }

      public int getCounter() {
         return counter.get();
      }

      public synchronized void onException(JMSException ex) {
         LOG.error("JMS Exception occurred.  Shutting down client.");
      }
   }
}
