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

import static org.junit.Assert.*;

import java.net.URI;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

import jakarta.jms.DeliveryMode;
import jakarta.jms.Destination;
import jakarta.jms.ExceptionListener;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.MessageConsumer;
import jakarta.jms.MessageListener;
import jakarta.jms.Session;
import jakarta.jms.TextMessage;

import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQMessageProducer;
import org.apache.activemq.ActiveMQSession;
import org.apache.activemq.RedeliveryPolicy;
import org.apache.activemq.broker.BrokerFactory;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.util.Wait;
import org.apache.activemq.util.Wait.Condition;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Test validates that the AMQ consumer blocks on redelivery of a message,
 * through all redeliveries, until the message is either successfully consumed
 * or sent to the DLQ.
 */
public class AMQ1853Test {
    private static BrokerService broker;

    private static final Logger LOG = LoggerFactory.getLogger(AMQ1853Test.class);
    static final String jmsConnectionURI = "failover:(vm://localhost)";

    // Virtual Topic that the test publishes 10 messages to
    private static final String queueFail = "Queue.BlockingConsumer.QueueFail";

    // Number of messages

    private final int producerMessages = 5;
    private final int totalNumberMessages = producerMessages * 2;
    private final int maxRedeliveries = 2;
    private final int redeliveryDelay = 1000;

    private Map<String, AtomicInteger> messageList = null;

    @Before
    public void setUp() throws Exception {
        broker = BrokerFactory.createBroker(new URI("broker:()/localhost?persistent=false"));
        broker.setUseJmx(false);
        broker.setDeleteAllMessagesOnStartup(true);
        broker.start();
        broker.waitUntilStarted();
    }

    @After
    public void tearDown() throws Exception {
        if (broker != null) {
            broker.stop();
            broker.waitUntilStopped();
            broker = null;
        }
    }

    @Test
    public void testConsumerMessagesAreNotOrdered() throws Exception {

        TestConsumer consumerAllFail = null;
        messageList = new Hashtable<String, AtomicInteger>();

        try {

            // The first 2 consumers will rollback, ultimately causing messages to land on the DLQ

            TestProducer producerAllFail = new TestProducer(queueFail);
            thread(producerAllFail, false);

            consumerAllFail = new TestConsumer(queueFail, true);
            thread(consumerAllFail, false);

            // Give the consumers a second to start
            Thread.sleep(1000);

            thread(producerAllFail, false);

            // Give the consumers a second to start
            Thread.sleep(1000);

            producerAllFail.getLatch().await();

            LOG.info("producer successful, count = " + producerAllFail.getLatch().getCount());
            LOG.info("final message list size =  " + messageList.size());

            assertTrue("message list size =  " + messageList.size() + " exptected:" + totalNumberMessages,
                Wait.waitFor(new Condition() {
                    @Override
                    public boolean isSatisified() throws Exception {
                        return totalNumberMessages == messageList.size();
                    }
                }));

            consumerAllFail.getLatch().await();

            LOG.info("consumerAllFail successful, count = " + consumerAllFail.getLatch().getCount());

            Iterator<String> keys = messageList.keySet().iterator();
            for (AtomicInteger counter : messageList.values()) {
                String message = keys.next();
                LOG.info("final count for message " + message + " counter =  " + counter.get());
                assertTrue("for message " + message + " counter =  " + counter.get(), counter.get() == maxRedeliveries + 1);
            }

            assertFalse(consumerAllFail.messageReceiptIsOrdered());
        } finally {
            if (consumerAllFail != null) {
                consumerAllFail.setStop(true);
            }
        }
    }

    private static Thread thread(Runnable runnable, boolean daemon) {
        Thread brokerThread = new Thread(runnable);
        brokerThread.setDaemon(daemon);
        brokerThread.start();
        return brokerThread;
    }

    private class TestProducer implements Runnable {

        private CountDownLatch latch = null;
        private String destinationName = null;

        public TestProducer(String destinationName) {
            this.destinationName = destinationName;
            // We run the producer 2 times
            latch = new CountDownLatch(totalNumberMessages);
        }

        public CountDownLatch getLatch() {
            return latch;
        }

        public void run() {

            ActiveMQConnectionFactory connectionFactory = null;
            ActiveMQConnection connection = null;
            ActiveMQSession session = null;
            Destination destination = null;

            try {
                LOG.info("Started TestProducer for destination (" + destinationName + ")");

                connectionFactory = new ActiveMQConnectionFactory(jmsConnectionURI);
                connection = (ActiveMQConnection) connectionFactory.createConnection();
                connection.setCopyMessageOnSend(false);
                connection.start();
                session = (ActiveMQSession) connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

                destination = session.createQueue(this.destinationName);

                // Create a MessageProducer from the Session to the Topic or Queue
                ActiveMQMessageProducer producer = (ActiveMQMessageProducer) session.createProducer(destination);
                producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

                for (int i = 0; i < (producerMessages); i++) {
                    TextMessage message = (TextMessage) session.createTextMessage();
                    message.setLongProperty("TestTime", (System.currentTimeMillis()));
                    try {
                        producer.send(message);
                        LOG.info("Producer (" + destinationName + ")\n" + message.getJMSMessageID() + " = sent messageId\n");

                        latch.countDown();
                        LOG.info(" Latch count  " + latch.getCount());
                        LOG.info("Producer message list size = " + messageList.keySet().size());
                        messageList.put(message.getJMSMessageID(), new AtomicInteger(0));
                        LOG.info("Producer message list size = " + messageList.keySet().size());

                    } catch (Exception deeperException) {
                        LOG.info("Producer for destination (" + destinationName + ") Caught: " + deeperException);
                    }

                    Thread.sleep(1000);
                }

                LOG.info("Finished TestProducer for destination (" + destinationName + ")");

            } catch (Exception e) {
                LOG.error("Terminating TestProducer(" + destinationName + ")Caught: " + e);
            } finally {
                try {
                    if (session != null) {
                        session.close();
                    }
                    if (connection != null) {
                        connection.close();
                    }
                } catch (Exception e) {
                    LOG.error("Closing connection/session (" + destinationName + ")Caught: " + e);
                }
            }
        }
    }

    private class TestConsumer implements Runnable, ExceptionListener, MessageListener {

        private CountDownLatch latch = null;
        private int receivedMessageCounter = 0;
        private boolean bFakeFail = false;
        String destinationName = null;
        boolean bMessageReceiptIsOrdered = true;
        boolean bStop = false;
        String previousMessageId = null;

        private ActiveMQConnectionFactory connectionFactory = null;
        private ActiveMQConnection connection = null;
        private Session session = null;
        private MessageConsumer consumer = null;

        public TestConsumer(String destinationName, boolean bFakeFail) {
            this.bFakeFail = bFakeFail;
            latch = new CountDownLatch(totalNumberMessages * (this.bFakeFail ? (maxRedeliveries + 1) : 1));
            this.destinationName = destinationName;
        }

        public CountDownLatch getLatch() {
            return latch;
        }

        public boolean messageReceiptIsOrdered() {
            return bMessageReceiptIsOrdered;
        }

        public void run() {

            try {
                LOG.info("Started TestConsumer for destination (" + destinationName + ")");

                connectionFactory = new ActiveMQConnectionFactory(jmsConnectionURI);
                connection = (ActiveMQConnection) connectionFactory.createConnection();
                connection.setNonBlockingRedelivery(true);
                session = connection.createSession(true, Session.SESSION_TRANSACTED);

                RedeliveryPolicy policy = connection.getRedeliveryPolicy();
                policy.setInitialRedeliveryDelay(redeliveryDelay);
                policy.setBackOffMultiplier(-1);
                policy.setRedeliveryDelay(redeliveryDelay);
                policy.setMaximumRedeliveryDelay(-1);
                policy.setUseExponentialBackOff(false);
                policy.setMaximumRedeliveries(maxRedeliveries);

                connection.setExceptionListener(this);
                Destination destination = session.createQueue(destinationName);
                consumer = session.createConsumer(destination);
                consumer.setMessageListener(this);

                connection.start();

                while (!bStop) {
                    Thread.sleep(100);
                }

                LOG.info("Finished TestConsumer for destination name (" + destinationName + ") remaining " + this.latch.getCount()
                        + " messages " + this.toString());

            } catch (Exception e) {
                LOG.error("Consumer (" + destinationName + ") Caught: " + e);
            } finally {
                try {
                    if (consumer != null) {
                        consumer.close();
                    }
                    if (session != null) {
                        session.close();
                    }
                    if (connection != null) {
                        connection.close();
                    }
                } catch (Exception e) {
                    LOG.error("Closing connection/session (" + destinationName + ")Caught: " + e);
                }
            }
        }

        public synchronized void onException(JMSException ex) {
            LOG.error("Consumer for destination, (" + destinationName + "), JMS Exception occured.  Shutting down client.");
        }

        public synchronized void setStop(boolean bStop) {
            this.bStop = bStop;
        }

        public synchronized void onMessage(Message message) {
            receivedMessageCounter++;
            latch.countDown();

            LOG.info("Consumer for destination (" + destinationName + ") latch countdown: " + latch.getCount() +
                     " :: Number messages received " + this.receivedMessageCounter);

            try {

                if (receivedMessageCounter % (maxRedeliveries + 1) == 1) {
                    previousMessageId = message.getJMSMessageID();
                }

                if (bMessageReceiptIsOrdered) {
                    bMessageReceiptIsOrdered = previousMessageId.trim().equals(message.getJMSMessageID());
                }

                final String jmsMessageId = message.getJMSMessageID();
                assertTrue("Did not find expected ", Wait.waitFor(new Wait.Condition() {
                    @Override
                    public boolean isSatisified() throws Exception {
                        return messageList.containsKey(jmsMessageId);
                    }
                }));

                AtomicInteger counter = messageList.get(jmsMessageId);
                counter.incrementAndGet();

                LOG.info("Consumer for destination (" + destinationName + ")\n" + message.getJMSMessageID() + " = currentMessageId\n"
                        + previousMessageId + " = previousMessageId\n" + bMessageReceiptIsOrdered + "= bMessageReceiptIsOrdered\n"
                        + ">>LATENCY " + (System.currentTimeMillis() - message.getLongProperty("TestTime")) + "\n" + "message counter = "
                        + counter.get());

                if (!bFakeFail) {
                    LOG.debug("Consumer on destination " + destinationName + " committing JMS Session for message: " + message.toString());
                    session.commit();
                } else {
                    LOG.debug("Consumer on destination " + destinationName + " rolling back JMS Session for message: " + message.toString());
                    session.rollback(); // rolls back all the consumed messages on the session to
                }

            } catch (Exception ex) {
                ex.printStackTrace();
                LOG.error("Error reading JMS Message from destination " + destinationName + ".");
            }
        }
    }
}
