/**
 * 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 static org.junit.Assert.assertEquals;

import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;

import jakarta.jms.Connection;
import jakarta.jms.Destination;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.MessageConsumer;
import jakarta.jms.MessageListener;
import jakarta.jms.Session;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.advisory.AdvisoryBroker;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.ConnectionContext;
import org.apache.activemq.command.ActiveMQDestination;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ConnectionId;
import org.apache.activemq.command.ConnectionInfo;
import org.apache.activemq.command.ConsumerId;
import org.apache.activemq.command.ConsumerInfo;
import org.apache.activemq.command.SessionId;
import org.apache.activemq.command.SessionInfo;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
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 AMQ4853Test {

    private static final transient Logger LOG = LoggerFactory.getLogger(AMQ4853Test.class);
    private static BrokerService brokerService;
    private static final String BROKER_ADDRESS = "tcp://localhost:0";
    private static final ActiveMQQueue DESTINATION = new ActiveMQQueue("TEST.QUEUE");
    private CountDownLatch cycleDoneLatch;

    private String connectionUri;

    @Before
    public void setUp() throws Exception {
        brokerService = new BrokerService();
        brokerService.setPersistent(false);
        brokerService.setUseJmx(false);
        brokerService.setAdvisorySupport(true);
        brokerService.setDeleteAllMessagesOnStartup(true);
        connectionUri = brokerService.addConnector(BROKER_ADDRESS).getPublishableConnectString();

        brokerService.start();
        brokerService.waitUntilStarted();
    }

    @After
    public void tearDown() throws Exception {
        brokerService.stop();
        brokerService.waitUntilStopped();
    }

    /**
     * Test to shows the performance of the removing consumers while other stay active.
     * @throws Exception
     */
    @Ignore
    @Test
    public void test() throws Exception {

        // Create a stable set of consumers to fill in the advisory broker's consumer list.
        ArrayList<Consumer> fixedConsumers = new ArrayList<Consumer>(100);
        for (int i = 0; i < 200; ++i) {
            fixedConsumers.add(new Consumer());
        }

        // Create a set of consumers that comes online for a short time and then
        // goes offline again.  Cycles will repeat as each batch completes
        final int fixedDelayConsumers = 300;
        final int fixedDelayCycles = 25;

        final CountDownLatch fixedDelayCycleLatch = new CountDownLatch(fixedDelayCycles);

        // Update so done method can track state.
        cycleDoneLatch = fixedDelayCycleLatch;

        CyclicBarrier barrier = new CyclicBarrier(fixedDelayConsumers, new Runnable() {
            @Override
            public void run() {
                LOG.info("Fixed delay consumers cycle {} completed.", fixedDelayCycleLatch.getCount());
                fixedDelayCycleLatch.countDown();
            }
        });

        for (int i = 0; i < fixedDelayConsumers; ++i) {
            new Thread(new FixedDelyConsumer(barrier)).start();
        }

        fixedDelayCycleLatch.await(10, TimeUnit.MINUTES);

        // Clean up.

        for (Consumer consumer : fixedConsumers) {
            consumer.close();
        }
        fixedConsumers.clear();
    }

    private ConnectionInfo createConnectionInfo() {
        ConnectionId id = new ConnectionId();
        id.setValue("ID:123456789:0:1");

        ConnectionInfo info = new ConnectionInfo();
        info.setConnectionId(id);
        return info;
    }

    private SessionInfo createSessionInfo(ConnectionInfo connection) {
        SessionId id = new SessionId(connection.getConnectionId(), 1);

        SessionInfo info = new SessionInfo();
        info.setSessionId(id);

        return info;
    }

    public ConsumerInfo createConsumerInfo(SessionInfo session, int value, ActiveMQDestination destination) {
        ConsumerId id = new ConsumerId();
        id.setConnectionId(session.getSessionId().getConnectionId());
        id.setSessionId(1);
        id.setValue(value);

        ConsumerInfo info = new ConsumerInfo();
        info.setConsumerId(id);
        info.setDestination(destination);
        return info;
    }

    /**
     * Test to shows the performance impact of removing consumers in various scenarios.
     * @throws Exception
     */
    @Ignore
    @Test
    public void testPerformanceOfRemovals() throws Exception {
        // setup
        AdvisoryBroker testObj = (AdvisoryBroker) brokerService.getBroker().getAdaptor(AdvisoryBroker.class);
        ActiveMQDestination destination = new ActiveMQQueue("foo");
        ConnectionInfo connectionInfo = createConnectionInfo();
        ConnectionContext connectionContext = new ConnectionContext(connectionInfo);
        connectionContext.setBroker(brokerService.getBroker());
        SessionInfo sessionInfo = createSessionInfo(connectionInfo);

        long start = System.currentTimeMillis();

        for (int i = 0; i < 200; ++i) {

            for (int j = 1; j <= 500; j++) {
                ConsumerInfo consumerInfo = createConsumerInfo(sessionInfo, j, destination);
                testObj.addConsumer(connectionContext, consumerInfo);
            }

            for (int j = 500; j > 0; j--) {
                ConsumerInfo consumerInfo = createConsumerInfo(sessionInfo, j, destination);
                testObj.removeConsumer(connectionContext, consumerInfo);
            }

            for (int j = 1; j <= 500; j++) {
                ConsumerInfo consumerInfo = createConsumerInfo(sessionInfo, j, destination);
                testObj.addConsumer(connectionContext, consumerInfo);
            }

            for (int j = 1; j <= 500; j++) {
                ConsumerInfo consumerInfo = createConsumerInfo(sessionInfo, j, destination);
                testObj.removeConsumer(connectionContext, consumerInfo);
            }
        }

        long finish = System.currentTimeMillis();

        long totalTime = finish - start;

        LOG.info("Total test time: {} seconds", TimeUnit.MILLISECONDS.toSeconds(totalTime));

        assertEquals(0, testObj.getAdvisoryConsumers().size());
    }

    @Test
    public void testEqualsNeeded() throws Exception {
        // setup
        AdvisoryBroker testObj = (AdvisoryBroker) brokerService.getBroker().getAdaptor(AdvisoryBroker.class);
        ActiveMQDestination destination = new ActiveMQQueue("foo");
        ConnectionInfo connectionInfo = createConnectionInfo();
        ConnectionContext connectionContext = new ConnectionContext(connectionInfo);
        connectionContext.setBroker(brokerService.getBroker());
        SessionInfo sessionInfo = createSessionInfo(connectionInfo);

        for (int j = 1; j <= 5; j++) {
            ConsumerInfo consumerInfo = createConsumerInfo(sessionInfo, j, destination);
            testObj.addConsumer(connectionContext, consumerInfo);
        }

        for (int j = 1; j <= 5; j++) {
            ConsumerInfo consumerInfo = createConsumerInfo(sessionInfo, j, destination);
            testObj.removeConsumer(connectionContext, consumerInfo);
        }

        assertEquals(0, testObj.getAdvisoryConsumers().size());
    }

    private boolean done() {
        if (cycleDoneLatch == null) {
            return true;
        }
        return cycleDoneLatch.getCount() == 0;
    }

    class Consumer implements MessageListener {

        Connection connection;
        Session session;
        Destination destination;
        MessageConsumer consumer;

        Consumer() throws JMSException {
            ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(connectionUri);
            connection = factory.createConnection();
            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            consumer = session.createConsumer(DESTINATION);
            consumer.setMessageListener(this);
            connection.start();
        }

        @Override
        public void onMessage(Message message) {
        }

        public void close() {
            try {
                connection.close();
            } catch(Exception e) {
            }

            connection = null;
            session = null;
            consumer = null;
        }
    }

    class FixedDelyConsumer implements Runnable {

        private final CyclicBarrier barrier;
        private final int sleepInterval;

        public FixedDelyConsumer(CyclicBarrier barrier) {
            this.barrier = barrier;
            this.sleepInterval = 1000;
        }

        public FixedDelyConsumer(CyclicBarrier barrier, int sleepInterval) {
            this.barrier = barrier;
            this.sleepInterval = sleepInterval;
        }

        @Override
        public void run() {
            while (!done()) {

                try {
                    Consumer consumer = new Consumer();
                    TimeUnit.MILLISECONDS.sleep(sleepInterval);
                    consumer.close();
                    barrier.await();
                } catch (Exception ex) {
                    return;
                }
            }
        }
    }

}
