/**
 * 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 java.net.URI;
import java.util.Arrays;

import jakarta.jms.MessageConsumer;

import org.apache.activemq.JmsMultipleBrokersTestSupport;
import org.apache.activemq.advisory.AdvisorySupport;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.region.Destination;
import org.apache.activemq.broker.region.DestinationInterceptor;
import org.apache.activemq.broker.region.RegionBroker;
import org.apache.activemq.broker.region.virtual.CompositeTopic;
import org.apache.activemq.broker.region.virtual.VirtualDestination;
import org.apache.activemq.broker.region.virtual.VirtualDestinationInterceptor;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ActiveMQTopic;
import org.apache.activemq.command.BrokerInfo;
import org.apache.activemq.network.NetworkConnector;
import org.apache.activemq.util.MessageIdList;
import org.apache.activemq.util.Wait;
import org.apache.activemq.test.annotations.ParallelTest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.experimental.categories.Category;

@Category(ParallelTest.class)
public class AdvisoryViaNetworkTest extends JmsMultipleBrokersTestSupport {
    private static final Logger LOG = LoggerFactory.getLogger(AdvisoryViaNetworkTest.class);


    @Override
    protected BrokerService createBroker(String brokerName) throws Exception {
        BrokerService broker = new BrokerService();
        broker.setPersistent(false);
        broker.setUseJmx(false);
        broker.setBrokerName(brokerName);
        broker.addConnector(new URI(AUTO_ASSIGN_TRANSPORT));
        brokers.put(brokerName, new BrokerItem(broker));

        return broker;
    }

    public void testAdvisoryForwarding() throws Exception {
        ActiveMQTopic advisoryTopic = new ActiveMQTopic("ActiveMQ.Advisory.Producer.Topic.FOO");

        createBroker("A");
        createBroker("B");
        NetworkConnector networkBridge = bridgeBrokers("A", "B");
        networkBridge.addStaticallyIncludedDestination(advisoryTopic);
        startAllBrokers();
        verifyPeerBrokerInfo(brokers.get("A"), 1);


        MessageConsumer consumerA = createConsumer("A", advisoryTopic);
        MessageConsumer consumerB = createConsumer("B", advisoryTopic);

        this.sendMessages("A", new ActiveMQTopic("FOO"), 1);

        MessageIdList messagesA = getConsumerMessages("A", consumerA);
        MessageIdList messagesB = getConsumerMessages("B", consumerB);

        LOG.info("consumerA = " + messagesA);
        LOG.info("consumerB = " + messagesB);

        messagesA.assertMessagesReceived(2);
        messagesB.assertMessagesReceived(2);
    }

    /**
     * Test that explicitly setting advisoryPrefetchSize works for advisory topics
     * on a network connector
     *
     * @throws Exception
     */
    public void testAdvisoryPrefetchSize() throws Exception {
        ActiveMQTopic advisoryTopic = new ActiveMQTopic("ActiveMQ.Advisory.Consumer.Topic.A.>");
        ActiveMQTopic topic1 = new ActiveMQTopic("A.FOO");

        createBroker("A");
        BrokerService brokerB = createBroker("B");
        NetworkConnector networkBridge = bridgeBrokers("A", "B");
        networkBridge.addStaticallyIncludedDestination(advisoryTopic);
        networkBridge.addStaticallyIncludedDestination(topic1);
        networkBridge.setDuplex(true);
        networkBridge.setAdvisoryPrefetchSize(10);
        networkBridge.setPrefetchSize(1);

        startAllBrokers();
        verifyPeerBrokerInfo(brokers.get("A"), 1);
        verifyPeerBrokerInfo(brokers.get("B"), 1);

        createConsumer("A", topic1);
        createConsumer("A", new ActiveMQTopic("A.FOO2"));

        // Wait for network bridge to propagate consumers to broker B
        waitForConsumerOnBroker(brokerB, advisoryTopic);
        waitForConsumerOnBroker(brokerB, topic1);

        //verify that brokerB's advisory prefetch is 10 but normal topic prefetch is 1
        assertEquals(10, brokerB.getDestination(advisoryTopic).getConsumers().get(0).getPrefetchSize());
        assertEquals(1, brokerB.getDestination(topic1).getConsumers().get(0).getPrefetchSize());

        //both advisory messages are not acked yet because of optimized acks
        assertDeqInflight(0, 2, topic1, new ActiveMQTopic("A.FOO2"));
    }

    /**
     * Test that explicitly setting advisoryPrefetchSize to 1 works for advisory topics
     * on a network connector
     *
     * @throws Exception
     */
    public void testAdvisoryPrefetchSize1() throws Exception {
        ActiveMQTopic advisoryTopic = new ActiveMQTopic("ActiveMQ.Advisory.Consumer.Topic.A.>");
        ActiveMQTopic topic1 = new ActiveMQTopic("A.FOO");

        createBroker("A");
        BrokerService brokerB = createBroker("B");
        NetworkConnector networkBridge = bridgeBrokers("A", "B");
        networkBridge.addStaticallyIncludedDestination(advisoryTopic);
        networkBridge.addStaticallyIncludedDestination(topic1);
        networkBridge.setDuplex(true);
        networkBridge.setAdvisoryPrefetchSize(1);
        networkBridge.setPrefetchSize(10);

        startAllBrokers();
        verifyPeerBrokerInfo(brokers.get("A"), 1);
        verifyPeerBrokerInfo(brokers.get("B"), 1);

        createConsumer("A", topic1);
        createConsumer("A", new ActiveMQTopic("A.FOO2"));

        // Wait for network bridge to propagate consumers to broker B
        waitForConsumerOnBroker(brokerB, advisoryTopic);
        waitForConsumerOnBroker(brokerB, topic1);

        //verify that brokerB's advisory prefetch is 1 but normal topic prefetch is 10
        assertEquals(1, brokerB.getDestination(advisoryTopic).getConsumers().get(0).getPrefetchSize());
        assertEquals(10, brokerB.getDestination(topic1).getConsumers().get(0).getPrefetchSize());

        assertDeqInflight(2, 0, topic1, new ActiveMQTopic("A.FOO2"));
    }

    /**
     * Test that if advisoryPrefetchSize isn't set then prefetchSize is used instead
     * for backwards compatibility
     *
     * @throws Exception
     */
    public void testAdvisoryPrefetchSizeNotSet() throws Exception {
        ActiveMQTopic advisoryTopic = new ActiveMQTopic("ActiveMQ.Advisory.Consumer.Topic.A.>");
        ActiveMQTopic topic1 = new ActiveMQTopic("A.FOO");

        createBroker("A");
        BrokerService brokerB = createBroker("B");
        NetworkConnector networkBridge = bridgeBrokers("A", "B");
        networkBridge.addStaticallyIncludedDestination(advisoryTopic);
        networkBridge.addStaticallyIncludedDestination(topic1);
        networkBridge.setDuplex(true);
        networkBridge.setPrefetchSize(10);

        startAllBrokers();
        verifyPeerBrokerInfo(brokers.get("A"), 1);
        verifyPeerBrokerInfo(brokers.get("B"), 1);

        createConsumer("A", topic1);
        createConsumer("A", new ActiveMQTopic("A.FOO2"));

        // Wait for network bridge to propagate consumers to broker B
        waitForConsumerOnBroker(brokerB, advisoryTopic);
        waitForConsumerOnBroker(brokerB, topic1);

        //verify that both consumers have a prefetch of 10
        assertEquals(10, brokerB.getDestination(advisoryTopic).getConsumers().get(0).getPrefetchSize());
        assertEquals(10, brokerB.getDestination(topic1).getConsumers().get(0).getPrefetchSize());

        assertDeqInflight(0, 2, topic1, new ActiveMQTopic("A.FOO2"));
    }

    /**
     * Test that if advisoryPrefetchSize isn't set then prefetchSize is used instead
     * for backwards compatibility (test when set to 1)
     *
     * @throws Exception
     */
    public void testPrefetchSize1() throws Exception {
        ActiveMQTopic advisoryTopic = new ActiveMQTopic("ActiveMQ.Advisory.Consumer.Topic.A.>");
        ActiveMQTopic topic1 = new ActiveMQTopic("A.FOO");

        createBroker("A");
        BrokerService brokerB = createBroker("B");
        NetworkConnector networkBridge = bridgeBrokers("A", "B");
        networkBridge.addStaticallyIncludedDestination(advisoryTopic);
        networkBridge.setDuplex(true);
        networkBridge.setPrefetchSize(1);

        startAllBrokers();
        verifyPeerBrokerInfo(brokers.get("A"), 1);
        verifyPeerBrokerInfo(brokers.get("B"), 1);

        createConsumer("A", topic1);
        createConsumer("A", new ActiveMQTopic("A.FOO2"));

        // Wait for network bridge to propagate consumers to broker B
        waitForConsumerOnBroker(brokerB, advisoryTopic);
        waitForConsumerOnBroker(brokerB, topic1);

        //verify that both consumers have a prefetch of 1
        assertEquals(1, brokerB.getDestination(advisoryTopic).getConsumers().get(0).getPrefetchSize());
        assertEquals(1, brokerB.getDestination(topic1).getConsumers().get(0).getPrefetchSize());

        assertDeqInflight(2, 0, topic1, new ActiveMQTopic("A.FOO2"));
    }

    /**
     * Test configuring the advisoryAckPercentage works with advisoryPrefetchSize
     * @throws Exception
     */
    public void testAdvisoryPrefetchSizePercent() throws Exception {
        ActiveMQTopic advisoryTopic = new ActiveMQTopic("ActiveMQ.Advisory.Consumer.Topic.A.>");

        createBroker("A");
        createBroker("B");
        NetworkConnector networkBridge = bridgeBrokers("A", "B");
        networkBridge.addStaticallyIncludedDestination(advisoryTopic);
        networkBridge.setDuplex(true);
        networkBridge.setAdvisoryPrefetchSize(10);
        networkBridge.setAdvisoryAckPercentage(65);

        startAllBrokers();
        verifyPeerBrokerInfo(brokers.get("A"), 1);
        verifyPeerBrokerInfo(brokers.get("B"), 1);

        for (int i = 0; i < 10; i++) {
            createConsumer("A", new ActiveMQTopic("A.FOO"));
        }

        assertDeqInflight(7, 3, new ActiveMQTopic("A.FOO"));
    }

    /**
     * Test configuring the advisoryAckPercentage works when only prefetchSize exists
     * and is applied against that instead for advisory consumers
     *
     * @throws Exception
     */
    public void testPrefetchSizePercent() throws Exception {
        ActiveMQTopic advisoryTopic = new ActiveMQTopic("ActiveMQ.Advisory.Consumer.Topic.A.>");

        createBroker("A");
        createBroker("B");
        NetworkConnector networkBridge = bridgeBrokers("A", "B");
        networkBridge.addStaticallyIncludedDestination(advisoryTopic);
        networkBridge.setDuplex(true);
        networkBridge.setPrefetchSize(10);
        networkBridge.setAdvisoryAckPercentage(65);

        startAllBrokers();
        verifyPeerBrokerInfo(brokers.get("A"), 1);
        verifyPeerBrokerInfo(brokers.get("B"), 1);

        for (int i = 0; i < 10; i++) {
            createConsumer("A", new ActiveMQTopic("A.FOO"));
        }

        assertDeqInflight(7, 3, new ActiveMQTopic("A.FOO"));
    }

    private void assertDeqInflight(final int dequeue, final int inflight,
                                   final ActiveMQTopic... topics) throws Exception {
        // Use >= instead of == because duplex bridges with statically included destinations
        // may generate additional advisory messages from the bridge's own subscriptions,
        // depending on subscription registration ordering.
        assertTrue("deq and inflight as expected", Wait.waitFor(() -> {
            long actualDeq = 0;
            long actualInflight = 0;
            for (final ActiveMQTopic topic : topics) {
                final ActiveMQTopic advisory = AdvisorySupport.getConsumerAdvisoryTopic(topic);
                final Destination destination = brokers.get("A").broker.getDestination(advisory);
                if (destination != null) {
                    actualDeq += destination.getDestinationStatistics().getDequeues().getCount();
                    actualInflight += destination.getDestinationStatistics().getInflight().getCount();
                }
            }
            LOG.info("A Deq:{} (expected >={}), Inflight:{} (expected >={})",
                    actualDeq, dequeue, actualInflight, inflight);
            return actualDeq >= dequeue && actualInflight >= inflight;
        }));
    }


    public void testAdvisoryForwardingDuplexNC() throws Exception {
        ActiveMQTopic advisoryTopic = new ActiveMQTopic("ActiveMQ.Advisory.Producer.Topic.FOO");

        createBroker("A");
        createBroker("B");
        NetworkConnector networkBridge = bridgeBrokers("A", "B");
        networkBridge.addStaticallyIncludedDestination(advisoryTopic);
        networkBridge.setDuplex(true);
        startAllBrokers();
        verifyPeerBrokerInfo(brokers.get("A"), 1);
        verifyPeerBrokerInfo(brokers.get("B"), 1);


        MessageConsumer consumerA = createConsumer("A", advisoryTopic);
        MessageConsumer consumerB = createConsumer("B", advisoryTopic);

        this.sendMessages("A", new ActiveMQTopic("FOO"), 1);

        MessageIdList messagesA = getConsumerMessages("A", consumerA);
        MessageIdList messagesB = getConsumerMessages("B", consumerB);

        LOG.info("consumerA = " + messagesA);
        LOG.info("consumerB = " + messagesB);

        messagesA.assertMessagesReceived(2);
        messagesB.assertMessagesReceived(2);
    }

    public void testBridgeRelevantAdvisoryNotAvailable() throws Exception {
        ActiveMQTopic advisoryTopic = new ActiveMQTopic("ActiveMQ.Advisory.Consumer.Topic.FOO");
        createBroker("A");
        createBroker("B");
        NetworkConnector networkBridge = bridgeBrokers("A", "B");
        networkBridge.addStaticallyIncludedDestination(advisoryTopic);

        startAllBrokers();
        verifyPeerBrokerInfo(brokers.get("A"), 1);


        MessageConsumer consumerA = createConsumer("A", advisoryTopic);
        MessageConsumer consumerB = createConsumer("B", advisoryTopic);

        createConsumer("A", new ActiveMQTopic("FOO"));

        MessageIdList messagesA = getConsumerMessages("A", consumerA);
        MessageIdList messagesB = getConsumerMessages("B", consumerB);

        LOG.info("consumerA = " + messagesA);
        LOG.info("consumerB = " + messagesB);

        messagesA.assertMessagesReceived(1);
        messagesB.assertMessagesReceived(0);
    }

    public void testAdvisoryViaVirtualDest() throws Exception {
        ActiveMQQueue advisoryQueue = new ActiveMQQueue("advQ");
        createBroker("A");

        // convert advisories into advQ that cross the network bridge
        CompositeTopic compositeTopic = new CompositeTopic();
        compositeTopic.setName("ActiveMQ.Advisory.Connection");
        compositeTopic.setForwardOnly(false);
        compositeTopic.setForwardTo(Arrays.asList(advisoryQueue));
        VirtualDestinationInterceptor virtualDestinationInterceptor = new VirtualDestinationInterceptor();
        virtualDestinationInterceptor.setVirtualDestinations(new VirtualDestination[]{compositeTopic});
        brokers.get("A").broker.setDestinationInterceptors(new DestinationInterceptor[]{virtualDestinationInterceptor});

        createBroker("B");
        NetworkConnector networkBridge = bridgeBrokers("A", "B");
        networkBridge.setDuplex(true);
        networkBridge.setPrefetchSize(1); // so advisories are acked immediately b/c we check inflight count below

        startAllBrokers();
        verifyPeerBrokerInfo(brokers.get("A"), 1);
        verifyPeerBrokerInfo(brokers.get("B"), 1);

        MessageConsumer consumerB = createConsumer("B", advisoryQueue);

        // to make a connection on A
        createConsumer("A", new ActiveMQTopic("FOO"));

        MessageIdList messagesB = getConsumerMessages("B", consumerB);

        messagesB.waitForMessagesToArrive(2);

        assertTrue("deq and inflight as expected", Wait.waitFor(new Wait.Condition() {
            @Override
            public boolean isSatisified() throws Exception {
                RegionBroker regionBroker = (RegionBroker) brokers.get("A").broker.getRegionBroker();
                LOG.info("A Deq:" + regionBroker.getDestinationStatistics().getDequeues().getCount());
                LOG.info("A Inflight:" + regionBroker.getDestinationStatistics().getInflight().getCount());
                return regionBroker.getDestinationStatistics().getDequeues().getCount() > 2
                        && regionBroker.getDestinationStatistics().getInflight().getCount() == 0;
            }
        }));

    }

    private void verifyPeerBrokerInfo(BrokerItem brokerItem, final int max) throws Exception {
        final BrokerService broker = brokerItem.broker;
        final RegionBroker regionBroker = (RegionBroker) broker.getRegionBroker();
        Wait.waitFor(new Wait.Condition() {
            @Override
            public boolean isSatisified() throws Exception {
                LOG.info("verify infos " + broker.getBrokerName() + ", len: " + regionBroker.getPeerBrokerInfos().length);
                return max == regionBroker.getPeerBrokerInfos().length;
            }
         }, 120 * 1000);
        LOG.info("verify infos " + broker.getBrokerName() + ", len: " + regionBroker.getPeerBrokerInfos().length);
        for (BrokerInfo info : regionBroker.getPeerBrokerInfos()) {
            LOG.info(info.getBrokerName());
        }
        assertEquals(broker.getBrokerName(), max, regionBroker.getPeerBrokerInfos().length);
    }


    @Override
    public void setUp() throws Exception {
        super.setAutoFail(true);
        super.setUp();
    }

    private void waitForConsumerOnBroker(final BrokerService broker, final ActiveMQTopic topic) throws Exception {
        assertTrue("Consumer on " + broker.getBrokerName() + " for " + topic,
            Wait.waitFor(() -> {
                try {
                    return !broker.getDestination(topic).getConsumers().isEmpty();
                } catch (final Exception e) {
                    return false;
                }
            }, 30000, 100));
    }

}
