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

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.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
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.Queue;
import jakarta.jms.Session;
import jakarta.jms.TextMessage;
import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQSession;
import org.apache.activemq.broker.BrokerPlugin;
import org.apache.activemq.broker.BrokerPluginSupport;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.ProducerBrokerExchange;
import org.apache.activemq.broker.region.policy.PolicyEntry;
import org.apache.activemq.broker.region.policy.PolicyMap;
import org.apache.activemq.transaction.Synchronization;
import org.apache.activemq.util.Wait;
import org.junit.After;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


import static org.junit.Assert.*;

public class FailoverTxSlowAckTest {

    private static final Logger LOG = LoggerFactory.getLogger(FailoverTxSlowAckTest.class);
    private static final String QUEUE_IN = "IN";
    private static final String QUEUE_OUT = "OUT";

    private static final String MESSAGE_TEXT = "Test message ";
    private static final String TRANSPORT_URI = "tcp://localhost:0";
    private String url;
    final int prefetch = 1;
    BrokerService broker;

    @After
    public void stopBroker() throws Exception {
        if (broker != null) {
            broker.stop();
        }
    }

    public void startBroker(boolean deleteAllMessagesOnStartup) throws Exception {
        broker = createBroker(deleteAllMessagesOnStartup);
        broker.start();
    }

    public BrokerService createBroker(boolean deleteAllMessagesOnStartup) throws Exception {
        return createBroker(deleteAllMessagesOnStartup, TRANSPORT_URI);
    }

    public BrokerService createBroker(boolean deleteAllMessagesOnStartup, String bindAddress) throws Exception {
        broker = new BrokerService();
        broker.addConnector(bindAddress);
        broker.setDeleteAllMessagesOnStartup(deleteAllMessagesOnStartup);
        PolicyMap policyMap = new PolicyMap();
        PolicyEntry defaultEntry = new PolicyEntry();
        defaultEntry.setOptimizedDispatch(true);
        policyMap.setDefaultEntry(defaultEntry);
        broker.setDestinationPolicy(policyMap);

        return broker;
    }

    @Test
    public void testFailoverDuringAckRollsback() throws Exception {
        broker = createBroker(true);

        final ExecutorService executorService = Executors.newFixedThreadPool(2);
        broker.setPlugins(new BrokerPlugin[] {
                new BrokerPluginSupport() {
                    int sendCount = 0;
                    @Override
                    public void send(ProducerBrokerExchange producerExchange, org.apache.activemq.command.Message messageSend) throws Exception {
                        super.send(producerExchange, messageSend);
                        sendCount++;
                        if (sendCount > 1) {
                            // need new thread b/c we have the service write lock
                            executorService.execute(new Runnable() {
                                @Override
                                public void run() {
                                    LOG.info("Stopping broker before commit...");
                                    try {
                                        broker.stop();
                                    } catch (Exception e) {
                                        e.printStackTrace();
                                    }
                                }
                            });
                        }
                    }}});

        broker.start();
        url = broker.getTransportConnectors().get(0).getConnectUri().toString();

        ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory("failover:(" + url + ")");
        cf.setWatchTopicAdvisories(false);
        cf.setDispatchAsync(false);

        final ActiveMQConnection connection = (ActiveMQConnection) cf.createConnection();
        connection.start();

        final Session producerSession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        final Queue in = producerSession.createQueue(QUEUE_IN + "?consumer.prefetchSize=" + prefetch);

        final Session consumerSession = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
        final Queue out = consumerSession.createQueue(QUEUE_OUT);
        final MessageProducer consumerProducer = consumerSession.createProducer(out);


        final CountDownLatch commitDoneLatch = new CountDownLatch(1);
        final CountDownLatch messagesReceived = new CountDownLatch(1);
        final CountDownLatch brokerDisconnectedLatch = new CountDownLatch(1);
        final AtomicInteger receivedCount = new AtomicInteger();

        final AtomicBoolean gotDisconnect = new AtomicBoolean();
        final AtomicBoolean gotReconnected = new AtomicBoolean();

        final MessageConsumer testConsumer = consumerSession.createConsumer(in);
        testConsumer.setMessageListener(new MessageListener() {

            @Override
            public void onMessage(Message message) {
                LOG.info("consume one and commit");

                assertNotNull("got message", message);
                receivedCount.incrementAndGet();
                messagesReceived.countDown();


                try {
                    // ensure message expires broker side so it won't get redelivered
                    TimeUnit.SECONDS.sleep(1);

                    consumerProducer.send(message);

                    // hack to block the transaction completion
                    // ensure session does not get to send commit message before failover reconnect
                    // if the commit message is in progress during failover we get rollback via the state
                    // tracker
                    ((ActiveMQSession) consumerSession).getTransactionContext().addSynchronization(new Synchronization() {

                        @Override
                        public void beforeEnd() throws Exception {

                            LOG.info("waiting for failover reconnect");

                            gotDisconnect.set(Wait.waitFor(new Wait.Condition() {
                                @Override
                                public boolean isSatisified() throws Exception {
                                    return !((ActiveMQSession) consumerSession).getConnection().getTransport().isConnected();
                                }
                            }));

                            //connect down to trigger reconnect
                            brokerDisconnectedLatch.countDown();

                            LOG.info("got disconnect");
                            gotReconnected.set(Wait.waitFor(new Wait.Condition() {
                                @Override
                                public boolean isSatisified() throws Exception {
                                    return ((ActiveMQSession) consumerSession).getConnection().getTransport().isConnected();
                                }
                            }));

                            LOG.info("got failover reconnect");

                        }
                    });


                    consumerSession.commit();
                    LOG.info("done commit");

                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    commitDoneLatch.countDown();
                }
            }
        });

        executorService.execute(new Runnable() {
            @Override
            public void run() {
                LOG.info("producer started");
                try {
                    produceMessage(producerSession, in, 1);
                } catch (jakarta.jms.IllegalStateException SessionClosedExpectedOnShutdown) {
                } catch (JMSException e) {
                    e.printStackTrace();
                    fail("unexpceted ex on producer: " + e);
                }
                LOG.info("producer done");
            }
        });

        // will be stopped by the plugin on TX ack
        broker.waitUntilStopped();
        //await for listener to detect disconnect
        brokerDisconnectedLatch.await();
        broker = createBroker(false, url);
        broker.start();

        assertTrue("message was recieved ", messagesReceived.await(20, TimeUnit.SECONDS));
        assertTrue("tx complete through failover", commitDoneLatch.await(40, TimeUnit.SECONDS));
        assertEquals("one delivery", 1, receivedCount.get());

        assertTrue("got disconnect/reconnect", gotDisconnect.get());
        assertTrue("got reconnect", gotReconnected.get());

        assertNull("No message produced", receiveMessage(cf, out));
    }


    private Message receiveMessage(ActiveMQConnectionFactory cf,
            Queue destination) throws Exception {
        final ActiveMQConnection connection = (ActiveMQConnection) cf.createConnection();
        connection.start();
        final Session consumerSession = connection.createSession(true, Session.SESSION_TRANSACTED);
        final MessageConsumer consumer = consumerSession.createConsumer(destination);
        Message msg = consumer.receive(4000);
        consumerSession.commit();
        connection.close();
        return msg;
    }

    private void produceMessage(final Session producerSession, Queue destination, long count)
        throws JMSException {
        MessageProducer producer = producerSession.createProducer(destination);
        for (int i=0; i<count; i++) {
            TextMessage message = producerSession.createTextMessage(MESSAGE_TEXT + i);
            // have it expire so it will only be delivered once
            producer.send(message, DeliveryMode.PERSISTENT, Message.DEFAULT_PRIORITY, 500);
        }
        producer.close();
    }
}
