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

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
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.Topic;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.activemq.advisory.AdvisorySupport;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.region.policy.ConstantPendingMessageLimitStrategy;
import org.apache.activemq.broker.region.policy.FilePendingSubscriberMessageStoragePolicy;
import org.apache.activemq.broker.region.policy.OldestMessageEvictionStrategy;
import org.apache.activemq.broker.region.policy.PendingSubscriberMessageStoragePolicy;
import org.apache.activemq.broker.region.policy.PolicyEntry;
import org.apache.activemq.broker.region.policy.PolicyMap;
import org.apache.activemq.broker.region.policy.VMPendingSubscriberMessageStoragePolicy;
import org.apache.activemq.command.ActiveMQDestination;
import org.apache.activemq.command.ActiveMQMessage;
import org.apache.activemq.command.ActiveMQTopic;
import org.apache.activemq.util.Wait;
import org.junit.After;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class MessageEvictionTest {

   static final Logger LOG = LoggerFactory.getLogger(MessageEvictionTest.class);
   private BrokerService broker;
   private ConnectionFactory connectionFactory;
   Connection connection;
   private Session session;
   private Topic destination;
   private final String destinationName = "verifyEvection";
   protected int numMessages = 2000;
   protected String payload = new String(new byte[1024 * 2]);

   public void setUp(PendingSubscriberMessageStoragePolicy pendingSubscriberPolicy) throws Exception {
      broker = createBroker(pendingSubscriberPolicy);
      broker.start();
      connectionFactory = createConnectionFactory();
      connection = connectionFactory.createConnection();
      connection.start();
      session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
      destination = session.createTopic(destinationName);
   }

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

   @Test
   public void testMessageEvictionMemoryUsageFileCursor() throws Exception {
      setUp(new FilePendingSubscriberMessageStoragePolicy());
      doTestMessageEvictionMemoryUsage();
   }

   @Test
   public void testMessageEvictionMemoryUsageVmCursor() throws Exception {
      setUp(new VMPendingSubscriberMessageStoragePolicy());
      doTestMessageEvictionMemoryUsage();
   }

   @Test
   public void testMessageEvictionDiscardedAdvisory() throws Exception {
      setUp(new VMPendingSubscriberMessageStoragePolicy());

      ExecutorService executor = Executors.newSingleThreadExecutor();
      final CountDownLatch consumerRegistered = new CountDownLatch(1);
      final CountDownLatch gotAdvisory = new CountDownLatch(1);
      final CountDownLatch advisoryIsGood = new CountDownLatch(1);

      executor.execute(new Runnable() {
         @Override
         public void run() {
            try {
               ActiveMQTopic discardedAdvisoryDestination = AdvisorySupport.getMessageDiscardedAdvisoryTopic(destination);
               // use separate session rather than asyncDispatch on consumer session
               // as we want consumer session to block
               Session advisorySession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
               final MessageConsumer consumer = advisorySession.createConsumer(discardedAdvisoryDestination);
               consumer.setMessageListener(new MessageListener() {
                  int advisoriesReceived = 0;

                  @Override
                  public void onMessage(Message message) {
                     try {
                        LOG.info("advisory:" + message);
                        ActiveMQMessage activeMQMessage = (ActiveMQMessage) message;
                        assertNotNull(activeMQMessage.getStringProperty(AdvisorySupport.MSG_PROPERTY_CONSUMER_ID));
                        assertEquals(++advisoriesReceived, activeMQMessage.getIntProperty(AdvisorySupport.MSG_PROPERTY_DISCARDED_COUNT));
                        message.acknowledge();
                        advisoryIsGood.countDown();
                     } catch (JMSException e) {
                        e.printStackTrace();
                        fail(e.toString());
                     } finally {
                        gotAdvisory.countDown();
                     }
                  }
               });
               consumerRegistered.countDown();
               gotAdvisory.await(120, TimeUnit.SECONDS);
               consumer.close();
               advisorySession.close();
            } catch (Exception e) {
               e.printStackTrace();
               fail(e.toString());
            }
         }
      });
      assertTrue("we have an advisory consumer", consumerRegistered.await(60, TimeUnit.SECONDS));
      doTestMessageEvictionMemoryUsage();
      assertTrue("got an advisory for discarded", gotAdvisory.await(0, TimeUnit.SECONDS));
      assertTrue("advisory is good", advisoryIsGood.await(0, TimeUnit.SECONDS));
   }

   public void doTestMessageEvictionMemoryUsage() throws Exception {

      ExecutorService executor = Executors.newCachedThreadPool();
      final CountDownLatch doAck = new CountDownLatch(1);
      final CountDownLatch ackDone = new CountDownLatch(1);
      final CountDownLatch consumerRegistered = new CountDownLatch(1);
      executor.execute(new Runnable() {
         @Override
         public void run() {
            try {
               final MessageConsumer consumer = session.createConsumer(destination);
               consumer.setMessageListener(new MessageListener() {
                  @Override
                  public void onMessage(Message message) {
                     try {
                        // very slow, only ack once
                        doAck.await(60, TimeUnit.SECONDS);
                        LOG.info("acking: " + message.getJMSMessageID());
                        message.acknowledge();
                        ackDone.countDown();
                     } catch (Exception e) {
                        e.printStackTrace();
                        fail(e.toString());
                     } finally {
                        consumerRegistered.countDown();
                        ackDone.countDown();
                     }
                  }
               });
               consumerRegistered.countDown();
               ackDone.await(60, TimeUnit.SECONDS);
               consumer.close();
            } catch (Exception e) {
               e.printStackTrace();
               fail(e.toString());
            }
         }
      });

      assertTrue("we have a consumer", consumerRegistered.await(10, TimeUnit.SECONDS));

      final AtomicInteger sent = new AtomicInteger(0);
      final CountDownLatch sendDone = new CountDownLatch(1);
      executor.execute(new Runnable() {
         @Override
         public void run() {
            MessageProducer producer;
            try {
               producer = session.createProducer(destination);
               for (int i = 0; i < numMessages; i++) {
                  producer.send(session.createTextMessage(payload));
                  sent.incrementAndGet();
                  TimeUnit.MILLISECONDS.sleep(10);
               }
               producer.close();
               sendDone.countDown();
            } catch (Exception e) {
               sendDone.countDown();
               e.printStackTrace();
               fail(e.toString());
            }
         }
      });

      assertTrue("messages sending done", sendDone.await(180, TimeUnit.SECONDS));
      assertEquals("all message were sent", numMessages, sent.get());

      doAck.countDown();
      executor.shutdown();
      executor.awaitTermination(30, TimeUnit.SECONDS);

      assertTrue("usage goes to 0 once consumer goes away", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return 0 == TestSupport.getDestination(broker, ActiveMQDestination.transform(destination)).getMemoryUsage().getPercentUsage();
         }
      }));
   }

   BrokerService createBroker(PendingSubscriberMessageStoragePolicy pendingSubscriberPolicy) throws Exception {
      BrokerService brokerService = new BrokerService();
      brokerService.addConnector("tcp://localhost:0");
      brokerService.setUseJmx(false);
      brokerService.setDeleteAllMessagesOnStartup(true);

      // spooling to disk early so topic memory limit is not reached
      brokerService.getSystemUsage().getMemoryUsage().setLimit(500 * 1024);

      final List<PolicyEntry> policyEntries = new ArrayList<>();
      final PolicyEntry entry = new PolicyEntry();
      entry.setTopic(">");

      entry.setAdvisoryForDiscardingMessages(true);

      // so consumer does not get over run while blocked limit the prefetch
      entry.setTopicPrefetch(50);

      entry.setPendingSubscriberPolicy(pendingSubscriberPolicy);

      // limit the number of outstanding messages, large enough to use the file store
      // or small enough not to blow memory limit
      int pendingMessageLimit = 50;
      if (pendingSubscriberPolicy instanceof FilePendingSubscriberMessageStoragePolicy) {
         pendingMessageLimit = 500;
      }
      ConstantPendingMessageLimitStrategy pendingMessageLimitStrategy = new ConstantPendingMessageLimitStrategy();
      pendingMessageLimitStrategy.setLimit(pendingMessageLimit);
      entry.setPendingMessageLimitStrategy(pendingMessageLimitStrategy);

      // to keep the limit in check and up to date rather than just the first few, evict some
      OldestMessageEvictionStrategy messageEvictionStrategy = new OldestMessageEvictionStrategy();
      // whether to check expiry before eviction, default limit 1000 is fine as no ttl set in this test
      //messageEvictionStrategy.setEvictExpiredMessagesHighWatermark(1000);
      entry.setMessageEvictionStrategy(messageEvictionStrategy);

      // let evicted messaged disappear
      entry.setDeadLetterStrategy(null);
      policyEntries.add(entry);

      final PolicyMap policyMap = new PolicyMap();
      policyMap.setPolicyEntries(policyEntries);
      brokerService.setDestinationPolicy(policyMap);

      return brokerService;
   }

   ConnectionFactory createConnectionFactory() throws Exception {
      String url = broker.getTransportConnectors().get(0).getServer().getConnectURI().toString();
      ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(url);
      factory.setWatchTopicAdvisories(false);
      return factory;
   }

}
