/**
 * 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.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.Topic;
import javax.jms.TopicSubscriber;
import javax.management.ObjectName;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import junit.framework.Test;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.CombinationTestSupport;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.jmx.DestinationViewMBean;
import org.apache.activemq.broker.region.policy.FilePendingQueueMessageStoragePolicy;
import org.apache.activemq.broker.region.policy.PendingQueueMessageStoragePolicy;
import org.apache.activemq.broker.region.policy.PolicyEntry;
import org.apache.activemq.broker.region.policy.PolicyMap;
import org.apache.activemq.broker.region.policy.VMPendingQueueMessageStoragePolicy;
import org.apache.activemq.command.ActiveMQDestination;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ActiveMQTopic;
import org.apache.activemq.util.Wait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ExpiredMessagesWithNoConsumerTest extends CombinationTestSupport {

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

   private final ActiveMQDestination destination = new ActiveMQQueue("test");

   private boolean optimizedDispatch = true;
   private PendingQueueMessageStoragePolicy pendingQueuePolicy;

   private BrokerService broker;
   private String connectionUri;
   private Connection connection;
   private Session session;
   private MessageProducer producer;

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

   public static void main(String[] args) {
      junit.textui.TestRunner.run(suite());
   }

   protected void createBrokerWithMemoryLimit() throws Exception {
      createBrokerWithMemoryLimit(800);
   }

   protected void createBrokerWithMemoryLimit(int expireMessagesPeriod) throws Exception {
      doCreateBroker(true, expireMessagesPeriod);
   }

   protected void createBroker() throws Exception {
      doCreateBroker(false, 800);
   }

   private void doCreateBroker(boolean memoryLimit, int expireMessagesPeriod) throws Exception {
      broker = new BrokerService();
      broker.setBrokerName("localhost");
      broker.setUseJmx(true);
      broker.setDeleteAllMessagesOnStartup(true);
      broker.addConnector("tcp://localhost:0");

      PolicyMap policyMap = new PolicyMap();
      PolicyEntry defaultEntry = new PolicyEntry();
      defaultEntry.setOptimizedDispatch(optimizedDispatch);
      defaultEntry.setExpireMessagesPeriod(expireMessagesPeriod);
      defaultEntry.setMaxExpirePageSize(800);

      defaultEntry.setPendingQueuePolicy(pendingQueuePolicy);

      if (memoryLimit) {
         // so memory is not consumed by DLQ turn if off
         defaultEntry.setDeadLetterStrategy(null);
         defaultEntry.setMemoryLimit(200 * 1000);
      }

      policyMap.setDefaultEntry(defaultEntry);

      broker.setDestinationPolicy(policyMap);
      broker.start();
      broker.waitUntilStarted();

      connectionUri = broker.getTransportConnectors().get(0).getPublishableConnectString();
   }

   public void testExpiredNonPersistentMessagesWithNoConsumer() throws Exception {

      createBrokerWithMemoryLimit(2000);

      ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(connectionUri);
      connection = factory.createConnection();
      session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
      producer = session.createProducer(destination);
      producer.setTimeToLive(1000);
      producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
      connection.start();
      final long sendCount = 2000;

      final Thread producingThread = new Thread("Producing Thread") {
         @Override
         public void run() {
            try {
               int i = 0;
               long tStamp = System.currentTimeMillis();
               while (i++ < sendCount) {
                  producer.send(session.createTextMessage("test"));
                  if (i % 100 == 0) {
                     LOG.info("sent: " + i + " @ " + ((System.currentTimeMillis() - tStamp) / 100) + "m/ms");
                     tStamp = System.currentTimeMillis();
                  }

                  if (135 == i) {
                     // allow pending messages to expire, before usage limit kicks in  to flush them
                     TimeUnit.SECONDS.sleep(5);
                  }
               }
            } catch (Throwable ex) {
               ex.printStackTrace();
            }
         }
      };

      producingThread.start();

      assertTrue("producer failed to complete within allocated time", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            producingThread.join(TimeUnit.SECONDS.toMillis(3000));
            return !producingThread.isAlive();
         }
      }));

      TimeUnit.SECONDS.sleep(5);

      final DestinationViewMBean view = createView(destination);
      Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            try {
               LOG.info("enqueue=" + view.getEnqueueCount() + ", dequeue=" + view.getDequeueCount() + ", inflight=" + view.getInFlightCount() + ", expired= " + view.getExpiredCount() + ", size= " + view.getQueueSize());
               return view.getDequeueCount() != 0 && view.getDequeueCount() == view.getExpiredCount() && view.getDequeueCount() == view.getEnqueueCount() && view.getQueueSize() == 0;
            } catch (Exception ignored) {
               LOG.info(ignored.toString());
            }
            return false;
         }
      }, Wait.MAX_WAIT_MILLIS * 10);
      LOG.info("enqueue=" + view.getEnqueueCount() + ", dequeue=" + view.getDequeueCount() + ", inflight=" + view.getInFlightCount() + ", expired= " + view.getExpiredCount() + ", size= " + view.getQueueSize());

      assertEquals("memory usage doesn't go to duck egg", 0, view.getMemoryPercentUsage());
      assertEquals("0 queue", 0, view.getQueueSize());
   }

   public void initCombosForTestExpiredMessagesWithNoConsumer() {
      addCombinationValues("optimizedDispatch", new Object[]{Boolean.TRUE, Boolean.FALSE});
      addCombinationValues("pendingQueuePolicy", new Object[]{null, new VMPendingQueueMessageStoragePolicy(), new FilePendingQueueMessageStoragePolicy()});
   }

   public void testExpiredMessagesWithNoConsumer() throws Exception {

      createBrokerWithMemoryLimit();

      ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(connectionUri);
      connection = factory.createConnection();
      session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
      producer = session.createProducer(destination);
      producer.setTimeToLive(1000);
      connection.start();
      final long sendCount = 2000;

      final Thread producingThread = new Thread("Producing Thread") {
         @Override
         public void run() {
            try {
               int i = 0;
               long tStamp = System.currentTimeMillis();
               while (i++ < sendCount) {
                  producer.send(session.createTextMessage("test"));
                  if (i % 100 == 0) {
                     LOG.info("sent: " + i + " @ " + ((System.currentTimeMillis() - tStamp) / 100) + "m/ms");
                     tStamp = System.currentTimeMillis();
                  }
               }
            } catch (Throwable ex) {
               ex.printStackTrace();
            }
         }
      };

      producingThread.start();

      assertTrue("producer failed to complete within allocated time", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            producingThread.join(TimeUnit.SECONDS.toMillis(3000));
            return !producingThread.isAlive();
         }
      }));

      final DestinationViewMBean view = createView(destination);
      Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            LOG.info("enqueue=" + view.getEnqueueCount() + ", dequeue=" + view.getDequeueCount() + ", inflight=" + view.getInFlightCount() + ", expired= " + view.getExpiredCount() + ", size= " + view.getQueueSize());
            return sendCount == view.getExpiredCount();
         }
      }, Wait.MAX_WAIT_MILLIS * 10);
      LOG.info("enqueue=" + view.getEnqueueCount() + ", dequeue=" + view.getDequeueCount() + ", inflight=" + view.getInFlightCount() + ", expired= " + view.getExpiredCount() + ", size= " + view.getQueueSize());

      assertEquals("Not all sent messages have expired", sendCount, view.getExpiredCount());
      assertEquals("memory usage doesn't go to duck egg", 0, view.getMemoryPercentUsage());
   }

   // first ack delivered after expiry
   public void testExpiredMessagesWithVerySlowConsumer() throws Exception {
      createBroker();
      final long queuePrefetch = 5;
      ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(connectionUri + "?jms.prefetchPolicy.queuePrefetch=" + queuePrefetch);
      connection = factory.createConnection();
      session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
      producer = session.createProducer(destination);
      final int ttl = 4000;
      producer.setTimeToLive(ttl);

      final long sendCount = 10;
      final CountDownLatch receivedOneCondition = new CountDownLatch(1);
      final CountDownLatch waitCondition = new CountDownLatch(1);

      MessageConsumer consumer = session.createConsumer(destination);
      consumer.setMessageListener(new MessageListener() {

         @Override
         public void onMessage(Message message) {
            try {
               LOG.info("Got my message: " + message);
               receivedOneCondition.countDown();
               waitCondition.await(6, TimeUnit.MINUTES);
               LOG.info("acking message: " + message);
               message.acknowledge();
            } catch (Exception e) {
               e.printStackTrace();
               fail(e.toString());
            }
         }
      });

      connection.start();

      final Thread producingThread = new Thread("Producing Thread") {
         @Override
         public void run() {
            try {
               int i = 0;
               long tStamp = System.currentTimeMillis();
               while (i++ < sendCount) {
                  producer.send(session.createTextMessage("test"));
                  if (i % 100 == 0) {
                     LOG.info("sent: " + i + " @ " + ((System.currentTimeMillis() - tStamp) / 100) + "m/ms");
                     tStamp = System.currentTimeMillis();
                  }
               }
            } catch (Throwable ex) {
               ex.printStackTrace();
            }
         }
      };

      producingThread.start();
      assertTrue("got one message", receivedOneCondition.await(20, TimeUnit.SECONDS));

      assertTrue("producer failed to complete within allocated time", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            producingThread.join(1000);
            return !producingThread.isAlive();
         }
      }, Wait.MAX_WAIT_MILLIS * 10));

      final DestinationViewMBean view = createView(destination);

      assertTrue("all dispatched up to default prefetch ", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return queuePrefetch == view.getDispatchCount();
         }
      }));
      assertTrue("all non inflight have expired ", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            LOG.info("enqueue=" + view.getEnqueueCount() + ", dequeue=" + view.getDequeueCount() + ", inflight=" + view.getInFlightCount() + ", expired= " + view.getExpiredCount() + ", size= " + view.getQueueSize());

            return view.getExpiredCount() > 0 && (view.getEnqueueCount() - view.getInFlightCount()) == view.getExpiredCount();
         }
      }));

      LOG.info("enqueue=" + view.getEnqueueCount() + ", dequeue=" + view.getDequeueCount() + ", inflight=" + view.getInFlightCount() + ", expired= " + view.getExpiredCount() + ", size= " + view.getQueueSize());

      // let the ack happen
      waitCondition.countDown();

      Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return 0 == view.getInFlightCount();
         }
      });
      LOG.info("enqueue=" + view.getEnqueueCount() + ", dequeue=" + view.getDequeueCount() + ", inflight=" + view.getInFlightCount() + ", expired= " + view.getExpiredCount() + ", size= " + view.getQueueSize());

      assertEquals("inflight reduced to duck", 0, view.getInFlightCount());
      assertEquals("size didn't get back to 0 ", 0, view.getQueueSize());
      assertEquals("dequeues didn't match sent/expired ", sendCount, view.getDequeueCount());

      consumer.close();

      Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return 0 == view.getInFlightCount();
         }
      });
      assertEquals("inflight goes to zero on close", 0, view.getInFlightCount());

      LOG.info("done: " + getName());
   }

   public void testExpiredMessagesWithVerySlowConsumerCanContinue() throws Exception {
      createBroker();
      final long queuePrefetch = 600;
      ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(connectionUri + "?jms.prefetchPolicy.queuePrefetch=" + queuePrefetch);
      connection = factory.createConnection();
      session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
      producer = session.createProducer(destination);
      final int ttl = 4000;
      producer.setTimeToLive(ttl);

      final long sendCount = 1500;
      final CountDownLatch receivedOneCondition = new CountDownLatch(1);
      final CountDownLatch waitCondition = new CountDownLatch(1);
      final AtomicLong received = new AtomicLong();
      MessageConsumer consumer = session.createConsumer(destination);
      consumer.setMessageListener(new MessageListener() {

         @Override
         public void onMessage(Message message) {
            try {
               if (LOG.isDebugEnabled()) {
                  LOG.debug("Got my message: " + message);
               }
               receivedOneCondition.countDown();
               received.incrementAndGet();
               waitCondition.await(5, TimeUnit.MINUTES);
               if (LOG.isDebugEnabled()) {
                  LOG.debug("acking message: " + message);
               }
               message.acknowledge();
            } catch (Exception e) {
               e.printStackTrace();
               fail(e.toString());
            }
         }
      });

      connection.start();

      final Thread producingThread = new Thread("Producing Thread") {
         @Override
         public void run() {
            try {
               int i = 0;
               long tStamp = System.currentTimeMillis();
               while (i++ < sendCount) {
                  producer.send(session.createTextMessage("test"));
                  if (i % 100 == 0) {
                     LOG.info("sent: " + i + " @ " + ((System.currentTimeMillis() - tStamp) / 100) + "m/ms");
                     tStamp = System.currentTimeMillis();
                  }
               }
            } catch (Throwable ex) {
               ex.printStackTrace();
            }
         }
      };

      producingThread.start();
      assertTrue("got one message", receivedOneCondition.await(20, TimeUnit.SECONDS));

      assertTrue("producer failed to complete within allocated time", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            producingThread.join(1000);
            return !producingThread.isAlive();
         }
      }, Wait.MAX_WAIT_MILLIS * 10));

      final DestinationViewMBean view = createView(destination);

      assertTrue("Not all dispatched up to default prefetch ", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return queuePrefetch == view.getDispatchCount();
         }
      }));

      assertTrue("all non inflight have expired ", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            LOG.info("enqueue=" + view.getEnqueueCount() + ", dequeue=" + view.getDequeueCount() + ", inflight=" + view.getInFlightCount() + ", expired= " + view.getExpiredCount() + ", size= " + view.getQueueSize());

            return view.getExpiredCount() > 0 && (view.getEnqueueCount() - view.getInFlightCount()) == view.getExpiredCount();
         }
      }));

      LOG.info("enqueue=" + view.getEnqueueCount() + ", dequeue=" + view.getDequeueCount() + ", inflight=" + view.getInFlightCount() + ", expired= " + view.getExpiredCount() + ", size= " + view.getQueueSize());

      // let the ack happen
      waitCondition.countDown();

      Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return 0 == view.getInFlightCount();
         }
      });
      LOG.info("enqueue=" + view.getEnqueueCount() + ", dequeue=" + view.getDequeueCount() + ", inflight=" + view.getInFlightCount() + ", expired= " + view.getExpiredCount() + ", size= " + view.getQueueSize());

      assertEquals("inflight didn't reduce to duck", 0, view.getInFlightCount());
      assertEquals("size doesn't get back to 0 ", 0, view.getQueueSize());
      assertEquals("dequeues don't match sent/expired ", sendCount, view.getDequeueCount());

      // produce some more
      producer.setTimeToLive(0);
      long tStamp = System.currentTimeMillis();
      for (int i = 0; i < sendCount; i++) {
         producer.send(session.createTextMessage("test-" + i));
         if (i % 100 == 0) {
            LOG.info("sent: " + i + " @ " + ((System.currentTimeMillis() - tStamp) / 100) + "m/ms");
            tStamp = System.currentTimeMillis();
         }
      }

      Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return received.get() >= sendCount;
         }
      });

      consumer.close();

      Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return 0 == view.getInFlightCount();
         }
      });
      assertEquals("inflight did not go to zero on close", 0, view.getInFlightCount());

      LOG.info("done: " + getName());
   }

   public void testExpireMessagesForDurableSubscriber() throws Exception {
      createBroker();
      ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(connectionUri);
      connection = factory.createConnection();
      connection.setClientID("myConnection");
      session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
      connection.start();
      Topic destination = session.createTopic("test");
      producer = session.createProducer(destination);
      final int ttl = 1000;
      producer.setTimeToLive(ttl);

      final long sendCount = 10;

      TopicSubscriber sub = session.createDurableSubscriber(destination, "mySub");
      sub.close();

      for (int i = 0; i < sendCount; i++) {
         producer.send(session.createTextMessage("test"));
      }

      DestinationViewMBean view = createView((ActiveMQTopic) destination);

      LOG.info("messages sent");
      LOG.info("expired=" + view.getExpiredCount() + " " + view.getEnqueueCount());
      assertEquals(0, view.getExpiredCount());
      assertEquals(10, view.getEnqueueCount());

      Thread.sleep(5000);

      LOG.info("expired=" + view.getExpiredCount() + " " + view.getEnqueueCount());
      assertEquals(10, view.getExpiredCount());
      assertEquals(10, view.getEnqueueCount());

      final AtomicLong received = new AtomicLong();
      sub = session.createDurableSubscriber(destination, "mySub");
      sub.setMessageListener(new MessageListener() {
         @Override
         public void onMessage(Message message) {
            received.incrementAndGet();
         }
      });

      LOG.info("Waiting for messages to arrive");

      Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return received.get() >= sendCount;
         }
      }, 1000);

      LOG.info("received=" + received.get());
      LOG.info("expired=" + view.getExpiredCount() + " " + view.getEnqueueCount());

      assertEquals(0, received.get());
      assertEquals(10, view.getExpiredCount());
      assertEquals(10, view.getEnqueueCount());
   }

   protected DestinationViewMBean createView(ActiveMQDestination destination) throws Exception {
      String domain = "org.apache.activemq";
      ObjectName name;
      if (destination.isQueue()) {
         name = new ObjectName(domain + ":type=Broker,brokerName=localhost,destinationType=Queue,destinationName=test");
      } else {
         name = new ObjectName(domain + ":type=Broker,brokerName=localhost,destinationType=Topic,destinationName=test");
      }

      return (DestinationViewMBean) broker.getManagementContext().newProxyInstance(name, DestinationViewMBean.class, true);
   }

   @Override
   protected void tearDown() throws Exception {
      connection.stop();
      broker.stop();
      broker.waitUntilStopped();
   }

   public boolean getOptimizedDispatch() {
      return this.optimizedDispatch;
   }

   public void setOptimizedDispatch(boolean option) {
      this.optimizedDispatch = option;
   }

   public PendingQueueMessageStoragePolicy getPendingQueuePolicy() {
      return this.pendingQueuePolicy;
   }

   public void setPendingQueuePolicy(PendingQueueMessageStoragePolicy policy) {
      this.pendingQueuePolicy = policy;
   }
}
