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

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import jakarta.jms.Connection;
import jakarta.jms.DeliveryMode;
import jakarta.jms.MessageNotWriteableException;
import jakarta.jms.Queue;
import jakarta.jms.QueueBrowser;
import jakarta.jms.Session;
import javax.management.ObjectName;

import javax.management.openmbean.CompositeData;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.BrokerTestSupport;
import org.apache.activemq.broker.StubConnection;
import org.apache.activemq.broker.TransportConnector;
import org.apache.activemq.broker.jmx.ManagementContext;
import org.apache.activemq.broker.jmx.QueueViewMBean;
import org.apache.activemq.broker.region.policy.PolicyEntry;
import org.apache.activemq.broker.region.policy.PolicyMap;
import org.apache.activemq.command.ActiveMQDestination;
import org.apache.activemq.command.ActiveMQMessage;
import org.apache.activemq.command.ActiveMQTextMessage;
import org.apache.activemq.command.ConnectionId;
import org.apache.activemq.command.ConnectionInfo;
import org.apache.activemq.command.ConsumerInfo;
import org.apache.activemq.command.DestinationInfo;
import org.apache.activemq.command.Message;
import org.apache.activemq.command.MessageAck;
import org.apache.activemq.command.MessageDispatch;
import org.apache.activemq.command.MessageId;
import org.apache.activemq.command.ProducerInfo;
import org.apache.activemq.command.SessionInfo;
import org.apache.activemq.transport.Transport;
import org.apache.activemq.transport.TransportFactory;
import org.apache.activemq.util.Wait;
import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class duplicates most of the functionality in {@link NetworkTestSupport}
 * and {@link BrokerTestSupport} because more control was needed over how brokers
 * and connectors are created. Also, this test asserts message counts via JMX on
 * each broker.
 */
public class BrokerNetworkWithStuckMessagesTest {

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

    private BrokerService localBroker;
    private BrokerService remoteBroker;
    private BrokerService secondRemoteBroker;
    private DemandForwardingBridge bridge;

    protected Map<String, BrokerService> brokers = new HashMap<String, BrokerService>();
    protected ArrayList<StubConnection> connections = new ArrayList<StubConnection>();

    protected TransportConnector connector;
    protected TransportConnector remoteConnector;
    protected TransportConnector secondRemoteConnector;

    protected long idGenerator;
    protected int msgIdGenerator;
    protected int tempDestGenerator;
    protected int maxWait = 4000;
    protected String queueName = "TEST";

    protected String amqDomain = "org.apache.activemq";

    @Before
    public void setUp() throws Exception {

        // For those who want visual confirmation:
        //   Uncomment the following to enable JMX support on a port number to use
        //   Jconsole to view each broker. You will need to add some calls to
        //   Thread.sleep() to be able to actually slow things down so that you
        //   can manually see JMX attrs.
//        System.setProperty("com.sun.management.jmxremote", "");
//        System.setProperty("com.sun.management.jmxremote.port", "1099");
//        System.setProperty("com.sun.management.jmxremote.authenticate", "false");
//        System.setProperty("com.sun.management.jmxremote.ssl", "false");

        // Create the local broker
        createBroker();
        // Create the remote broker
        createRemoteBroker();

        // Remove the activemq-data directory from the creation of the remote broker
        FileUtils.deleteDirectory(new File("activemq-data"));

        // Create a network bridge between the local and remote brokers so that
        // demand-based forwarding can take place
        NetworkBridgeConfiguration config = new NetworkBridgeConfiguration();
        config.setBrokerName("local");
        config.setDispatchAsync(false);
        config.setDuplex(true);

        Transport localTransport = createTransport();
        Transport remoteTransport = createRemoteTransport();

        // Create a network bridge between the two brokers
        bridge = new DemandForwardingBridge(config, localTransport, remoteTransport);
        bridge.setBrokerService(localBroker);
        bridge.start();


        // introduce a second broker/bridge on remote that should not get any messages because of networkTtl=1
        // local <-> remote <-> secondRemote
        createSecondRemoteBroker();
        config = new NetworkBridgeConfiguration();
        config.setBrokerName("remote");
        config.setDuplex(true);

        localTransport = createRemoteTransport();
        remoteTransport = createSecondRemoteTransport();

        // Create a network bridge between the two brokers
        bridge = new DemandForwardingBridge(config, localTransport, remoteTransport);
        bridge.setBrokerService(remoteBroker);
        bridge.start();

        waitForBridgeFormation();
    }

    protected void waitForBridgeFormation() throws Exception {
        for (final BrokerService broker : brokers.values()) {
            if (!broker.getNetworkConnectors().isEmpty()) {
                // Max wait here is 30 secs
                Wait.waitFor(new Wait.Condition() {
                    @Override
                    public boolean isSatisified() throws Exception {
                        return !broker.getNetworkConnectors().get(0).activeBridges().isEmpty();
                    }});
            }
        }
    }

    @After
    public void tearDown() throws Exception {
        bridge.stop();
        localBroker.stop();
        remoteBroker.stop();
        secondRemoteBroker.stop();
    }

    @Test(timeout=120000)
    public void testBrokerNetworkWithStuckMessages() throws Exception {

        int sendNumMessages = 10;
        int receiveNumMessages = 5;

        // Create a producer
        StubConnection connection1 = createConnection();
        ConnectionInfo connectionInfo1 = createConnectionInfo();
        SessionInfo sessionInfo1 = createSessionInfo(connectionInfo1);
        ProducerInfo producerInfo = createProducerInfo(sessionInfo1);
        connection1.send(connectionInfo1);
        connection1.send(sessionInfo1);
        connection1.send(producerInfo);

        // Create a destination on the local broker
        ActiveMQDestination destinationInfo1 = null;

        // Send a 10 messages to the local broker
        for (int i = 0; i < sendNumMessages; ++i) {
            destinationInfo1 = createDestinationInfo(connection1, connectionInfo1, ActiveMQDestination.QUEUE_TYPE);
            connection1.request(createMessage(producerInfo, destinationInfo1, DeliveryMode.NON_PERSISTENT));
        }

        // Ensure that there are 10 messages on the local broker
        Object[] messages = browseQueueWithJmx(localBroker);
        assertEquals(sendNumMessages, messages.length);

        // Create a synchronous consumer on the remote broker
        StubConnection connection2 = createRemoteConnection();
        ConnectionInfo connectionInfo2 = createConnectionInfo();
        SessionInfo sessionInfo2 = createSessionInfo(connectionInfo2);
        connection2.send(connectionInfo2);
        connection2.send(sessionInfo2);
        ActiveMQDestination destinationInfo2 =
            createDestinationInfo(connection2, connectionInfo2, ActiveMQDestination.QUEUE_TYPE);
        final ConsumerInfo consumerInfo2 = createConsumerInfo(sessionInfo2, destinationInfo2);
        connection2.send(consumerInfo2);

        // Consume 5 of the messages from the remote broker and ack them.
        for (int i = 0; i < receiveNumMessages; ++i) {
            Message message1 = receiveMessage(connection2, 20000);
            assertNotNull(message1);
            LOG.info("on remote, got: " + message1.getMessageId());
            connection2.send(createAck(consumerInfo2, message1, 1, MessageAck.INDIVIDUAL_ACK_TYPE));
            assertTrue("JMSActiveMQBrokerPath property present and correct",
                    ((ActiveMQMessage)message1).getStringProperty(ActiveMQMessage.BROKER_PATH_PROPERTY).contains(localBroker.getBroker().getBrokerId().toString()));
        }

        // Ensure that there are zero messages on the local broker. This tells
        // us that those messages have been prefetched to the remote broker
        // where the demand exists.
        Wait.waitFor(new Wait.Condition() {
            @Override
            public boolean isSatisified() throws Exception {
                Object[] result = browseQueueWithJmx(localBroker);
               return 0 == result.length;
            }
        });
        messages = browseQueueWithJmx(localBroker);
        assertEquals(0, messages.length);

        // try and pull the messages from remote, should be denied b/c on networkTtl
        LOG.info("creating demand on second remote...");
        StubConnection connection3 = createSecondRemoteConnection();
        ConnectionInfo connectionInfo3 = createConnectionInfo();
        SessionInfo sessionInfo3 = createSessionInfo(connectionInfo3);
        connection3.send(connectionInfo3);
        connection3.send(sessionInfo3);
        ActiveMQDestination destinationInfo3 =
            createDestinationInfo(connection3, connectionInfo3, ActiveMQDestination.QUEUE_TYPE);
        final ConsumerInfo consumerInfoS3 = createConsumerInfo(sessionInfo3, destinationInfo3);
        connection3.send(consumerInfoS3);

        Message messageExceedingTtl = receiveMessage(connection3, 5000);
        if (messageExceedingTtl != null) {
            LOG.error("got message on Second remote: " + messageExceedingTtl);
            connection3.send(createAck(consumerInfoS3, messageExceedingTtl, 1, MessageAck.INDIVIDUAL_ACK_TYPE));
        }

        LOG.info("Closing consumer on remote");
        // Close the consumer on the remote broker
        connection2.send(consumerInfo2.createRemoveCommand());
        // also close connection etc.. so messages get dropped from the local consumer  q
        connection2.send(connectionInfo2.createRemoveCommand());

        // There should now be 5 messages stuck on the remote broker
        assertTrue("correct stuck message count", Wait.waitFor(new Wait.Condition() {
            @Override
            public boolean isSatisified() throws Exception {
                Object[] result = browseQueueWithJmx(remoteBroker);
                return 5 == result.length;
            }
        }));
        messages = browseQueueWithJmx(remoteBroker);
        assertEquals(5, messages.length);

        assertTrue("can see broker path property",
                ((String)((CompositeData)messages[1]).get("BrokerPath")).contains(localBroker.getBroker().getBrokerId().toString()));

        LOG.info("Messages now stuck on remote");

        // receive again on the origin broker
        ConsumerInfo consumerInfo1 = createConsumerInfo(sessionInfo1, destinationInfo1);
        connection1.send(consumerInfo1);
        LOG.info("create local consumer: " + consumerInfo1);

        Message message1 = receiveMessage(connection1, 20000);
        assertNotNull("Expect to get a replay as remote consumer is gone", message1);
        connection1.send(createAck(consumerInfo1, message1, 1, MessageAck.INDIVIDUAL_ACK_TYPE));
        LOG.info("acked one message on origin, waiting for all messages to percolate back");

        Wait.waitFor(new Wait.Condition() {
            @Override
            public boolean isSatisified() throws Exception {
                Object[] result = browseQueueWithJmx(localBroker);
               return 4 == result.length;
            }
        });
        messages = browseQueueWithJmx(localBroker);
        assertEquals(4, messages.length);

        LOG.info("checking for messages on remote again");
        // messages won't migrate back again till consumer closes
        connection2 = createRemoteConnection();
        connectionInfo2 = createConnectionInfo();
        sessionInfo2 = createSessionInfo(connectionInfo2);
        connection2.send(connectionInfo2);
        connection2.send(sessionInfo2);
        ConsumerInfo consumerInfo3 = createConsumerInfo(sessionInfo2, destinationInfo2);
        connection2.send(consumerInfo3);
        message1 = receiveMessage(connection2, 20000);
        assertNull("Messages have migrated back: " + message1, message1);

        // Consume the last 4 messages from the local broker and ack them just
        // to clean up the queue.
        int counter = 1;
        for (; counter < receiveNumMessages; counter++) {
            message1 = receiveMessage(connection1);
            LOG.info("local consume of: " + (message1 != null ? message1.getMessageId() : " null"));
            connection1.send(createAck(consumerInfo1, message1, 1, MessageAck.INDIVIDUAL_ACK_TYPE));
        }
        // Ensure that 5 messages were received
        assertEquals(receiveNumMessages, counter);

        // verify all messages consumed
        Wait.waitFor(new Wait.Condition() {
            @Override
            public boolean isSatisified() throws Exception {
                Object[] result = browseQueueWithJmx(remoteBroker);
               return 0 == result.length;
            }
        });
        messages = browseQueueWithJmx(remoteBroker);
        assertEquals(0, messages.length);

        Wait.waitFor(new Wait.Condition() {
            @Override
            public boolean isSatisified() throws Exception {
                Object[] result = browseQueueWithJmx(localBroker);
               return 0 == result.length;
            }
        });
        messages = browseQueueWithJmx(localBroker);
        assertEquals(0, messages.length);

        // Close the consumer on the remote broker
        connection2.send(consumerInfo3.createRemoveCommand());

        connection1.stop();
        connection2.stop();
        connection3.stop();
    }

    protected BrokerService createBroker() throws Exception {
        localBroker = new BrokerService();
        localBroker.setBrokerName("localhost");
        localBroker.setUseJmx(true);
        localBroker.setPersistenceAdapter(null);
        localBroker.setPersistent(false);
        connector = createConnector();
        localBroker.addConnector(connector);
        configureBroker(localBroker);
        localBroker.start();
        localBroker.waitUntilStarted();

        localBroker.getManagementContext().setConnectorPort(2221);

        brokers.put(localBroker.getBrokerName(), localBroker);

        return localBroker;
    }

    private void configureBroker(BrokerService broker) {
        PolicyMap policyMap = new PolicyMap();
        PolicyEntry defaultEntry = new PolicyEntry();
        defaultEntry.setExpireMessagesPeriod(0);
        ConditionalNetworkBridgeFilterFactory filterFactory = new ConditionalNetworkBridgeFilterFactory();
        filterFactory.setReplayWhenNoConsumers(true);
        defaultEntry.setNetworkBridgeFilterFactory(filterFactory);
        policyMap.setDefaultEntry(defaultEntry);
        broker.setDestinationPolicy(policyMap);
    }

    protected BrokerService createRemoteBroker() throws Exception {
        remoteBroker = new BrokerService();
        remoteBroker.setBrokerName("remotehost");
        remoteBroker.setUseJmx(true);
        remoteBroker.setPersistenceAdapter(null);
        remoteBroker.setPersistent(false);
        remoteConnector = createRemoteConnector();
        remoteBroker.addConnector(remoteConnector);
        configureBroker(remoteBroker);
        remoteBroker.start();
        remoteBroker.waitUntilStarted();

        remoteBroker.getManagementContext().setConnectorPort(2222);

        brokers.put(remoteBroker.getBrokerName(), remoteBroker);

        return remoteBroker;
    }

    protected BrokerService createSecondRemoteBroker() throws Exception {
        secondRemoteBroker = new BrokerService();
        secondRemoteBroker.setBrokerName("secondRemotehost");
        secondRemoteBroker.setUseJmx(false);
        secondRemoteBroker.setPersistenceAdapter(null);
        secondRemoteBroker.setPersistent(false);
        secondRemoteConnector = createSecondRemoteConnector();
        secondRemoteBroker.addConnector(secondRemoteConnector);
        configureBroker(secondRemoteBroker);
        secondRemoteBroker.start();
        secondRemoteBroker.waitUntilStarted();

        brokers.put(secondRemoteBroker.getBrokerName(), secondRemoteBroker);

        return secondRemoteBroker;
    }

    protected Transport createTransport() throws Exception {
        Transport transport = TransportFactory.connect(connector.getServer().getConnectURI());
        return transport;
    }

    protected Transport createRemoteTransport() throws Exception {
        Transport transport = TransportFactory.connect(remoteConnector.getServer().getConnectURI());
        return transport;
    }

    protected Transport createSecondRemoteTransport() throws Exception {
        Transport transport = TransportFactory.connect(secondRemoteConnector.getServer().getConnectURI());
        return transport;
    }

    protected TransportConnector createConnector() throws Exception, IOException, URISyntaxException {
        return new TransportConnector(TransportFactory.bind(new URI(getLocalURI())));
    }

    protected TransportConnector createRemoteConnector() throws Exception, IOException, URISyntaxException {
        return new TransportConnector(TransportFactory.bind(new URI(getRemoteURI())));
    }

    protected TransportConnector createSecondRemoteConnector() throws Exception, IOException, URISyntaxException {
        return new TransportConnector(TransportFactory.bind(new URI(getSecondRemoteURI())));
    }

    protected String getRemoteURI() {
        return "vm://remotehost";
    }

    protected String getSecondRemoteURI() {
        return "vm://secondRemotehost";
    }

    protected String getLocalURI() {
        return "vm://localhost";
    }

    protected StubConnection createConnection() throws Exception {
        Transport transport = TransportFactory.connect(connector.getServer().getConnectURI());
        StubConnection connection = new StubConnection(transport);
        connections.add(connection);
        return connection;
    }

    protected StubConnection createRemoteConnection() throws Exception {
        Transport transport = TransportFactory.connect(remoteConnector.getServer().getConnectURI());
        StubConnection connection = new StubConnection(transport);
        connections.add(connection);
        return connection;
    }

    protected StubConnection createSecondRemoteConnection() throws Exception {
        Transport transport = TransportFactory.connect(secondRemoteConnector.getServer().getConnectURI());
        StubConnection connection = new StubConnection(transport);
        connections.add(connection);
        return connection;
    }

    @SuppressWarnings({ "unchecked", "unused" })
    private Object[] browseQueueWithJms(BrokerService broker) throws Exception {
        Object[] messages = null;
        Connection connection = null;
        Session session = null;

        try {
            URI brokerUri = connector.getUri();
            ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(brokerUri.toString());
            connection = connectionFactory.createConnection();
            connection.start();
            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            Queue destination = session.createQueue(queueName);
            QueueBrowser browser = session.createBrowser(destination);
            List<Message> list = new ArrayList<Message>();
            for (Enumeration<Message> enumn = browser.getEnumeration(); enumn.hasMoreElements();) {
                list.add(enumn.nextElement());
            }
            messages = list.toArray();
        }
        finally {
            if (session != null) {
                session.close();
            }
            if (connection != null) {
                connection.close();
            }
        }
        LOG.info("+Browsed with JMS: " + messages.length);

        return messages;
    }

    private Object[] browseQueueWithJmx(BrokerService broker) throws Exception {
        Hashtable<String, String> params = new Hashtable<String, String>();
        params.put("brokerName", broker.getBrokerName());
        params.put("type", "Broker");
        params.put("destinationType", "Queue");
        params.put("destinationName", queueName);
        ObjectName queueObjectName = ObjectName.getInstance(amqDomain, params);

        ManagementContext mgmtCtx = broker.getManagementContext();
        QueueViewMBean queueView = (QueueViewMBean)mgmtCtx.newProxyInstance(queueObjectName, QueueViewMBean.class, true);

        Object[] messages = queueView.browse();

        LOG.info("+Browsed with JMX: " + messages.length);

        return messages;
    }

    protected ConnectionInfo createConnectionInfo() throws Exception {
        ConnectionInfo info = new ConnectionInfo();
        info.setConnectionId(new ConnectionId("connection:" + (++idGenerator)));
        info.setClientId(info.getConnectionId().getValue());
        return info;
    }

    protected SessionInfo createSessionInfo(ConnectionInfo connectionInfo) throws Exception {
        SessionInfo info = new SessionInfo(connectionInfo, ++idGenerator);
        return info;
    }

    protected ProducerInfo createProducerInfo(SessionInfo sessionInfo) throws Exception {
        ProducerInfo info = new ProducerInfo(sessionInfo, ++idGenerator);
        return info;
    }

    protected ConsumerInfo createConsumerInfo(SessionInfo sessionInfo, ActiveMQDestination destination) throws Exception {
        ConsumerInfo info = new ConsumerInfo(sessionInfo, ++idGenerator);
        info.setBrowser(false);
        info.setDestination(destination);
        info.setPrefetchSize(1000);
        info.setDispatchAsync(false);
        return info;
    }

    protected DestinationInfo createTempDestinationInfo(ConnectionInfo connectionInfo, byte destinationType) {
        DestinationInfo info = new DestinationInfo();
        info.setConnectionId(connectionInfo.getConnectionId());
        info.setOperationType(DestinationInfo.ADD_OPERATION_TYPE);
        info.setDestination(ActiveMQDestination.createDestination(info.getConnectionId() + ":" + (++tempDestGenerator), destinationType));
        return info;
    }

    protected ActiveMQDestination createDestinationInfo(StubConnection connection, ConnectionInfo connectionInfo1, byte destinationType) throws Exception {
        if ((destinationType & ActiveMQDestination.TEMP_MASK) != 0) {
            DestinationInfo info = createTempDestinationInfo(connectionInfo1, destinationType);
            connection.send(info);
            return info.getDestination();
        } else {
            return ActiveMQDestination.createDestination(queueName, destinationType);
        }
    }

    protected Message createMessage(ProducerInfo producerInfo, ActiveMQDestination destination, int deliveryMode) {
        Message message = createMessage(producerInfo, destination);
        message.setPersistent(deliveryMode == DeliveryMode.PERSISTENT);
        return message;
    }

    protected Message createMessage(ProducerInfo producerInfo, ActiveMQDestination destination) {
        ActiveMQTextMessage message = new ActiveMQTextMessage();
        message.setMessageId(new MessageId(producerInfo, ++msgIdGenerator));
        message.setDestination(destination);
        message.setPersistent(false);
        try {
            message.setText("Test Message Payload.");
        } catch (MessageNotWriteableException e) {
        }
        return message;
    }

    protected MessageAck createAck(ConsumerInfo consumerInfo, Message msg, int count, byte ackType) {
        MessageAck ack = new MessageAck();
        ack.setAckType(ackType);
        ack.setConsumerId(consumerInfo.getConsumerId());
        ack.setDestination(msg.getDestination());
        ack.setLastMessageId(msg.getMessageId());
        ack.setMessageCount(count);
        return ack;
    }

    public Message receiveMessage(StubConnection connection) throws InterruptedException {
        return receiveMessage(connection, maxWait);
    }

    public Message receiveMessage(StubConnection connection, long timeout) throws InterruptedException {
        while (true) {
            Object o = connection.getDispatchQueue().poll(timeout, TimeUnit.MILLISECONDS);

            if (o == null) {
                return null;
            }
            if (o instanceof MessageDispatch) {

                MessageDispatch dispatch = (MessageDispatch)o;
                if (dispatch.getMessage() == null) {
                    return null;
                }
                dispatch.setMessage(dispatch.getMessage().copy());
                dispatch.getMessage().setRedeliveryCounter(dispatch.getRedeliveryCounter());
                return dispatch.getMessage();
            }
        }
    }
}
