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

import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;

import javax.jms.Connection;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.Session;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.advisory.AdvisoryBroker;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.ConnectionContext;
import org.apache.activemq.command.ActiveMQDestination;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ConnectionId;
import org.apache.activemq.command.ConnectionInfo;
import org.apache.activemq.command.ConsumerId;
import org.apache.activemq.command.ConsumerInfo;
import org.apache.activemq.command.SessionId;
import org.apache.activemq.command.SessionInfo;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AMQ4853Test {

   private static final transient Logger LOG = LoggerFactory.getLogger(AMQ4853Test.class);
   private static BrokerService brokerService;
   private static final String BROKER_ADDRESS = "tcp://localhost:0";
   private static final ActiveMQQueue DESTINATION = new ActiveMQQueue("TEST.QUEUE");
   private CountDownLatch cycleDoneLatch;

   private String connectionUri;

   @Before
   public void setUp() throws Exception {
      brokerService = new BrokerService();
      brokerService.setPersistent(false);
      brokerService.setUseJmx(false);
      brokerService.setAdvisorySupport(true);
      brokerService.setDeleteAllMessagesOnStartup(true);
      connectionUri = brokerService.addConnector(BROKER_ADDRESS).getPublishableConnectString();

      brokerService.start();
      brokerService.waitUntilStarted();
   }

   @After
   public void tearDown() throws Exception {
      brokerService.stop();
      brokerService.waitUntilStopped();
   }

   /**
    * Test to shows the performance of the removing consumers while other stay active.
    *
    * @throws Exception
    */
   @Ignore
   @Test
   public void test() throws Exception {

      // Create a stable set of consumers to fill in the advisory broker's consumer list.
      ArrayList<Consumer> fixedConsumers = new ArrayList<Consumer>(100);
      for (int i = 0; i < 200; ++i) {
         fixedConsumers.add(new Consumer());
      }

      // Create a set of consumers that comes online for a short time and then
      // goes offline again.  Cycles will repeat as each batch completes
      final int fixedDelayConsumers = 300;
      final int fixedDelayCycles = 25;

      final CountDownLatch fixedDelayCycleLatch = new CountDownLatch(fixedDelayCycles);

      // Update so done method can track state.
      cycleDoneLatch = fixedDelayCycleLatch;

      CyclicBarrier barrier = new CyclicBarrier(fixedDelayConsumers, new Runnable() {
         @Override
         public void run() {
            LOG.info("Fixed delay consumers cycle {} completed.", fixedDelayCycleLatch.getCount());
            fixedDelayCycleLatch.countDown();
         }
      });

      for (int i = 0; i < fixedDelayConsumers; ++i) {
         new Thread(new FixedDelyConsumer(barrier)).start();
      }

      fixedDelayCycleLatch.await(10, TimeUnit.MINUTES);

      // Clean up.

      for (Consumer consumer : fixedConsumers) {
         consumer.close();
      }
      fixedConsumers.clear();
   }

   private ConnectionInfo createConnectionInfo() {
      ConnectionId id = new ConnectionId();
      id.setValue("ID:123456789:0:1");

      ConnectionInfo info = new ConnectionInfo();
      info.setConnectionId(id);
      return info;
   }

   private SessionInfo createSessionInfo(ConnectionInfo connection) {
      SessionId id = new SessionId(connection.getConnectionId(), 1);

      SessionInfo info = new SessionInfo();
      info.setSessionId(id);

      return info;
   }

   public ConsumerInfo createConsumerInfo(SessionInfo session, int value, ActiveMQDestination destination) {
      ConsumerId id = new ConsumerId();
      id.setConnectionId(session.getSessionId().getConnectionId());
      id.setSessionId(1);
      id.setValue(value);

      ConsumerInfo info = new ConsumerInfo();
      info.setConsumerId(id);
      info.setDestination(destination);
      return info;
   }

   /**
    * Test to shows the performance impact of removing consumers in various scenarios.
    *
    * @throws Exception
    */
   @Ignore
   @Test
   public void testPerformanceOfRemovals() throws Exception {
      // setup
      AdvisoryBroker testObj = (AdvisoryBroker) brokerService.getBroker().getAdaptor(AdvisoryBroker.class);
      ActiveMQDestination destination = new ActiveMQQueue("foo");
      ConnectionInfo connectionInfo = createConnectionInfo();
      ConnectionContext connectionContext = new ConnectionContext(connectionInfo);
      connectionContext.setBroker(brokerService.getBroker());
      SessionInfo sessionInfo = createSessionInfo(connectionInfo);

      long start = System.currentTimeMillis();

      for (int i = 0; i < 200; ++i) {

         for (int j = 1; j <= 500; j++) {
            ConsumerInfo consumerInfo = createConsumerInfo(sessionInfo, j, destination);
            testObj.addConsumer(connectionContext, consumerInfo);
         }

         for (int j = 500; j > 0; j--) {
            ConsumerInfo consumerInfo = createConsumerInfo(sessionInfo, j, destination);
            testObj.removeConsumer(connectionContext, consumerInfo);
         }

         for (int j = 1; j <= 500; j++) {
            ConsumerInfo consumerInfo = createConsumerInfo(sessionInfo, j, destination);
            testObj.addConsumer(connectionContext, consumerInfo);
         }

         for (int j = 1; j <= 500; j++) {
            ConsumerInfo consumerInfo = createConsumerInfo(sessionInfo, j, destination);
            testObj.removeConsumer(connectionContext, consumerInfo);
         }
      }

      long finish = System.currentTimeMillis();

      long totalTime = finish - start;

      LOG.info("Total test time: {} seconds", TimeUnit.MILLISECONDS.toSeconds(totalTime));

      assertEquals(0, testObj.getAdvisoryConsumers().size());
   }

   @Test
   public void testEqualsNeeded() throws Exception {
      // setup
      AdvisoryBroker testObj = (AdvisoryBroker) brokerService.getBroker().getAdaptor(AdvisoryBroker.class);
      ActiveMQDestination destination = new ActiveMQQueue("foo");
      ConnectionInfo connectionInfo = createConnectionInfo();
      ConnectionContext connectionContext = new ConnectionContext(connectionInfo);
      connectionContext.setBroker(brokerService.getBroker());
      SessionInfo sessionInfo = createSessionInfo(connectionInfo);

      for (int j = 1; j <= 5; j++) {
         ConsumerInfo consumerInfo = createConsumerInfo(sessionInfo, j, destination);
         testObj.addConsumer(connectionContext, consumerInfo);
      }

      for (int j = 1; j <= 5; j++) {
         ConsumerInfo consumerInfo = createConsumerInfo(sessionInfo, j, destination);
         testObj.removeConsumer(connectionContext, consumerInfo);
      }

      assertEquals(0, testObj.getAdvisoryConsumers().size());
   }

   private boolean done() {
      if (cycleDoneLatch == null) {
         return true;
      }
      return cycleDoneLatch.getCount() == 0;
   }

   class Consumer implements MessageListener {

      Connection connection;
      Session session;
      Destination destination;
      MessageConsumer consumer;

      Consumer() throws JMSException {
         ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(connectionUri);
         connection = factory.createConnection();
         session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
         consumer = session.createConsumer(DESTINATION);
         consumer.setMessageListener(this);
         connection.start();
      }

      @Override
      public void onMessage(Message message) {
      }

      public void close() {
         try {
            connection.close();
         }
         catch (Exception e) {
         }

         connection = null;
         session = null;
         consumer = null;
      }
   }

   class FixedDelyConsumer implements Runnable {

      private final CyclicBarrier barrier;
      private final int sleepInterval;

      public FixedDelyConsumer(CyclicBarrier barrier) {
         this.barrier = barrier;
         this.sleepInterval = 1000;
      }

      public FixedDelyConsumer(CyclicBarrier barrier, int sleepInterval) {
         this.barrier = barrier;
         this.sleepInterval = sleepInterval;
      }

      @Override
      public void run() {
         while (!done()) {

            try {
               Consumer consumer = new Consumer();
               TimeUnit.MILLISECONDS.sleep(sleepInterval);
               consumer.close();
               barrier.await();
            }
            catch (Exception ex) {
               return;
            }
         }
      }
   }

}
