/**
 * 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 java.util.HashMap;
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.AtomicInteger;
import jakarta.jms.Connection;
import jakarta.jms.DeliveryMode;
import jakarta.jms.ExceptionListener;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.MessageConsumer;
import jakarta.jms.MessageListener;
import jakarta.jms.MessageProducer;
import jakarta.jms.Session;
import jakarta.jms.TextMessage;
import junit.framework.TestCase;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.store.kahadb.KahaDBPersistenceAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.experimental.categories.Category;
import org.apache.activemq.test.annotations.ParallelTest;

@Category(ParallelTest.class)
public class AMQ4092Test extends TestCase {

    private static final Logger log = LoggerFactory.getLogger(AMQ4092Test.class);

    static final String QUEUE_NAME = "TEST";

    // increase limits to expedite failure
    static final int NUM_TO_SEND_PER_PRODUCER = 1000; // 10000
    static final int NUM_PRODUCERS = 5; // 40

    static final ActiveMQQueue[] DESTINATIONS = new ActiveMQQueue[]{
            new ActiveMQQueue("A"),
            new ActiveMQQueue("B")
            // A/B seems to be sufficient for concurrentStoreAndDispatch=true
    };

    static final boolean debug = false;

    private BrokerService brokerService;

    private ActiveMQQueue destination;
    private HashMap<Thread, Throwable> exceptions = new HashMap<Thread, Throwable>();
    private ExceptionListener exceptionListener = new ExceptionListener() {
        @Override
        public void onException(JMSException exception) {
            exception.printStackTrace();
            exceptions.put(Thread.currentThread(), exception);
        }
    };

    @Override
    protected void setUp() throws Exception {
        brokerService = new BrokerService();
        brokerService.setDeleteAllMessagesOnStartup(true);
        ((KahaDBPersistenceAdapter)brokerService.getPersistenceAdapter()).setConcurrentStoreAndDispatchQueues(false);
        brokerService.addConnector("tcp://localhost:0");
        brokerService.start();
        destination = new ActiveMQQueue();
        destination.setCompositeDestinations(DESTINATIONS);
        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                exceptions.put(t, e);
            }
        });
    }

    @Override
    protected void tearDown() throws Exception {
        // Stop any running threads.
        brokerService.stop();
    }


    public void testConcurrentGroups() throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(new TestConsumer());
        for (int i=0; i<NUM_PRODUCERS; i++) {
            executorService.submit(new TestProducer());
        }
        executorService.shutdown();
        executorService.awaitTermination(5, TimeUnit.MINUTES);
        assertTrue("no exceptions: " + exceptions, exceptions.isEmpty());
    }

    
    class TestProducer implements Runnable {

        public void produceMessages() throws Exception {
            ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(
                    brokerService.getTransportConnectors().get(0).getConnectUri().toString()
            );
            connectionFactory.setExceptionListener(exceptionListener);
            connectionFactory.setUseAsyncSend(true);
            Connection connection = connectionFactory.createConnection();
            Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            MessageProducer producer = session.createProducer(destination);
            producer.setDeliveryMode(DeliveryMode.PERSISTENT);

                String name = new String(new byte[2*1024]);
                for (int i = 1; i <= NUM_TO_SEND_PER_PRODUCER; i++) {

                    TextMessage message = session.createTextMessage(name + "_" + i);
                    for (int j=0; j<100; j++) {
                        message.setStringProperty("Prop" + j, ""+j);
                    }
                    message.setStringProperty("JMSXGroupID", Thread.currentThread().getName()+i);
                    message.setIntProperty("JMSXGroupSeq", 1);
                    producer.send(message);
                }

            producer.close();
            session.close();
            connection.close();
        }

        @Override
        public void run() {
            try {
                produceMessages();
            } catch (Exception e) {
                e.printStackTrace();
                exceptions.put(Thread.currentThread(), e);
            }
        }
    }

    class TestConsumer implements Runnable {

        private CountDownLatch finishLatch = new CountDownLatch(1);



        public void consume() throws Exception {
            ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(
                    brokerService.getTransportConnectors().get(0).getConnectUri().toString()
            );

            connectionFactory.setExceptionListener(exceptionListener);
            final int totalMessageCount = NUM_TO_SEND_PER_PRODUCER * DESTINATIONS.length * NUM_PRODUCERS;
            final AtomicInteger counter = new AtomicInteger();
            final MessageListener listener = new MessageListener() {
                public void onMessage(Message message) {

                    if (debug) {
                        try {
                            log.info(((TextMessage) message).getText());
                        } catch (JMSException e) {
                            e.printStackTrace();
                        }
                    }

                    boolean first = false;
                    try {
                        first = message.getBooleanProperty("JMSXGroupFirstForConsumer");
                    } catch (JMSException e) {
                        e.printStackTrace();
                        exceptions.put(Thread.currentThread(), e);
                    }
                    assertTrue("Always is first message", first);
                    if (counter.incrementAndGet() == totalMessageCount) {
                            log.info("Got all:" + counter.get());
                            finishLatch.countDown();

                    }
                }
            };

            int consumerCount = DESTINATIONS.length * 100;
            Connection[] connections = new Connection[consumerCount];

            Session[] sessions = new Session[consumerCount];
            MessageConsumer[] consumers = new MessageConsumer[consumerCount];

            for (int i = 0; i < consumerCount; i++) {
                connections[i] = connectionFactory.createConnection();
                connections[i].start();

                sessions[i] = connections[i].createSession(false, Session.AUTO_ACKNOWLEDGE);

                consumers[i] = sessions[i].createConsumer(DESTINATIONS[i%DESTINATIONS.length], null);
                consumers[i].setMessageListener(listener);
            }


            log.info("received " + counter.get() + " messages");

            assertTrue("got all messages in time", finishLatch.await(4, TimeUnit.MINUTES));

            log.info("received " + counter.get() + " messages");

            for (MessageConsumer consumer : consumers) {
                consumer.close();
            }

            for (Session session : sessions) {
                session.close();
            }

            for (Connection connection : connections) {
                connection.close();
            }
        }

        @Override
        public void run() {
            try {
                consume();
            } catch (Exception e) {
                e.printStackTrace();
                exceptions.put(Thread.currentThread(), e);
            }
        }
    }

}
