/**
 * 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.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Vector;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import jakarta.jms.BytesMessage;
import jakarta.jms.Connection;
import jakarta.jms.DeliveryMode;
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 junit.framework.Test;

import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.CombinationTestSupport;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.region.policy.PolicyEntry;
import org.apache.activemq.broker.region.policy.PolicyMap;
import org.apache.activemq.broker.region.policy.VMPendingQueueMessageStoragePolicy;
import org.apache.activemq.command.MessageId;
import org.apache.activemq.command.ProducerId;
import org.apache.activemq.store.kahadb.KahaDBPersistenceAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AMQ2413Test extends CombinationTestSupport implements MessageListener {
    private static final Logger LOG = LoggerFactory.getLogger(AMQ2413Test.class);
    BrokerService broker;
    private ActiveMQConnectionFactory factory;

    private static final int HANG_THRESHOLD = 60;
    private static final int SEND_COUNT = 1000;
    private static final int RECEIVER_THINK_TIME = 1;
    private static final int CONSUMER_COUNT = 1;
    private static final int PRODUCER_COUNT = 50;
    private static final int TO_SEND = SEND_COUNT / PRODUCER_COUNT;

    public int deliveryMode = DeliveryMode.NON_PERSISTENT;
    public int ackMode = Session.DUPS_OK_ACKNOWLEDGE;
    public boolean useVMCursor = false;
    public boolean useOptimizeAcks = false;

    private final ArrayList<Service> services = new ArrayList<Service>(CONSUMER_COUNT + PRODUCER_COUNT);
    AtomicInteger count = new AtomicInteger(0);
    Semaphore receivedMessages;
    AtomicBoolean running = new AtomicBoolean(false);

    public void initCombos() {
        addCombinationValues("deliveryMode", new Object[] { DeliveryMode.PERSISTENT, DeliveryMode.NON_PERSISTENT });
        addCombinationValues("ackMode", new Object[] { Session.DUPS_OK_ACKNOWLEDGE, Session.AUTO_ACKNOWLEDGE });
        addCombinationValues("useVMCursor", new Object[] { true, false });
        // addCombinationValues("useOptimizeAcks", new Object[] {true, false});
    }

    @Override
    protected void setUp() throws Exception {
        broker = new BrokerService();
        broker.setDataDirectory("target" + File.separator + "test-data" + File.separator + "AMQ2401Test");
        broker.setDeleteAllMessagesOnStartup(true);

        KahaDBPersistenceAdapter kahaDb = (KahaDBPersistenceAdapter) broker.getPersistenceAdapter();
        kahaDb.setConcurrentStoreAndDispatchQueues(false);
        broker.addConnector("tcp://0.0.0.0:2401");
        PolicyMap policies = new PolicyMap();
        PolicyEntry entry = new PolicyEntry();
        entry.setMemoryLimit(1024 * 1024);
        entry.setProducerFlowControl(true);
        if (useVMCursor) {
            entry.setPendingQueuePolicy(new VMPendingQueueMessageStoragePolicy());
        }
        entry.setQueue(">");
        policies.setDefaultEntry(entry);
        broker.setDestinationPolicy(policies);
        broker.start();
        broker.waitUntilStarted();

        count.set(0);
        receivedMessages = new Semaphore(0);

        factory = new ActiveMQConnectionFactory("tcp://0.0.0.0:2401");
        // factory = new ActiveMQConnectionFactory("vm://localhost?broker.useJmx=false&broker.persistent=false");
        setAutoFail(true);
        super.setUp();
    }

    @Override
    protected void tearDown() throws Exception {
        running.set(false);
        for (Service service : services) {
            service.close();
        }

        broker.stop();
        broker.waitUntilStopped();

        super.tearDown();
    }

    public void testReceipt() throws Exception {

        running.set(true);
        TestProducer p = null;
        TestConsumer c = null;
        try {

            for (int i = 0; i < CONSUMER_COUNT; i++) {
                TestConsumer consumer = new TestConsumer();
                consumer.start();
                services.add(consumer);
            }
            for (int i = 0; i < PRODUCER_COUNT; i++) {
                TestProducer producer = new TestProducer(i);
                producer.start();
                services.add(producer);
            }
            waitForMessageReceipt();

        } finally {
            if (p != null) {
                p.close();
            }

            if (c != null) {
                c.close();
            }
        }

    }

    /*
     * (non-Javadoc)
     *
     * @see jakarta.jms.MessageListener#onMessage(jakarta.jms.Message)
     */
    @Override
    public void onMessage(Message message) {
        receivedMessages.release();
        if (count.incrementAndGet() % 100 == 0) {
            LOG.info("Received message " + count);
        }
        track(message);
        if (RECEIVER_THINK_TIME > 0) {
            try {
                Thread.sleep(RECEIVER_THINK_TIME);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

    }

    HashMap<ProducerId, boolean[]> tracker = new HashMap<ProducerId, boolean[]>();

    private synchronized void track(Message message) {
        try {
            MessageId id = new MessageId(message.getJMSMessageID());
            ProducerId pid = id.getProducerId();
            int seq = (int) id.getProducerSequenceId();
            boolean[] ids = tracker.get(pid);
            if (ids == null) {
                ids = new boolean[TO_SEND + 1];
                ids[seq] = true;
                tracker.put(pid, ids);
            } else {
                assertTrue("not already received: " + id, !ids[seq]);
                ids[seq] = true;
            }
        } catch (Exception e) {
            LOG.error(e.toString());
        }
    }

    /**
     * @throws InterruptedException
     * @throws TimeoutException
     *
     */
    private void waitForMessageReceipt() throws InterruptedException, TimeoutException {
        try {
            while (count.get() < SEND_COUNT) {
                if (!receivedMessages.tryAcquire(HANG_THRESHOLD, TimeUnit.SECONDS)) {
                    if (count.get() == SEND_COUNT)
                        break;
                    verifyTracking();
                    throw new TimeoutException("@count=" + count.get() + " Message not received for more than " + HANG_THRESHOLD + " seconds");
                }
            }
        } finally {
            running.set(false);
        }
    }

    private void verifyTracking() {
        Vector<MessageId> missing = new Vector<MessageId>();
        for (ProducerId pid : tracker.keySet()) {
            boolean[] ids = tracker.get(pid);
            for (int i = 1; i < TO_SEND + 1; i++) {
                if (!ids[i]) {
                    missing.add(new MessageId(pid, i));
                }
            }
        }
        assertTrue("No missing messages: " + missing, missing.isEmpty());
    }

    private interface Service {
        public void start() throws Exception;

        public void close();
    }

    private class TestProducer implements Runnable, Service {
        Thread thread;
        BytesMessage message;
        Connection connection;
        Session session;
        MessageProducer producer;

        TestProducer(int id) throws Exception {
            thread = new Thread(this, "TestProducer-" + id);
            connection = factory.createConnection();
            connection.start();
            session = connection.createSession(false, Session.DUPS_OK_ACKNOWLEDGE);
            producer = session.createProducer(session.createQueue("AMQ2401Test"));
        }

        @Override
        public void start() {
            thread.start();
        }

        @Override
        public void run() {

            int i = 1;
            for (; i <= TO_SEND; i++) {
                try {

                    if (+i % 100 == 0) {
                        LOG.info(Thread.currentThread().getName() + " Sending message " + i);
                    }
                    message = session.createBytesMessage();
                    message.writeBytes(new byte[1024]);
                    producer.setDeliveryMode(deliveryMode);
                    producer.send(message);
                } catch (JMSException jmse) {
                    jmse.printStackTrace();
                    break;
                }
            }
            LOG.info(Thread.currentThread().getName() + " Sent: " + (i - 1));
        }

        @Override
        public void close() {
            try {
                connection.close();
            } catch (JMSException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    private class TestConsumer implements Runnable, Service {
        ActiveMQConnection connection;
        Session session;
        MessageConsumer consumer;

        TestConsumer() throws Exception {
            factory.setOptimizeAcknowledge(false);
            connection = (ActiveMQConnection) factory.createConnection();
            if (useOptimizeAcks) {
                connection.setOptimizeAcknowledge(true);
            }

            session = connection.createSession(false, ackMode);
            consumer = session.createConsumer(session.createQueue("AMQ2401Test"));

            consumer.setMessageListener(AMQ2413Test.this);
        }

        @Override
        public void start() throws Exception {
            connection.start();
        }

        @Override
        public void close() {
            try {
                connection.close();
            } catch (JMSException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        /*
         * (non-Javadoc)
         *
         * @see java.lang.Runnable#run()
         */
        @Override
        public void run() {
            while (running.get()) {
                try {
                    onMessage(consumer.receive());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static Test suite() {
        return suite(AMQ2413Test.class);
    }
}
