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

import javax.jms.InvalidSelectorException;
import javax.management.ObjectName;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;

import junit.framework.TestCase;

import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.ConnectionContext;
import org.apache.activemq.broker.ProducerBrokerExchange;
import org.apache.activemq.command.ActiveMQDestination;
import org.apache.activemq.command.ActiveMQMessage;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ConsumerInfo;
import org.apache.activemq.command.Message;
import org.apache.activemq.command.MessageAck;
import org.apache.activemq.command.MessageDispatchNotification;
import org.apache.activemq.command.MessageId;
import org.apache.activemq.command.MessagePull;
import org.apache.activemq.command.ProducerInfo;
import org.apache.activemq.command.Response;
import org.apache.activemq.filter.MessageEvaluationContext;
import org.apache.activemq.state.ProducerState;
import org.apache.activemq.store.MessageStore;
import org.apache.activemq.thread.TaskRunnerFactory;

public class SubscriptionAddRemoveQueueTest extends TestCase {

   Queue queue;

   ConsumerInfo info = new ConsumerInfo();
   List<SimpleImmediateDispatchSubscription> subs = new ArrayList<>();
   ConnectionContext context = new ConnectionContext();
   ProducerBrokerExchange producerBrokerExchange = new ProducerBrokerExchange();
   ProducerInfo producerInfo = new ProducerInfo();
   ProducerState producerState = new ProducerState(producerInfo);
   ActiveMQDestination destination = new ActiveMQQueue("TEST");
   int numSubscriptions = 1000;
   boolean working = true;
   int senders = 20;

   @Override
   public void setUp() throws Exception {
      BrokerService brokerService = new BrokerService();
      brokerService.start();
      DestinationStatistics parentStats = new DestinationStatistics();
      parentStats.setEnabled(true);

      TaskRunnerFactory taskFactory = new TaskRunnerFactory();
      MessageStore store = null;

      info.setDestination(destination);
      info.setPrefetchSize(100);

      producerBrokerExchange.setProducerState(producerState);
      producerBrokerExchange.setConnectionContext(context);

      queue = new Queue(brokerService, destination, store, parentStats, taskFactory);
      queue.initialize();
   }

   public void testNoDispatchToRemovedConsumers() throws Exception {
      final AtomicInteger producerId = new AtomicInteger();
      Runnable sender = new Runnable() {
         @Override
         public void run() {
            AtomicInteger id = new AtomicInteger();
            int producerIdAndIncrement = producerId.getAndIncrement();
            while (working) {
               try {
                  Message msg = new ActiveMQMessage();
                  msg.setDestination(destination);
                  msg.setMessageId(new MessageId(producerIdAndIncrement + ":0:" + id.getAndIncrement()));
                  queue.send(producerBrokerExchange, msg);
               } catch (Exception e) {
                  e.printStackTrace();
                  fail("unexpected exception in sendMessage, ex:" + e);
               }
            }
         }
      };

      Runnable subRemover = new Runnable() {
         @Override
         public void run() {
            for (Subscription sub : subs) {
               try {
                  queue.removeSubscription(context, sub, 0);
               } catch (Exception e) {
                  e.printStackTrace();
                  fail("unexpected exception in removeSubscription, ex:" + e);
               }
            }
         }
      };

      for (int i = 0; i < numSubscriptions; i++) {
         SimpleImmediateDispatchSubscription sub = new SimpleImmediateDispatchSubscription();
         subs.add(sub);
         queue.addSubscription(context, sub);
      }
      assertEquals("there are X subscriptions", numSubscriptions, queue.getDestinationStatistics().getConsumers().getCount());
      ExecutorService executor = Executors.newCachedThreadPool();
      for (int i = 0; i < senders; i++) {
         executor.submit(sender);
      }

      Thread.sleep(1000);
      for (SimpleImmediateDispatchSubscription sub : subs) {
         assertTrue("There are some locked messages in the subscription", hasSomeLocks(sub.dispatched));
      }

      Future<?> result = executor.submit(subRemover);
      result.get();
      working = false;
      assertEquals("there are no subscriptions", 0, queue.getDestinationStatistics().getConsumers().getCount());

      for (SimpleImmediateDispatchSubscription sub : subs) {
         assertTrue("There are no locked messages in any removed subscriptions", !hasSomeLocks(sub.dispatched));
      }

   }

   private boolean hasSomeLocks(List<MessageReference> dispatched) {
      boolean hasLock = false;
      for (MessageReference mr : dispatched) {
         QueueMessageReference qmr = (QueueMessageReference) mr;
         if (qmr.getLockOwner() != null) {
            hasLock = true;
            break;
         }
      }
      return hasLock;
   }

   public class SimpleImmediateDispatchSubscription implements Subscription, LockOwner {

      private SubscriptionStatistics subscriptionStatistics = new SubscriptionStatistics();
      List<MessageReference> dispatched = Collections.synchronizedList(new ArrayList<MessageReference>());

      @Override
      public long getPendingMessageSize() {
         return 0;
      }

      @Override
      public void acknowledge(ConnectionContext context, MessageAck ack) throws Exception {
      }

      @Override
      public void add(MessageReference node) throws Exception {
         // immediate dispatch
         QueueMessageReference qmr = (QueueMessageReference) node;
         qmr.lock(this);
         dispatched.add(qmr);
      }

      @Override
      public ConnectionContext getContext() {
         return null;
      }

      @Override
      public int getCursorMemoryHighWaterMark() {
         return 0;
      }

      @Override
      public void setCursorMemoryHighWaterMark(int cursorMemoryHighWaterMark) {
      }

      @Override
      public boolean isSlowConsumer() {
         return false;
      }

      @Override
      public void unmatched(MessageReference node) throws IOException {
      }

      @Override
      public long getTimeOfLastMessageAck() {
         return 0;
      }

      @Override
      public long getConsumedCount() {
         return 0;
      }

      @Override
      public void incrementConsumedCount() {
      }

      @Override
      public void resetConsumedCount() {
      }

      @Override
      public void add(ConnectionContext context, Destination destination) throws Exception {
      }

      @Override
      public void destroy() {
      }

      @Override
      public void gc() {
      }

      @Override
      public ConsumerInfo getConsumerInfo() {
         return info;
      }

      @Override
      public long getDequeueCounter() {
         return 0;
      }

      @Override
      public long getDispatchedCounter() {
         return 0;
      }

      @Override
      public int getDispatchedQueueSize() {
         return 0;
      }

      @Override
      public long getEnqueueCounter() {
         return 0;
      }

      @Override
      public int getInFlightSize() {
         return 0;
      }

      @Override
      public int getInFlightUsage() {
         return 0;
      }

      @Override
      public ObjectName getObjectName() {
         return null;
      }

      @Override
      public int getPendingQueueSize() {
         return 0;
      }

      @Override
      public int getPrefetchSize() {
         return 0;
      }

      @Override
      public String getSelector() {
         return null;
      }

      @Override
      public boolean isBrowser() {
         return false;
      }

      @Override
      public boolean isFull() {
         return false;
      }

      @Override
      public boolean isHighWaterMark() {
         return false;
      }

      @Override
      public boolean isLowWaterMark() {
         return false;
      }

      @Override
      public boolean isRecoveryRequired() {
         return false;
      }

      public boolean isSlave() {
         return false;
      }

      @Override
      public boolean matches(MessageReference node, MessageEvaluationContext context) throws IOException {
         return true;
      }

      @Override
      public boolean matches(ActiveMQDestination destination) {
         return false;
      }

      @Override
      public void processMessageDispatchNotification(MessageDispatchNotification mdn) throws Exception {
      }

      @Override
      public Response pullMessage(ConnectionContext context, MessagePull pull) throws Exception {
         return null;
      }

      @Override
      public boolean isWildcard() {
         return false;
      }

      @Override
      public List<MessageReference> remove(ConnectionContext context, Destination destination) throws Exception {
         return new ArrayList<>(dispatched);
      }

      @Override
      public void setObjectName(ObjectName objectName) {
      }

      @Override
      public void setSelector(String selector) throws InvalidSelectorException, UnsupportedOperationException {
      }

      @Override
      public void updateConsumerPrefetch(int newPrefetch) {
      }

      @Override
      public boolean addRecoveredMessage(ConnectionContext context, MessageReference message) throws Exception {
         return false;
      }

      @Override
      public ActiveMQDestination getActiveMQDestination() {
         return null;
      }

      @Override
      public int getLockPriority() {
         return 0;
      }

      @Override
      public boolean isLockExclusive() {
         return false;
      }

      public void addDestination(Destination destination) {
      }

      public void removeDestination(Destination destination) {
      }

      @Override
      public int countBeforeFull() {
         return 10;
      }

      @Override
      public SubscriptionStatistics getSubscriptionStatistics() {
         return subscriptionStatistics;
      }

      @Override
      public long getInFlightMessageSize() {
         return subscriptionStatistics.getInflightMessageSize().getTotalSize();
      }

   }
}
