/**
 * 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 ONDITIONS 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.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Session;
import java.io.File;
import java.util.concurrent.TimeUnit;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.broker.BrokerFactory;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.command.ActiveMQTopic;
import org.apache.activemq.store.kahadb.KahaDBPersistenceAdapter;
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;

import static org.junit.Assert.assertTrue;

public class DurableSubSelectorDelayWithRestartTest {

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

   public static final long RUNTIME = 1 * 60 * 1000;

   private boolean RESTART = true;
   private int NUMBER_SUBSCRIBERS = 3;

   private BrokerService broker;
   private ActiveMQTopic topic;

   @Test
   public void testProcess() throws Exception {

      MsgProducer msgProducer = new MsgProducer();
      msgProducer.start();

      DurableSubscriber subscribers[] = new DurableSubscriber[NUMBER_SUBSCRIBERS];

      for (int i = 0; i < subscribers.length - 1; i++) {
         subscribers[i] = new DurableSubscriber(i);
         subscribers[i].process();
      }

      // wait for server to finish
      msgProducer.join();

      //for the last subscriber pop one message into the topic.
      subscribers[(subscribers.length - 1)] = new DurableSubscriber((subscribers.length - 1));
      subscribers[(subscribers.length - 1)].subscribe();
      MsgProducer msgProducer2 = new MsgProducer();
      msgProducer2.send();
      subscribers[(subscribers.length - 1)].process();

      // unsubscribe all, but the last subscriber.
      for (int j = 0; j < (subscribers.length - 1); j++) {
         LOG.info("Unsubscribing subscriber " + subscribers[j]);
         subscribers[j].unsubscribe();
      }

      final KahaDBPersistenceAdapter pa = (KahaDBPersistenceAdapter) broker.getPersistenceAdapter();
      assertTrue("small number of journal files should be left ", Wait.waitFor(new Wait.Condition() {

         @Override
         public boolean isSatisified() throws Exception {
            LOG.info("journal data file count - expected {} actual {}", 4, pa.getStore().getJournal().getFileMap().size());
            return pa.getStore().getJournal().getFileMap().size() < 4;
         }
      }, TimeUnit.MINUTES.toMillis(3)));

      LOG.info("DONE.");
   }

   /**
    * Message Producer
    */
   final class MsgProducer extends Thread {

      final String url = "failover:(tcp://localhost:61656)";

      final ConnectionFactory cf = new ActiveMQConnectionFactory(url);

      int transRover = 0;
      int messageRover = 0;

      public MsgProducer() {
         super("MsgProducer");
         setDaemon(true);
      }

      @Override
      public void run() {
         long endTime = RUNTIME + System.currentTimeMillis();

         try {
            while (endTime > System.currentTimeMillis()) {
               Thread.sleep(400);
               send();

               //restart broker all the time
               if (RESTART) {
                  destroyBroker();
                  startBroker(false);
               }
            }
         } catch (Throwable e) {
            e.printStackTrace(System.out);
            throw new RuntimeException(e);
         }
      }

      public void send() throws JMSException {

         int trans = ++transRover;
         boolean relevantTrans = true;
         int count = 40;

         LOG.info("Sending Trans[id=" + trans + ", count=" + count + "]");

         Connection con = cf.createConnection();

         Session sess = con.createSession(false, Session.AUTO_ACKNOWLEDGE);

         MessageProducer prod = sess.createProducer(null);

         for (int i = 0; i < count; i++) {
            Message message = sess.createMessage();
            message.setIntProperty("ID", ++messageRover);
            message.setIntProperty("TRANS", trans);
            message.setBooleanProperty("RELEVANT", false);
            prod.send(topic, message);
         }

         Message message = sess.createMessage();
         message.setIntProperty("ID", ++messageRover);
         message.setIntProperty("TRANS", trans);
         message.setBooleanProperty("COMMIT", true);
         message.setBooleanProperty("RELEVANT", relevantTrans);
         prod.send(topic, message);

         LOG.info("Committed Trans[id=" + trans + ", count=" + count + "], ID=" + messageRover);

         sess.close();
         con.close();
      }
   }

   /**
    * Consumes massages from a durable subscription. Goes online/offline
    * periodically. Checks the incoming messages against the sent messages of
    * the server.
    */
   private final class DurableSubscriber {

      final String url = "failover:(tcp://localhost:61656)";

      final ConnectionFactory cf = new ActiveMQConnectionFactory(url);

      private final String subName;

      private final int id;
      private final String conClientId;
      private final String selector;

      public DurableSubscriber(int id) throws JMSException {
         this.id = id;
         conClientId = "cli" + id;
         subName = "subscription" + id;
         selector = "RELEVANT = true";
      }

      private void process() throws JMSException {
         long end = System.currentTimeMillis() + 20000;
         int transCount = 0;

         LOG.info(toString() + " ONLINE.");
         Connection con = openConnection();

         Session sess = con.createSession(false, Session.AUTO_ACKNOWLEDGE);
         MessageConsumer consumer = sess.createDurableSubscriber(topic, subName, selector, false);

         try {

            do {
               long max = end - System.currentTimeMillis();

               if (max <= 0) {
                  break;
               }

               Message message = consumer.receive(max);
               if (message == null) {
                  continue;
               }

               LOG.info("Received Trans[id=" + message.getIntProperty("TRANS") + ", count=" + transCount + "] in " + this + ".");

            } while (true);

         } finally {
            try {
               sess.close();
               con.close();
            } catch (Exception e) {
            }

            LOG.info(toString() + " OFFLINE.");
         }
      }

      private Connection openConnection() throws JMSException {
         Connection con = cf.createConnection();
         con.setClientID(conClientId);
         con.start();
         return con;
      }

      public void subscribe() throws JMSException {
         LOG.info(toString() + "SUBSCRIBING");
         Connection con = openConnection();

         Session sess = con.createSession(false, Session.AUTO_ACKNOWLEDGE);
         sess.createDurableSubscriber(topic, subName, selector, false);

         sess.close();
         con.close();
      }

      private void unsubscribe() throws JMSException {
         Connection con = openConnection();
         Session session = con.createSession(false, Session.AUTO_ACKNOWLEDGE);
         session.unsubscribe(subName);
         session.close();
         con.close();
      }

      @Override
      public String toString() {
         return "DurableSubscriber[id=" + id + "]";
      }
   }

   @Before
   public void setUp() throws Exception {
      topic = new ActiveMQTopic("TopicT");
      startBroker();
   }

   @After
   public void tearDown() throws Exception {
      destroyBroker();
   }

   private void startBroker() throws Exception {
      startBroker(true);
   }

   private void startBroker(boolean deleteAllMessages) throws Exception {
      if (broker != null)
         return;

      broker = BrokerFactory.createBroker("broker:(vm://" + getName() + ")");
      broker.setBrokerName(getName());
      broker.setAdvisorySupport(false);
      broker.setDeleteAllMessagesOnStartup(deleteAllMessages);

      File kahadbData = new File("activemq-data/" + getName() + "-kahadb");
      if (deleteAllMessages)
         delete(kahadbData);

      broker.setPersistent(true);
      KahaDBPersistenceAdapter kahadb = new KahaDBPersistenceAdapter();
      kahadb.setDirectory(kahadbData);
      kahadb.setJournalMaxFileLength(10 * 1024);
      kahadb.setCleanupInterval(5000);
      broker.setPersistenceAdapter(kahadb);

      broker.addConnector("tcp://localhost:61656");

      broker.getSystemUsage().getMemoryUsage().setLimit(256 * 1024 * 1024);
      broker.getSystemUsage().getTempUsage().setLimit(256 * 1024 * 1024);
      broker.getSystemUsage().getStoreUsage().setLimit(256 * 1024 * 1024);

      LOG.info(toString() + "Starting Broker...");
      broker.start();
      broker.waitUntilStarted();

      LOG.info(toString() + " Broker started!!");
   }

   protected static String getName() {
      return "DurableSubSelectorDelayTest";
   }

   private static boolean delete(File path) {
      if (path == null)
         return true;

      if (path.isDirectory()) {
         for (File file : path.listFiles()) {
            delete(file);
         }
      }
      return path.delete();
   }

   private void destroyBroker() throws Exception {
      if (broker == null)
         return;

      broker.stop();
      broker = null;
   }
}
