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

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import jakarta.jms.Connection;
import jakarta.jms.DeliveryMode;
import jakarta.jms.Destination;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.MessageConsumer;
import jakarta.jms.MessageListener;
import jakarta.jms.MessageProducer;
import jakarta.jms.Queue;
import jakarta.jms.Session;
import jakarta.jms.TextMessage;

import org.apache.activemq.command.ActiveMQDestination;
import org.apache.activemq.command.ActiveMQMessage;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.util.Wait;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.activemq.test.annotations.ParallelTest;
import org.junit.experimental.categories.Category;

@Category(ParallelTest.class)
public class MessageListenerRedeliveryTest {

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

    @Rule
    public TestName name = new TestName();

    private Connection connection;

    @Before
    public void setUp() throws Exception {
        connection = createConnection();
    }

    @After
    public void tearDown() throws Exception {
        if (connection != null) {
            connection.close();
            connection = null;
        }
    }

    protected String getTestName() {
        return name.getMethodName();
    }

    protected RedeliveryPolicy getRedeliveryPolicy() {
        final RedeliveryPolicy redeliveryPolicy = new RedeliveryPolicy();
        redeliveryPolicy.setInitialRedeliveryDelay(0);
        redeliveryPolicy.setRedeliveryDelay(1000);
        redeliveryPolicy.setMaximumRedeliveries(3);
        redeliveryPolicy.setBackOffMultiplier((short) 2);
        redeliveryPolicy.setUseExponentialBackOff(true);
        return redeliveryPolicy;
    }

    protected Connection createConnection() throws Exception {
        final ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("vm://localhost?broker.persistent=false&marshal=true");
        factory.setRedeliveryPolicy(getRedeliveryPolicy());
        return factory.createConnection();
    }

    private class TestMessageListener implements MessageListener {

        public volatile int counter;
        private final Session session;

        public TestMessageListener(Session session) {
            this.session = session;
        }

        @Override
        public void onMessage(Message message) {
            try {
                LOG.info("Message Received: " + message);
                counter++;
                if (counter <= 4) {
                    LOG.info("Message Rollback.");
                    session.rollback();
                } else {
                    LOG.info("Message Commit.");
                    message.acknowledge();
                    session.commit();
                }
            } catch (JMSException e) {
                LOG.error("Error when rolling back transaction");
            }
        }
    }

    @Test(timeout = 60000)
    public void testQueueRollbackConsumerListener() throws Exception {
        connection.start();

        final Session session = connection.createSession(true, Session.CLIENT_ACKNOWLEDGE);
        final Queue queue = session.createQueue("queue-" + getTestName());
        final MessageProducer producer = createProducer(session, queue);
        final Message message = createTextMessage(session);
        producer.send(message);
        session.commit();

        final MessageConsumer consumer = session.createConsumer(queue);

        final ActiveMQMessageConsumer mc = (ActiveMQMessageConsumer) consumer;
        mc.setRedeliveryPolicy(getRedeliveryPolicy());

        final TestMessageListener listener = new TestMessageListener(session);
        consumer.setMessageListener(listener);

        // first try.. should get 2 since there is no delay on the
        // first redeliver..
        assertTrue("first redelivery (counter==2)", Wait.waitFor(() -> listener.counter >= 2, 5000, 100));

        // 2nd redeliver (redelivery after 1 sec)
        assertTrue("second redelivery (counter==3)", Wait.waitFor(() -> listener.counter >= 3, 5000, 100));

        // 3rd redeliver (redelivery after 2 seconds) - it should give up after
        // that
        assertTrue("third redelivery (counter==4)", Wait.waitFor(() -> listener.counter >= 4, 5000, 100));

        // create new message
        producer.send(createTextMessage(session));
        session.commit();

        // it should be committed, so no redelivery
        assertTrue("committed message delivered (counter==5)", Wait.waitFor(() -> listener.counter >= 5, 5000, 100));

        // no redelivery, counter should still be 5 - sleep is acceptable here
        // because we are verifying that NO further redelivery occurs (negative test)
        Thread.sleep(1500);
        assertEquals(5, listener.counter);

        session.close();
    }

    @Test(timeout = 60000)
    public void testQueueRollbackSessionListener() throws Exception {
        connection.start();

        final Session session = connection.createSession(true, Session.CLIENT_ACKNOWLEDGE);
        final Queue queue = session.createQueue("queue-" + getTestName());
        final MessageProducer producer = createProducer(session, queue);
        final Message message = createTextMessage(session);
        producer.send(message);
        session.commit();

        final MessageConsumer consumer = session.createConsumer(queue);

        final ActiveMQMessageConsumer mc = (ActiveMQMessageConsumer) consumer;
        mc.setRedeliveryPolicy(getRedeliveryPolicy());

        final TestMessageListener listener = new TestMessageListener(session);
        consumer.setMessageListener(listener);

        // first try
        assertTrue("first redelivery (counter==2)", Wait.waitFor(() -> listener.counter >= 2, 5000, 100));

        // second try (redelivery after 1 sec)
        assertTrue("second redelivery (counter==3)", Wait.waitFor(() -> listener.counter >= 3, 5000, 100));

        // third try (redelivery after 2 seconds) - it should give up after that
        assertTrue("third redelivery (counter==4)", Wait.waitFor(() -> listener.counter >= 4, 5000, 100));

        // create new message
        producer.send(createTextMessage(session));
        session.commit();

        // it should be committed, so no redelivery
        assertTrue("committed message delivered (counter==5)", Wait.waitFor(() -> listener.counter >= 5, 5000, 100));

        // no redelivery, counter should still be 5 - sleep is acceptable here
        // because we are verifying that NO further redelivery occurs (negative test)
        Thread.sleep(1500);
        assertEquals(5, listener.counter);

        session.close();
    }

    @Test(timeout = 60000)
    public void testQueueSessionListenerExceptionRetry() throws Exception {
        connection.start();

        final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        final Queue queue = session.createQueue("queue-" + getTestName());
        final MessageProducer producer = createProducer(session, queue);
        Message message = createTextMessage(session, "1");
        producer.send(message);
        message = createTextMessage(session, "2");
        producer.send(message);

        final MessageConsumer consumer = session.createConsumer(queue);

        final CountDownLatch gotMessage = new CountDownLatch(2);
        final AtomicInteger count = new AtomicInteger(0);
        final int maxDeliveries = getRedeliveryPolicy().getMaximumRedeliveries();
        final ArrayList<String> received = new ArrayList<String>();
        consumer.setMessageListener(message1 -> {
            LOG.info("Message Received: " + message1);
            try {
                received.add(((TextMessage) message1).getText());
            } catch (JMSException e) {
                e.printStackTrace();
                fail(e.toString());
            }
            if (count.incrementAndGet() < maxDeliveries) {
                throw new RuntimeException(getTestName() + " force a redelivery");
            }
            // new blood
            count.set(0);
            gotMessage.countDown();
        });

        assertTrue("got message before retry expiry", gotMessage.await(20, TimeUnit.SECONDS));

        for (int i = 0; i < maxDeliveries; i++) {
            assertEquals("got first redelivered: " + i, "1", received.get(i));
        }
        for (int i = maxDeliveries; i < maxDeliveries * 2; i++) {
            assertEquals("got first redelivered: " + i, "2", received.get(i));
        }
        session.close();
    }

    @Test(timeout = 60000)
    public void testQueueSessionListenerExceptionDlq() throws Exception {
        connection.start();

        final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        final Queue queue = session.createQueue("queue-" + getTestName());
        final MessageProducer producer = createProducer(session, queue);
        Message message = createTextMessage(session);
        producer.send(message);

        final Message[] dlqMessage = new Message[1];
        final ActiveMQDestination dlqDestination = new ActiveMQQueue("ActiveMQ.DLQ");
        final MessageConsumer dlqConsumer = session.createConsumer(dlqDestination);
        final CountDownLatch gotDlqMessage = new CountDownLatch(1);
        dlqConsumer.setMessageListener(msg -> {
            LOG.info("DLQ Message Received: " + msg);
            dlqMessage[0] = msg;
            gotDlqMessage.countDown();
        });

        final MessageConsumer consumer = session.createConsumer(queue);

        final int maxDeliveries = getRedeliveryPolicy().getMaximumRedeliveries();
        final CountDownLatch gotMessage = new CountDownLatch(maxDeliveries);

        consumer.setMessageListener(msg -> {
            LOG.info("Message Received: " + msg);
            gotMessage.countDown();
            throw new RuntimeException(getTestName() + " force a redelivery");
        });

        assertTrue("got message before retry expiry", gotMessage.await(20, TimeUnit.SECONDS));

        // check DLQ
        assertTrue("got dlq message", gotDlqMessage.await(20, TimeUnit.SECONDS));

        // check DLQ message cause is captured
        message = dlqMessage[0];
        assertNotNull("dlq message captured", message);
        final String cause = message.getStringProperty(ActiveMQMessage.DLQ_DELIVERY_FAILURE_CAUSE_PROPERTY);

        LOG.info("DLQ'd message cause reported as: {}", cause);

        assertTrue("cause 'cause' exception is remembered", cause.contains("RuntimeException"));
        assertTrue("is correct exception", cause.contains(getTestName()));
        assertTrue("cause exception is remembered", cause.contains("Throwable"));
        assertTrue("cause policy is remembered", cause.contains("RedeliveryPolicy"));
        assertTrue("cause redelivered count is remembered", cause.contains("[" + (maxDeliveries+1) +"]"));

        session.close();
    }

    @Test(timeout = 60000)
    public void testTransactedQueueSessionListenerExceptionDlq() throws Exception {
        connection.start();

        final Session session = connection.createSession(true, Session.SESSION_TRANSACTED);
        final Queue queue = session.createQueue("queue-" + getTestName());
        final MessageProducer producer = createProducer(session, queue);
        Message message = createTextMessage(session);
        producer.send(message);
        session.commit();

        final Message[] dlqMessage = new Message[1];
        final ActiveMQDestination dlqDestination = new ActiveMQQueue("ActiveMQ.DLQ");
        final MessageConsumer dlqConsumer = session.createConsumer(dlqDestination);
        final CountDownLatch gotDlqMessage = new CountDownLatch(1);
        dlqConsumer.setMessageListener(msg -> {
            LOG.info("DLQ Message Received: " + msg);
            dlqMessage[0] = msg;
            gotDlqMessage.countDown();
        });

        final MessageConsumer consumer = session.createConsumer(queue);

        final int maxDeliveries = getRedeliveryPolicy().getMaximumRedeliveries();
        final CountDownLatch gotMessage = new CountDownLatch(maxDeliveries);

        consumer.setMessageListener(msg -> {
            LOG.info("Message Received: " + msg);
            gotMessage.countDown();
            try {
                session.rollback();
            } catch (JMSException e) {
                e.printStackTrace();
            }
            throw new RuntimeException(getTestName() + " force a redelivery");
        });

        assertTrue("got message before retry expiry", gotMessage.await(20, TimeUnit.SECONDS));

        // check DLQ
        assertTrue("got dlq message", gotDlqMessage.await(20, TimeUnit.SECONDS));

        // check DLQ message cause is captured
        message = dlqMessage[0];
        assertNotNull("dlq message captured", message);
        final String cause = message.getStringProperty(ActiveMQMessage.DLQ_DELIVERY_FAILURE_CAUSE_PROPERTY);

        LOG.info("DLQ'd message cause reported as: {}", cause);

        assertTrue("cause 'cause' exception is remembered", cause.contains("RuntimeException"));
        assertTrue("is correct exception", cause.contains(getTestName()));
        assertTrue("cause exception is remembered", cause.contains("Throwable"));
        assertTrue("cause policy is remembered", cause.contains("RedeliveryPolicy"));

        session.close();
    }

    private TextMessage createTextMessage(Session session, String text) throws JMSException {
        return session.createTextMessage(text);
    }

    private TextMessage createTextMessage(Session session) throws JMSException {
        return session.createTextMessage("Hello");
    }

    private MessageProducer createProducer(Session session, Destination queue) throws JMSException {
        final MessageProducer producer = session.createProducer(queue);
        producer.setDeliveryMode(getDeliveryMode());
        return producer;
    }

    protected int getDeliveryMode() {
        return DeliveryMode.PERSISTENT;
    }
}
