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

import javax.jms.Connection;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import java.util.ArrayList;
import java.util.Iterator;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.util.Wait;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertTrue;

public class QueueBridgeStandaloneReconnectTest {

   private SimpleJmsQueueConnector jmsQueueConnector;

   private BrokerService localBroker;
   private BrokerService foreignBroker;

   private ActiveMQConnectionFactory localConnectionFactory;
   private ActiveMQConnectionFactory foreignConnectionFactory;

   private Destination outbound;
   private Destination inbound;

   private final ArrayList<Connection> connections = new ArrayList<>();

   @Test(timeout = 60 * 1000)
   public void testSendAndReceiveOverConnectedBridges() throws Exception {

      startLocalBroker();
      startForeignBroker();

      jmsQueueConnector.start();

      sendMessageToForeignBroker("to.foreign.broker");
      sendMessageToLocalBroker("to.local.broker");

      final MessageConsumer local = createConsumerForLocalBroker();

      assertTrue("Should have received a Message.", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            Message message = local.receive(100);
            if (message != null && ((TextMessage) message).getText().equals("to.local.broker")) {
               return true;
            }
            return false;
         }
      }));

      final MessageConsumer foreign = createConsumerForForeignBroker();

      assertTrue("Should have received a Message.", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            Message message = foreign.receive(100);
            if (message != null && ((TextMessage) message).getText().equals("to.foreign.broker")) {
               return true;
            }
            return false;
         }
      }));
   }

   @Test(timeout = 60 * 1000)
   public void testSendAndReceiveOverBridgeWhenStartedBeforeBrokers() throws Exception {

      jmsQueueConnector.start();

      startLocalBroker();
      startForeignBroker();

      assertTrue("Should have Connected.", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return jmsQueueConnector.isConnected();
         }
      }));

      sendMessageToForeignBroker("to.foreign.broker");
      sendMessageToLocalBroker("to.local.broker");

      final MessageConsumer local = createConsumerForLocalBroker();

      assertTrue("Should have received a Message.", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            Message message = local.receive(100);
            if (message != null && ((TextMessage) message).getText().equals("to.local.broker")) {
               return true;
            }
            return false;
         }
      }));

      final MessageConsumer foreign = createConsumerForForeignBroker();

      assertTrue("Should have received a Message.", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            Message message = foreign.receive(100);
            if (message != null && ((TextMessage) message).getText().equals("to.foreign.broker")) {
               return true;
            }
            return false;
         }
      }));
   }

   @Test(timeout = 60 * 1000)
   public void testSendAndReceiveOverBridgeWithRestart() throws Exception {

      startLocalBroker();
      startForeignBroker();

      jmsQueueConnector.start();

      assertTrue("Should have Connected.", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return jmsQueueConnector.isConnected();
         }
      }));

      stopLocalBroker();
      stopForeignBroker();

      assertTrue("Should have detected connection drop.", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return !jmsQueueConnector.isConnected();
         }
      }));

      startLocalBroker();
      startForeignBroker();

      assertTrue("Should have Re-Connected.", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            return jmsQueueConnector.isConnected();
         }
      }));

      sendMessageToForeignBroker("to.foreign.broker");
      sendMessageToLocalBroker("to.local.broker");

      final MessageConsumer local = createConsumerForLocalBroker();

      assertTrue("Should have received a Message.", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            Message message = local.receive(100);
            if (message != null && ((TextMessage) message).getText().equals("to.local.broker")) {
               return true;
            }
            return false;
         }
      }));

      final MessageConsumer foreign = createConsumerForForeignBroker();

      assertTrue("Should have received a Message.", Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            Message message = foreign.receive(100);
            if (message != null && ((TextMessage) message).getText().equals("to.foreign.broker")) {
               return true;
            }
            return false;
         }
      }));
   }

   @Before
   public void setUp() throws Exception {

      localConnectionFactory = createLocalConnectionFactory();
      foreignConnectionFactory = createForeignConnectionFactory();

      outbound = new ActiveMQQueue("RECONNECT.TEST.OUT.QUEUE");
      inbound = new ActiveMQQueue("RECONNECT.TEST.IN.QUEUE");

      jmsQueueConnector = new SimpleJmsQueueConnector();

      // Wire the bridges.
      jmsQueueConnector.setOutboundQueueBridges(new OutboundQueueBridge[]{new OutboundQueueBridge("RECONNECT.TEST.OUT.QUEUE")});
      jmsQueueConnector.setInboundQueueBridges(new InboundQueueBridge[]{new InboundQueueBridge("RECONNECT.TEST.IN.QUEUE")});

      // Tell it how to reach the two brokers.
      jmsQueueConnector.setOutboundQueueConnectionFactory(new ActiveMQConnectionFactory("tcp://localhost:61617"));
      jmsQueueConnector.setLocalQueueConnectionFactory(new ActiveMQConnectionFactory("tcp://localhost:61616"));
   }

   @After
   public void tearDown() throws Exception {
      disposeConsumerConnections();

      try {
         jmsQueueConnector.stop();
         jmsQueueConnector = null;
      } catch (Exception e) {
      }

      try {
         stopLocalBroker();
      } catch (Throwable e) {
      }
      try {
         stopForeignBroker();
      } catch (Throwable e) {
      }
   }

   protected void disposeConsumerConnections() {
      for (Iterator<Connection> iter = connections.iterator(); iter.hasNext(); ) {
         Connection connection = iter.next();
         try {
            connection.close();
         } catch (Throwable ignore) {
         }
      }
   }

   protected void startLocalBroker() throws Exception {
      if (localBroker == null) {
         localBroker = createFirstBroker();
         localBroker.start();
         localBroker.waitUntilStarted();
      }
   }

   protected void stopLocalBroker() throws Exception {
      if (localBroker != null) {
         localBroker.stop();
         localBroker.waitUntilStopped();
         localBroker = null;
      }
   }

   protected void startForeignBroker() throws Exception {
      if (foreignBroker == null) {
         foreignBroker = createSecondBroker();
         foreignBroker.start();
         foreignBroker.waitUntilStarted();
      }
   }

   protected void stopForeignBroker() throws Exception {
      if (foreignBroker != null) {
         foreignBroker.stop();
         foreignBroker.waitUntilStopped();
         foreignBroker = null;
      }
   }

   protected BrokerService createFirstBroker() throws Exception {
      BrokerService broker = new BrokerService();
      broker.setBrokerName("broker1");
      broker.setPersistent(false);
      broker.setUseJmx(false);
      broker.addConnector("tcp://localhost:61616");

      return broker;
   }

   protected BrokerService createSecondBroker() throws Exception {

      BrokerService broker = new BrokerService();
      broker.setBrokerName("broker2");
      broker.setPersistent(false);
      broker.setUseJmx(false);
      broker.addConnector("tcp://localhost:61617");

      return broker;
   }

   protected ActiveMQConnectionFactory createLocalConnectionFactory() {
      return new ActiveMQConnectionFactory("tcp://localhost:61616");
   }

   protected ActiveMQConnectionFactory createForeignConnectionFactory() {
      return new ActiveMQConnectionFactory("tcp://localhost:61617");
   }

   protected void sendMessageToForeignBroker(String text) throws JMSException {
      Connection connection = null;
      try {
         connection = localConnectionFactory.createConnection();
         Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
         MessageProducer producer = session.createProducer(outbound);
         TextMessage message = session.createTextMessage();
         message.setText(text);
         producer.send(message);
      } finally {
         try {
            connection.close();
         } catch (Throwable ignore) {
         }
      }
   }

   protected void sendMessageToLocalBroker(String text) throws JMSException {
      Connection connection = null;
      try {
         connection = foreignConnectionFactory.createConnection();
         Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
         MessageProducer producer = session.createProducer(inbound);
         TextMessage message = session.createTextMessage();
         message.setText(text);
         producer.send(message);
      } finally {
         try {
            connection.close();
         } catch (Throwable ignore) {
         }
      }
   }

   protected MessageConsumer createConsumerForLocalBroker() throws JMSException {
      Connection connection = localConnectionFactory.createConnection();
      connections.add(connection);
      connection.start();

      Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
      return session.createConsumer(inbound);
   }

   protected MessageConsumer createConsumerForForeignBroker() throws JMSException {
      Connection connection = foreignConnectionFactory.createConnection();
      connections.add(connection);
      connection.start();

      Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
      return session.createConsumer(outbound);
   }
}
