/**
 * 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.io.IOException;
import java.util.Vector;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

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;
import static org.junit.Assert.fail;

public class DurableSubProcessMultiRestartTest {

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

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

   private BrokerService broker;
   private ActiveMQTopic topic;

   private final ReentrantReadWriteLock processLock = new ReentrantReadWriteLock(true);

   private int restartCount = 0;
   private final int SUBSCRIPTION_ID = 1;

   static final Vector<Throwable> exceptions = new Vector<>();

   /**
    * The test creates a durable subscriber and producer with a broker that is
    * continually restarted.
    *
    * Producer creates a message every .5 seconds -creates a new connection for
    * each message
    *
    * durable subscriber - comes online for 10 seconds, - then goes offline for
    * a "moment" - repeats the cycle
    *
    * approx every 10 seconds the broker restarts. Subscriber and Producer
    * connections will be closed BEFORE the restart.
    *
    * The Durable subscriber is "unsubscribed" before the the end of the test.
    *
    * checks for number of kahaDB files left on filesystem.
    *
    * @throws Exception
    */
   @Test
   public void testProcess() throws Exception {

      DurableSubscriber durableSubscriber = new DurableSubscriber(SUBSCRIPTION_ID);
      MsgProducer msgProducer = new MsgProducer();

      try {
         // register the subscription & start messages
         durableSubscriber.start();
         msgProducer.start();

         long endTime = System.currentTimeMillis() + RUNTIME;

         while (endTime > System.currentTimeMillis()) {
            Thread.sleep(10000);
            restartBroker();
         }
      } catch (Throwable e) {
         exit("ProcessTest.testProcess failed.", e);
      }

      // wait for threads to finish
      try {
         msgProducer.join();
         durableSubscriber.join();
      } catch (InterruptedException e) {
         e.printStackTrace(System.out);
      }

      // restart broker one last time
      restartBroker();

      assertTrue("no exceptions: " + exceptions, exceptions.isEmpty());

      final KahaDBPersistenceAdapter pa = (KahaDBPersistenceAdapter) broker.getPersistenceAdapter();
      assertTrue("only less than two journal files should be left: " + pa.getStore().getJournal().getFileMap().size(), Wait.waitFor(new Wait.Condition() {

         @Override
         public boolean isSatisified() throws Exception {
            return pa.getStore().getJournal().getFileMap().size() <= 2;
         }
      }, TimeUnit.MINUTES.toMillis(3)));

      LOG.info("DONE.");
   }

   private void restartBroker() throws Exception {
      LOG.info("Broker restart: waiting for components.");

      processLock.writeLock().lock();
      try {
         destroyBroker();
         startBroker(false);

         restartCount++;
         LOG.info("Broker restarted. count: " + restartCount);
      } finally {
         processLock.writeLock().unlock();
      }
   }

   /**
    * Producers messages
    */
   final class MsgProducer extends Thread {

      String url = "vm://" + DurableSubProcessMultiRestartTest.getName();

      final ConnectionFactory cf = new ActiveMQConnectionFactory(url);

      private long msgCount;
      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(500);

               processLock.readLock().lock();
               try {
                  send();
               } finally {
                  processLock.readLock().unlock();
               }
               LOG.info("MsgProducer msgCount=" + msgCount);
            }
         } catch (Throwable e) {
            exit("Server.run failed", e);
         }
      }

      public void send() throws JMSException {

         LOG.info("Sending ... ");

         Connection con = cf.createConnection();

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

         MessageProducer prod = sess.createProducer(null);
         Message message = sess.createMessage();
         message.setIntProperty("ID", ++messageRover);
         message.setBooleanProperty("COMMIT", true);
         prod.send(topic, message);

         msgCount++;
         LOG.info("Message Sent.");

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

   /**
    * Consumes massages from a durable subscription. Goes online/offline
    * periodically.
    */
   private final class DurableSubscriber extends Thread {

      String url = "tcp://localhost:61656";

      final ConnectionFactory cf = new ActiveMQConnectionFactory(url);

      public static final String SUBSCRIPTION_NAME = "subscription";

      private final int id;
      private final String conClientId;
      private long msgCount;

      public DurableSubscriber(int id) throws JMSException {
         super("DurableSubscriber" + id);
         setDaemon(true);

         this.id = id;
         conClientId = "cli" + id;

         subscribe();
      }

      @Override
      public void run() {

         long end = System.currentTimeMillis() + RUNTIME;

         try {

            // while (true) {
            while (end > System.currentTimeMillis()) {

               processLock.readLock().lock();
               try {
                  process(5000);
               } finally {
                  processLock.readLock().unlock();
               }
            }

            unsubscribe();

         } catch (JMSException maybe) {
            if (maybe.getCause() instanceof IOException) {
               // ok on broker shutdown;
            } else {
               exit(toString() + " failed with JMSException", maybe);
            }
         } catch (Throwable e) {
            exit(toString() + " failed.", e);
         }

         LOG.info(toString() + " DONE. MsgCout=" + msgCount);
      }

      private void process(long duration) throws JMSException {
         LOG.info(toString() + " ONLINE.");

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

         MessageConsumer consumer = sess.createDurableSubscriber(topic, SUBSCRIPTION_NAME);

         long end = System.currentTimeMillis() + duration;

         try {
            while (end > System.currentTimeMillis()) {
               Message message = consumer.receive(100);
               if (message != null) {
                  LOG.info(toString() + "received message...");
                  msgCount++;
               }
            }
         } finally {
            sess.close();
            con.close();
            LOG.info(toString() + " OFFLINE.");
         }
      }

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

      private void subscribe() throws JMSException {
         Connection con = openConnection();
         Session session = con.createSession(false, Session.AUTO_ACKNOWLEDGE);

         session.createDurableSubscriber(topic, SUBSCRIPTION_NAME);
         LOG.info(toString() + " SUBSCRIBED");

         session.close();
         con.close();
      }

      private void unsubscribe() throws JMSException {
         Connection con = openConnection();
         Session session = con.createSession(false, Session.AUTO_ACKNOWLEDGE);
         session.unsubscribe(SUBSCRIPTION_NAME);
         LOG.info(toString() + " UNSUBSCRIBED");

         session.close();
         con.close();
      }

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

   // -------- helper methods -----------

   public static void exit(String message) {
      exit(message, null);
   }

   public static void exit(String message, Throwable e) {
      Throwable cause = new RuntimeException(message, e);
      LOG.error(message, cause);
      exceptions.add(cause);
      fail(cause.toString());
   }

   @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);

      broker.setKeepDurableSubsActive(true);

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

      broker.setPersistent(true);
      KahaDBPersistenceAdapter kahadb = new KahaDBPersistenceAdapter();
      kahadb.setDirectory(kahadbData);
      kahadb.setJournalMaxFileLength(20 * 1024);
      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);

      broker.start();
   }

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

   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;
   }
}
