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

import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Vector;

import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQSession;
import org.apache.activemq.JmsMultipleBrokersTestSupport;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.TransportConnector;
import org.apache.activemq.broker.region.RegionBroker;
import org.apache.activemq.broker.region.policy.PolicyEntry;
import org.apache.activemq.broker.region.policy.PolicyMap;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ActiveMQTempQueue;
import org.apache.activemq.network.NetworkConnector;
import org.apache.activemq.util.Wait;
import org.apache.activemq.xbean.XBeanBrokerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RequestReplyNoAdvisoryNetworkTest extends JmsMultipleBrokersTestSupport {

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

   Vector<BrokerService> brokers = new Vector<>();
   BrokerService a, b;
   ActiveMQQueue sendQ = new ActiveMQQueue("sendQ");
   static final String connectionIdMarker = "ID:marker.";
   ActiveMQTempQueue replyQWildcard = new ActiveMQTempQueue(connectionIdMarker + ">");
   private final long receiveTimeout = 30000;

   public void testNonAdvisoryNetworkRequestReplyXmlConfig() throws Exception {
      final String xmlConfigString = new String("<beans" +
                                                   " xmlns=\"http://www.springframework.org/schema/beans\"" +
                                                   " xmlns:amq=\"http://activemq.apache.org/schema/core\"" +
                                                   " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" +
                                                   " xsi:schemaLocation=\"http://www.springframework.org/schema/beans" +
                                                   " http://www.springframework.org/schema/beans/spring-beans-2.0.xsd" +
                                                   " http://activemq.apache.org/schema/core" +
                                                   " http://activemq.apache.org/schema/core/activemq-core.xsd\">" +
                                                   "  <broker xmlns=\"http://activemq.apache.org/schema/core\" id=\"broker\"" +
                                                   "    allowTempAutoCreationOnSend=\"true\" schedulePeriodForDestinationPurge=\"1000\"" +
                                                   "    brokerName=\"%HOST%\" persistent=\"false\" advisorySupport=\"false\" useJmx=\"false\" >" +
                                                   "   <destinationPolicy>" +
                                                   "    <policyMap>" +
                                                   "     <policyEntries>" +
                                                   "      <policyEntry optimizedDispatch=\"true\"  gcInactiveDestinations=\"true\" gcWithNetworkConsumers=\"true\" inactiveTimoutBeforeGC=\"1000\">" +
                                                   "       <destination>" +
                                                   "        <tempQueue physicalName=\"" + replyQWildcard.getPhysicalName() + "\"/>" +
                                                   "       </destination>" +
                                                   "      </policyEntry>" +
                                                   "     </policyEntries>" +
                                                   "    </policyMap>" +
                                                   "   </destinationPolicy>" +
                                                   "   <networkConnectors>" +
                                                   "    <networkConnector uri=\"multicast://default\">" +
                                                   "     <staticallyIncludedDestinations>" +
                                                   "      <queue physicalName=\"" + sendQ.getPhysicalName() + "\"/>" +
                                                   "      <tempQueue physicalName=\"" + replyQWildcard.getPhysicalName() + "\"/>" +
                                                   "     </staticallyIncludedDestinations>" +
                                                   "    </networkConnector>" +
                                                   "   </networkConnectors>" +
                                                   "   <transportConnectors>" +
                                                   "     <transportConnector uri=\"tcp://0.0.0.0:0\" discoveryUri=\"multicast://default\" />" +
                                                   "   </transportConnectors>" +
                                                   "  </broker>" +
                                                   "</beans>");
      final String localProtocolScheme = "inline";
      URL.setURLStreamHandlerFactory(new URLStreamHandlerFactory() {
         @Override
         public URLStreamHandler createURLStreamHandler(String protocol) {
            if (localProtocolScheme.equalsIgnoreCase(protocol)) {
               return new URLStreamHandler() {
                  @Override
                  protected URLConnection openConnection(URL u) throws IOException {
                     return new URLConnection(u) {
                        @Override
                        public void connect() throws IOException {
                        }

                        @Override
                        public InputStream getInputStream() throws IOException {
                           return new ByteArrayInputStream(xmlConfigString.replace("%HOST%", url.getFile()).getBytes(StandardCharsets.UTF_8));
                        }
                     };
                  }
               };
            }
            return null;
         }
      });
      a = new XBeanBrokerFactory().createBroker(new URI("xbean:" + localProtocolScheme + ":A"));
      b = new XBeanBrokerFactory().createBroker(new URI("xbean:" + localProtocolScheme + ":B"));
      brokers.add(a);
      brokers.add(b);

      doTestNonAdvisoryNetworkRequestReply();
   }

   public void testNonAdvisoryNetworkRequestReply() throws Exception {
      createBridgeAndStartBrokers();
      doTestNonAdvisoryNetworkRequestReply();
   }

   public void testNonAdvisoryNetworkRequestReplyWithPIM() throws Exception {
      a = configureBroker("A");
      b = configureBroker("B");
      BrokerService hub = configureBroker("M");
      hub.setAllowTempAutoCreationOnSend(true);
      configureForPiggyInTheMiddle(bridge(a, hub));
      configureForPiggyInTheMiddle(bridge(b, hub));

      startBrokers();

      waitForBridgeFormation(hub, 2, 0);
      doTestNonAdvisoryNetworkRequestReply();
   }

   private void configureForPiggyInTheMiddle(NetworkConnector bridge) {
      bridge.setDuplex(true);
      bridge.setNetworkTTL(2);
   }

   public void doTestNonAdvisoryNetworkRequestReply() throws Exception {

      waitForBridgeFormation(a, 1, 0);
      waitForBridgeFormation(b, 1, 0);

      ActiveMQConnectionFactory sendFactory = createConnectionFactory(a);
      ActiveMQConnection sendConnection = createConnection(sendFactory);

      ActiveMQSession sendSession = (ActiveMQSession) sendConnection.createSession(false, Session.AUTO_ACKNOWLEDGE);
      MessageProducer producer = sendSession.createProducer(sendQ);
      ActiveMQTempQueue realReplyQ = (ActiveMQTempQueue) sendSession.createTemporaryQueue();
      TextMessage message = sendSession.createTextMessage("1");
      message.setJMSReplyTo(realReplyQ);
      producer.send(message);
      LOG.info("request sent");

      // responder
      ActiveMQConnectionFactory consumerFactory = createConnectionFactory(b);
      ActiveMQConnection consumerConnection = createConnection(consumerFactory);

      ActiveMQSession consumerSession = (ActiveMQSession) consumerConnection.createSession(false, Session.AUTO_ACKNOWLEDGE);
      MessageConsumer consumer = consumerSession.createConsumer(sendQ);
      TextMessage received = (TextMessage) consumer.receive(receiveTimeout);
      assertNotNull("got request from sender ok", received);

      LOG.info("got request, sending reply");

      MessageProducer consumerProducer = consumerSession.createProducer(received.getJMSReplyTo());
      consumerProducer.send(consumerSession.createTextMessage("got " + received.getText()));
      // temp dest on reply broker tied to this connection, setOptimizedDispatch=true ensures
      // message gets delivered before destination is removed
      consumerConnection.close();

      // reply consumer
      MessageConsumer replyConsumer = sendSession.createConsumer(realReplyQ);
      TextMessage reply = (TextMessage) replyConsumer.receive(receiveTimeout);
      assertNotNull("expected reply message", reply);
      assertEquals("text is as expected", "got 1", reply.getText());
      sendConnection.close();

      LOG.info("checking for dangling temp destinations");
      // ensure all temp dests get cleaned up on all brokers
      for (BrokerService brokerService : brokers) {
         final RegionBroker regionBroker = (RegionBroker) brokerService.getRegionBroker();
         assertTrue("all temps are gone on " + regionBroker.getBrokerName(), Wait.waitFor(new Wait.Condition() {
            @Override
            public boolean isSatisified() throws Exception {
               Map<?, ?> tempTopics = regionBroker.getTempTopicRegion().getDestinationMap();
               LOG.info("temp topics on " + regionBroker.getBrokerName() + ", " + tempTopics);
               Map<?, ?> tempQ = regionBroker.getTempQueueRegion().getDestinationMap();
               LOG.info("temp queues on " + regionBroker.getBrokerName() + ", " + tempQ);
               return tempQ.isEmpty() && tempTopics.isEmpty();
            }
         }));
      }
   }

   private ActiveMQConnection createConnection(ActiveMQConnectionFactory factory) throws Exception {
      ActiveMQConnection c = (ActiveMQConnection) factory.createConnection();
      c.start();
      return c;
   }

   private ActiveMQConnectionFactory createConnectionFactory(BrokerService brokerService) throws Exception {
      String target = brokerService.getTransportConnectors().get(0).getPublishableConnectString();
      ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(target);
      factory.setWatchTopicAdvisories(false);
      factory.setConnectionIDPrefix(connectionIdMarker + brokerService.getBrokerName());
      return factory;
   }

   public void createBridgeAndStartBrokers() throws Exception {
      a = configureBroker("A");
      b = configureBroker("B");
      bridge(a, b);
      bridge(b, a);
      startBrokers();
   }

   private void startBrokers() throws Exception {
      for (BrokerService broker : brokers) {
         broker.start();
      }
   }

   @Override
   public void tearDown() throws Exception {
      for (BrokerService broker : brokers) {
         broker.stop();
      }
      brokers.clear();
   }

   private NetworkConnector bridge(BrokerService from, BrokerService to) throws Exception {
      TransportConnector toConnector = to.getTransportConnectors().get(0);
      NetworkConnector bridge = from.addNetworkConnector("static://" + toConnector.getPublishableConnectString());
      bridge.addStaticallyIncludedDestination(sendQ);
      bridge.addStaticallyIncludedDestination(replyQWildcard);
      return bridge;
   }

   private BrokerService configureBroker(String brokerName) throws Exception {
      BrokerService broker = new BrokerService();
      broker.setBrokerName(brokerName);
      broker.setAdvisorySupport(false);
      broker.setPersistent(false);
      broker.setUseJmx(false);
      broker.setSchedulePeriodForDestinationPurge(1000);
      broker.setAllowTempAutoCreationOnSend(true);

      PolicyMap map = new PolicyMap();
      PolicyEntry tempReplyQPolicy = new PolicyEntry();
      tempReplyQPolicy.setOptimizedDispatch(true);
      tempReplyQPolicy.setGcInactiveDestinations(true);
      tempReplyQPolicy.setGcWithNetworkConsumers(true);
      tempReplyQPolicy.setInactiveTimeoutBeforeGC(1000);
      map.put(replyQWildcard, tempReplyQPolicy);
      broker.setDestinationPolicy(map);

      broker.addConnector("tcp://localhost:0");
      brokers.add(broker);
      return broker;
   }
}
