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

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import jakarta.jms.Connection;
import jakarta.jms.DeliveryMode;
import jakarta.jms.Destination;
import jakarta.jms.ExceptionListener;
import jakarta.jms.JMSException;
import jakarta.jms.MapMessage;
import jakarta.jms.Message;
import jakarta.jms.MessageConsumer;
import jakarta.jms.MessageProducer;
import jakarta.jms.ObjectMessage;
import jakarta.jms.Session;
import jakarta.jms.TextMessage;
import jakarta.jms.Topic;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQSession;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.TransportConnector;
import org.apache.activemq.broker.region.policy.PolicyEntry;
import org.apache.activemq.broker.region.policy.PolicyMap;
import org.junit.After;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This test can also be used to debug AMQ-6218 and AMQ-6221
 *
 * This test shows that messages are received with non-null data while
 * several consumers are used.
 */
public abstract class AbstractVmConcurrentDispatchTest {

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

    private final MessageType messageType;
    private final boolean reduceMemoryFootPrint;
    protected final boolean useTopic;

    protected static enum MessageType {TEXT, MAP, OBJECT}
    protected final static boolean[] booleanVals = {true, false};
    protected static boolean[] reduceMemoryFootPrintVals = booleanVals;
    protected static boolean[] useTopicVals = booleanVals;
    private String testTopicName = "mytopic";

    @Rule
    public TemporaryFolder dataFileDir = new TemporaryFolder(new File("target"));

    public AbstractVmConcurrentDispatchTest(MessageType messageType, boolean reduceMemoryFootPrint,
            boolean useTopic) {
        this.messageType = messageType;
        this.reduceMemoryFootPrint = reduceMemoryFootPrint;
        this.useTopic = useTopic;
    }

    private BrokerService broker;
    private final AtomicBoolean failure = new AtomicBoolean();
    private CountDownLatch ready;
    private URI connectionURI;
    private URI vmConnectionURI;

    private final boolean USE_VM_TRANSPORT = true;

    private final int NUM_CONSUMERS = 30;
    private final int NUM_PRODUCERS = 1;
    private final int NUM_TASKS = NUM_CONSUMERS + NUM_PRODUCERS;

    private final AtomicInteger count = new AtomicInteger();
    private String MessageId = null;
    private int MessageCount = 0;

    @Before
    public void setUp() throws Exception {
        broker = new BrokerService();
        TransportConnector connector = broker.addConnector("tcp://0.0.0.0:0");
        broker.setDeleteAllMessagesOnStartup(true);
        PolicyMap policyMap = new PolicyMap();
        PolicyEntry defaultPolicy = new PolicyEntry();
        defaultPolicy.setReduceMemoryFootprint(reduceMemoryFootPrint);
        policyMap.setDefaultEntry(defaultPolicy);
        broker.setDestinationPolicy(policyMap);
        broker.setDataDirectoryFile(dataFileDir.getRoot());
        configurePersistenceAdapter(broker);
        broker.start();
        broker.waitUntilStarted();

        ready = new CountDownLatch(NUM_TASKS);
        connectionURI = connector.getPublishableConnectURI();
        vmConnectionURI = broker.getVmConnectorURI();
    }

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

    protected abstract void configurePersistenceAdapter(final BrokerService broker) throws IOException;

    @Test(timeout=180000)
    public void testMessagesAreValid() throws Exception {
        if (this.useTopic) {
            Assume.assumeTrue(reduceMemoryFootPrint);
        }

        ExecutorService tasks = Executors.newFixedThreadPool(NUM_TASKS);
        for (int i = 0; i < NUM_CONSUMERS; i++) {
            LOG.info("Created Consumer: {}", i + 1);
            tasks.execute(new HelloWorldConsumer(useTopic));
        }

        for (int i = 0; i < NUM_PRODUCERS; i++) {
            LOG.info("Created Producer: {}", i + 1);
            tasks.execute(new HelloWorldProducer(useTopic));
        }

        assertTrue(ready.await(20, TimeUnit.SECONDS));

        try {
            tasks.shutdown();
            //run for 10 seconds as that seems to be enough time to cause an error
            //if there is going to be one
            tasks.awaitTermination(10, TimeUnit.SECONDS);
        } catch (Exception e) {
            //should get exception with no errors
        }

        assertFalse("Test Encountered a null bodied message", failure.get());
    }

    public URI getBrokerURI() {
        if (USE_VM_TRANSPORT) {
            return vmConnectionURI;
        } else {
            return connectionURI;
        }
    }

    public class HelloWorldProducer implements Runnable {

        final boolean useTopic;

        public HelloWorldProducer(boolean useTopic) {
            this.useTopic = useTopic;
        }

        @Override
        public void run() {
            try {
                ActiveMQConnectionFactory connectionFactory =
                    new ActiveMQConnectionFactory(getBrokerURI());

                Connection connection = connectionFactory.createConnection();
                connection.start();

                Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

                //If using topics, just test a generic topic name
                //If useTopic is false then we are testing virtual topics/queue consumes
                Destination destination = useTopic ? session.createTopic(testTopicName) :
                    session.createTopic("VirtualTopic.AMQ6218Test");

                MessageProducer producer = session.createProducer(destination);

                LOG.info("Producer: {}", destination);

                producer.setDeliveryMode(DeliveryMode.PERSISTENT);
                producer.setPriority(4);
                producer.setTimeToLive(0);

                ready.countDown();

                int j = 0;
                while (!failure.get()) {
                    j++;
                    String text = "AMQ Message Number :" + j;
                    Message message = null;
                    if (messageType.equals(MessageType.MAP)) {
                        MapMessage mapMessage = session.createMapMessage();
                        mapMessage.setString("text", text);
                        message = mapMessage;
                    } else if (messageType.equals(MessageType.OBJECT)) {
                        ObjectMessage objectMessage = session.createObjectMessage();
                        objectMessage.setObject(text);
                        message = objectMessage;
                    } else {
                        message = session.createTextMessage(text);
                    }
                    producer.send(message);
                    LOG.info("Sent message: {}", message.getJMSMessageID());
                }

                connection.close();
            } catch (Exception e) {
                LOG.error("Caught: " + e);
                e.printStackTrace();
            }
        }
    }

    public class HelloWorldConsumer implements Runnable, ExceptionListener {
        final boolean useTopic;

        public HelloWorldConsumer(boolean useTopic) {
            this.useTopic = useTopic;
        }

        @Override
        public void run() {
            try {

                int i = count.incrementAndGet();
                String destName = !useTopic ? "Consumer.Q" + i + ".VirtualTopic.AMQ6218Test" : testTopicName;
                LOG.info(destName);

                ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(getBrokerURI());
                Connection connection = connectionFactory.createConnection();
                connection.setClientID("clientId" + i);
                connection.start();
                Session session = connection.createSession(false, ActiveMQSession.INDIVIDUAL_ACKNOWLEDGE);

                Destination destination = useTopic ? session.createTopic(destName) : session.createQueue(destName);
                MessageConsumer consumer = useTopic ?
                        session.createDurableSubscriber((Topic) destination, "sub" + i) :
                            session.createConsumer(destination);

                ready.countDown();

                while (!failure.get()) {

                    Message message = consumer.receive(500);

                    if (message != null) {
                        synchronized (this) {
                            if (MessageId != null) {
                                if (message.getJMSMessageID().equalsIgnoreCase(MessageId)) {
                                    MessageCount++;
                                } else {
                                    LOG.info("Count of message " + MessageId + " is " + MessageCount);
                                    MessageCount = 1;
                                    MessageId = message.getJMSMessageID();
                                }
                            } else {
                                MessageId = message.getJMSMessageID();
                                MessageCount = 1;
                            }
                        }

                        String text = null;
                        if (messageType.equals(MessageType.OBJECT) && message instanceof ObjectMessage) {
                            ObjectMessage objectMessage = (ObjectMessage) message;
                            text = (String) objectMessage.getObject();
                        } else if (messageType.equals(MessageType.TEXT) && message instanceof TextMessage) {
                            TextMessage textMessage = (TextMessage) message;
                            text = textMessage.getText();
                        } else if (messageType.equals(MessageType.MAP) && message instanceof MapMessage) {
                            MapMessage mapMessage = (MapMessage) message;
                            text = mapMessage.getString("text");
                        } else {
                            LOG.info(destName + " Message is not a instanceof " + messageType + " message id: " + message.getJMSMessageID() + message);
                        }

                        if (text == null) {
                            LOG.warn(destName + " text received as a null " + message);
                            failure.set(true);
                        } else {
                            LOG.info(destName + " text " + text + " message id: " + message.getJMSMessageID());
                        }

                        message.acknowledge();
                    }
                }

                connection.close();
            } catch (Exception e) {
                LOG.error("Caught: ", e);
            }
        }

        @Override
        public synchronized void onException(JMSException ex) {
            LOG.error("JMS Exception occurred.  Shutting down client.");
        }
    }
}
