/**
 * 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 jakarta.jms.Connection;
import jakarta.jms.DeliveryMode;
import jakarta.jms.MessageConsumer;
import jakarta.jms.MessageProducer;
import jakarta.jms.Queue;
import jakarta.jms.Session;
import jakarta.jms.TextMessage;

import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.region.policy.PolicyEntry;
import org.apache.activemq.broker.region.policy.PolicyMap;

/**
 * Test case intended to demonstrate delivery interruption to queue consumers when a JMS selector leaves some messages
 * on the queue (due to use of a JMS Selector)
 *
 * testNonDiscriminatingConsumer() demonstrates proper functionality for consumers that don't use a selector to qualify
 * their input.
 *
 * testDiscriminatingConsumer() demonstrates the failure condition in which delivery to the consumer eventually halts.
 *
 * The expected behavior is for the delivery to the client to be maintained regardless of the depth of the queue,
 * particularly when the messages in the queue do not meet the selector criteria of the client.
 *
 * https://issues.apache.org/activemq/browse/AMQ-2217
 *
 */
public class DiscriminatingConsumerLoadTest extends TestSupport {

    private static final org.apache.commons.logging.Log LOG = org.apache.commons.logging.LogFactory.getLog(DiscriminatingConsumerLoadTest.class);

    private Connection producerConnection;
    private Connection consumerConnection;

    public static final String JMSTYPE_EATME = "DiscriminatingLoadClient.EatMe";
    public static final String JMSTYPE_IGNOREME = "DiscriminatingLoadClient.IgnoreMe";

    private final int testSize = 5000; // setting this to a small number will pass all tests

    BrokerService broker;

    @Override
    protected void setUp() throws Exception {
        broker = new BrokerService();
        broker.setPersistent(false);

        // workaround is to ensure sufficient dispatch buffer for the destination
        PolicyMap policyMap = new PolicyMap();
        PolicyEntry defaultPolicy = new PolicyEntry();
        defaultPolicy.setMaxPageSize(testSize);
        policyMap.setDefaultEntry(defaultPolicy);
        broker.setDestinationPolicy(policyMap);
        broker.start();

        super.setUp();
        this.producerConnection = this.createConnection();
        this.consumerConnection = this.createConnection();
    }

    /**
     * @see junit.framework.TestCase#tearDown()
     */
    @Override
    protected void tearDown() throws Exception {
        if (producerConnection != null) {
            producerConnection.close();
            producerConnection = null;
        }
        if (consumerConnection != null) {
            consumerConnection.close();
            consumerConnection = null;
        }
        super.tearDown();
        broker.stop();
    }

    /**
     * Test to check if a single consumer with no JMS selector will receive all intended messages
     *
     * @throws java.lang.Exception
     */
    public void testNonDiscriminatingConsumer() throws Exception {

        consumerConnection = createConnection();
        consumerConnection.start();
        LOG.info("consumerConnection = " + consumerConnection);

        try {
            Thread.sleep(1000);
        } catch (Exception e) {
        }

        // here we pass in null for the JMS selector
        Consumer consumer = new Consumer(consumerConnection, null);
        Thread consumerThread = new Thread(consumer);

        consumerThread.start();

        producerConnection = createConnection();
        producerConnection.start();
        LOG.info("producerConnection = " + producerConnection);

        try {
            Thread.sleep(3000);
        } catch (Exception e) {
        }

        Producer producer = new Producer(producerConnection);
        Thread producerThread = new Thread(producer);
        producerThread.start();

        // now that everything is running, let's wait for the consumer thread to finish ...
        consumerThread.join();
        producer.stop = true;

        if (consumer.getCount() == testSize)
            LOG.info("test complete .... all messsages consumed!!");
        else
            LOG.info("test failed .... Sent " + (testSize / 1) + " messages intended to be consumed ( " + testSize + " total), but only consumed "
                + consumer.getCount());

        assertTrue("Sent " + testSize + " messages intended to be consumed, but only consumed " + consumer.getCount(), (consumer.getCount() == testSize));
        assertFalse("Delivery of messages to consumer was halted during this test", consumer.deliveryHalted());
    }

    /**
     * Test to check if a single consumer with a JMS selector will receive all intended messages
     *
     * @throws java.lang.Exception
     */
    public void testDiscriminatingConsumer() throws Exception {

        consumerConnection = createConnection();
        consumerConnection.start();
        LOG.info("consumerConnection = " + consumerConnection);

        try {
            Thread.sleep(1000);
        } catch (Exception e) {
        }

        // here we pass the JMS selector we intend to consume
        Consumer consumer = new Consumer(consumerConnection, JMSTYPE_EATME);
        Thread consumerThread = new Thread(consumer);

        consumerThread.start();

        producerConnection = createConnection();
        producerConnection.start();
        LOG.info("producerConnection = " + producerConnection);

        try {
            Thread.sleep(3000);
        } catch (Exception e) {
        }

        Producer producer = new Producer(producerConnection);
        Thread producerThread = new Thread(producer);
        producerThread.start();

        // now that everything is running, let's wait for the consumer thread to finish ...
        consumerThread.join();
        producer.stop = true;

        if (consumer.getCount() == (testSize / 2)) {
            LOG.info("test complete .... all messsages consumed!!");
        } else {
            LOG.info("test failed .... Sent " + testSize + " original messages, only half of which (" + (testSize / 2)
                + ") were intended to be consumed: consumer paused at: " + consumer.getCount());
            // System.out.println("test failed .... Sent " + testSize + " original messages, only half of which (" +
            // (testSize / 2) +
            // ") were intended to be consumed: consumer paused at: " + consumer.getCount());

        }

        assertTrue("Sent " + testSize + " original messages, only half of which (" + (testSize / 2) + ") were intended to be consumed: consumer paused at: "
            + consumer.getCount(), (consumer.getCount() == (testSize / 2)));
        assertTrue("Delivery of messages to consumer was halted during this test as it only wants half", consumer.deliveryHalted());
    }

    /**
     * Helper class that will publish 2 * testSize messages. The messages will be distributed evenly between the
     * following two JMS types:
     *
     * @see JMSTYPE_INTENDED_FOR_CONSUMPTION
     * @see JMSTYPE_NOT_INTENDED_FOR_CONSUMPTION
     *
     */
    private class Producer extends Thread {
        private int counterSent = 0;
        private Connection connection = null;
        public boolean stop = false;

        public Producer(Connection connection) {
            this.connection = connection;
        }

        @Override
        public void run() {
            try {
                final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
                final Queue queue = session.createQueue("test");

                // wait for 10 seconds to allow consumer.receive to be run
                // first
                Thread.sleep(10000);
                MessageProducer producer = session.createProducer(queue);

                while (!stop && (counterSent < testSize)) {
                    // first send a message intended to be consumed ....
                    TextMessage message = session.createTextMessage("*** Ill ....... Ini ***"); // alma mater ...
                    message.setJMSType(JMSTYPE_EATME);
                    // LOG.info("sending .... JMSType = " + message.getJMSType());
                    producer.send(message, DeliveryMode.NON_PERSISTENT, 0, 1800000);

                    counterSent++;

                    // now send a message intended to be consumed by some other consumer in the the future
                    // ... we expect these messages to accrue in the queue
                    message = session.createTextMessage("*** Ill ....... Ini ***"); // alma mater ...
                    message.setJMSType(JMSTYPE_IGNOREME);
                    // LOG.info("sending .... JMSType = " + message.getJMSType());
                    producer.send(message, DeliveryMode.NON_PERSISTENT, 0, 1800000);

                    counterSent++;
                }

                session.close();

            } catch (Exception e) {
                e.printStackTrace();
            }
            LOG.info("producer thread complete ... " + counterSent + " messages sent to the queue");
        }
    }

    /**
     * Helper class that will consume messages from the queue based on the supplied JMS selector. Thread will stop after
     * the first receive(..) timeout, or once all expected messages have been received (see testSize). If the thread
     * stops due to a timeout, it is experiencing the delivery pause that is symptomatic of a bug in the broker.
     *
     */
    private class Consumer extends Thread {
        protected int counterReceived = 0;
        private String jmsSelector = null;
        private boolean deliveryHalted = false;

        public Consumer(Connection connection, String jmsSelector) {
            this.jmsSelector = jmsSelector;
        }

        @Override
        public void run() {
            try {
                Session session = consumerConnection.createSession(false, Session.AUTO_ACKNOWLEDGE);
                final Queue queue = session.createQueue("test");
                MessageConsumer consumer = null;
                if (null != this.jmsSelector) {
                    consumer = session.createConsumer(queue, "JMSType='" + this.jmsSelector + "'");
                } else {
                    consumer = session.createConsumer(queue);
                }

                while (!deliveryHalted && (counterReceived < testSize)) {
                    TextMessage result = (TextMessage) consumer.receive(30000);
                    if (result != null) {
                        counterReceived++;
                        // System.out.println("consuming .... JMSType = " + result.getJMSType() + " received = " +
                        // counterReceived);
                        LOG.info("consuming .... JMSType = " + result.getJMSType() + " received = " + counterReceived);
                    } else {
                        LOG.info("consuming .... timeout while waiting for a message ... broker must have stopped delivery ...  received = " + counterReceived);
                        deliveryHalted = true;
                    }
                }
                session.close();
            } catch (Exception e) {
                e.printStackTrace();
            }

        }

        public int getCount() {
            return this.counterReceived;
        }

        public boolean deliveryHalted() {
            return this.deliveryHalted;
        }
    }

}
