/**
 * 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
 * <p/>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p/>
 * 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.ArrayList;

import jakarta.jms.Connection;
import jakarta.jms.DeliveryMode;
import jakarta.jms.Message;
import jakarta.jms.MessageConsumer;
import jakarta.jms.MessageProducer;
import jakarta.jms.Session;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.TransportConnector;
import org.apache.activemq.broker.region.Queue;
import org.apache.activemq.command.ActiveMQQueue;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


import static org.junit.Assert.assertEquals;

public class AMQ6094Test {

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

    private BrokerService brokerService;
    private String connectionUri;

    @Before
    public void before() throws Exception {
        brokerService = new BrokerService();
        TransportConnector connector = brokerService.addConnector("tcp://localhost:0");
        connectionUri = connector.getPublishableConnectString();
        brokerService.setDeleteAllMessagesOnStartup(true);
        brokerService.start();
        brokerService.waitUntilStarted();
    }

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

    @Test
	public void testQueueMemoryUsage() throws Exception {

        final ArrayList<ThreadSlot> producerThreads = new ArrayList<>();

		final ArrayList<ThreadSlot> consumerThreads = new ArrayList<>();

		for (int i = 0; i < 4; i++)
			producerThreads.add(runInThread(new UnsafeRunnable() {

                @Override
                public void run() throws Exception {
                    producer(connectionUri, "queueA");
                }

			}));

		for (int i = 0; i < 4; i++)
			consumerThreads.add(runInThread(new UnsafeRunnable() {

                @Override
                public void run() throws Exception {
                    consumer(connectionUri, "queueA", 2500);
                }
            }));

		// kill and restart random threads
		for (int count = 0; count < 10; count++) {
			Thread.sleep(5000);
			final int i = (int) (Math.random() * consumerThreads.size());
			final ThreadSlot slot = consumerThreads.get(i);
			slot.thread.interrupt();
			consumerThreads.remove(i);
			consumerThreads.add(runInThread(slot.runnable));

			Queue queue = (Queue) brokerService.getDestination(new ActiveMQQueue("queueA"));
			LOG.info("cursorMemoryUsage: " + queue.getMessages().
			        getSystemUsage().getMemoryUsage().getUsage());
            LOG.info("messagesStat: " + queue.getDestinationStatistics().getMessages().getCount());

		}

        // verify usage
        Queue queue = (Queue) brokerService.getDestination(new ActiveMQQueue("queueA"));
        LOG.info("cursorMemoryUsage: " + queue.getMessages().
                getSystemUsage().getMemoryUsage().getUsage());
        LOG.info("messagesStat: " + queue.getDestinationStatistics().getMessages().getCount());

        // drain the queue
        for (ThreadSlot threadSlot: producerThreads) {
            threadSlot.thread.interrupt();
            threadSlot.thread.join(4000);
        }

        for (ThreadSlot threadSlot : consumerThreads) {
            threadSlot.thread.interrupt();
            threadSlot.thread.join(4000);
        }

        consumer(connectionUri, "queueA", 2500, true);

        LOG.info("After drain, cursorMemoryUsage: " + queue.getMessages().
                getSystemUsage().getMemoryUsage().getUsage());
        LOG.info("messagesStat: " + queue.getDestinationStatistics().getMessages().getCount());

        assertEquals("Queue memory usage to 0", 0, queue.getMessages().
                getSystemUsage().getMemoryUsage().getUsage());

	}

	public static void producer(String uri, String topic) throws Exception {
		final ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(
		        uri + "?jms.useCompression=true&jms.useAsyncSend=true&daemon=true");

		Connection connection = factory.createConnection();
		Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        MessageProducer producer = session.createProducer(new ActiveMQQueue(topic));

		producer.setTimeToLive(6000);
		producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

		while (true) {
			producer.send(session.createTextMessage(msg()));
			if (Math.random() > 0.5)
				Thread.sleep(1);
		}

	}

    public static void consumer(String uri, String queue, int prefetchSize) throws Exception {
        consumer(uri, queue, prefetchSize, false);
    }

	public static void consumer(String uri, String queue, int prefetchSize, boolean drain) throws Exception {
		final ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(
				uri + "?jms.prefetchPolicy.queuePrefetch=" + prefetchSize + "&jms.useAsyncSend=true");

		Connection connection = null;
		try {
			connection = factory.createConnection();
			connection.start();
			Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
			MessageConsumer consumer = session.createConsumer(new ActiveMQQueue(queue));
            if (drain) {
                Message message = null;
                do {
                    message = consumer.receive(4000);
                } while (message != null);
            } else {
                // block
                while (true) {
                    consumer.receive();
                }
            }
		} finally {

            Thread.interrupted();
			if (!drain) {
                Thread.sleep(5000); // delay closing of connection
            }

			LOG.info("Now closing");
			if (connection != null)
				connection.close();
		}

	}

	private static String msg() {
		final StringBuilder builder = new StringBuilder();
		for (int i = 0; i < 100; i++)
			builder.append("123457890");

		return builder.toString();
	}

	private static interface UnsafeRunnable {
		public void run() throws Exception;
	}

	public static class ThreadSlot {
		private UnsafeRunnable runnable;
		private Thread thread;
	}

	public static ThreadSlot runInThread(final UnsafeRunnable runnable) {
		final Thread thread = new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    runnable.run();
                } catch (Exception e) {
                    //e.printStackTrace();
                }

            }
        });

		thread.start();

		final ThreadSlot result = new ThreadSlot();
		result.thread = thread;
		result.runnable = runnable;
		return result;
	}
}
