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

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import jakarta.jms.Connection;
import jakarta.jms.JMSException;
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.command.ActiveMQQueue;
import org.apache.activemq.command.TransactionId;
import org.apache.activemq.store.kahadb.FilteredKahaDBPersistenceAdapter;
import org.apache.activemq.store.kahadb.KahaDBPersistenceAdapter;
import org.apache.activemq.store.kahadb.MultiKahaDBPersistenceAdapter;
import org.apache.activemq.store.kahadb.MultiKahaDBTransactionStore;
import org.apache.activemq.usage.SystemUsage;
import org.apache.activemq.util.Wait;
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;
import static org.junit.Assert.assertTrue;

public class StorePerDestinationTest  {
    static final Logger LOG = LoggerFactory.getLogger(StorePerDestinationTest.class);
    final static int maxFileLength = 1024*100;
    final static int numToSend = 5000;
    final Vector<Throwable> exceptions = new Vector<Throwable>();
    BrokerService brokerService;

    protected BrokerService createBroker(PersistenceAdapter kaha) throws Exception {

        BrokerService broker = new BrokerService();
        broker.setUseJmx(false);
        broker.setPersistenceAdapter(kaha);
        return broker;
    }

    protected PersistenceAdapter createStore(boolean delete) throws IOException {
        KahaDBPersistenceAdapter kaha = new KahaDBPersistenceAdapter();
        kaha.setJournalMaxFileLength(maxFileLength);
        kaha.setCleanupInterval(5000);
        if (delete) {
            kaha.deleteAllMessages();
        }
        return kaha;
    }

    @Before
    public void prepareCleanBrokerWithMultiStore() throws Exception {
           prepareBrokerWithMultiStore(true);
    }

    public void prepareBrokerWithMultiStore(boolean deleteAllMessages) throws Exception {

        MultiKahaDBPersistenceAdapter multiKahaDBPersistenceAdapter = new MultiKahaDBPersistenceAdapter();
        if (deleteAllMessages) {
            multiKahaDBPersistenceAdapter.deleteAllMessages();
        }
        ArrayList<FilteredKahaDBPersistenceAdapter> adapters = new ArrayList<FilteredKahaDBPersistenceAdapter>();

        FilteredKahaDBPersistenceAdapter theRest = new FilteredKahaDBPersistenceAdapter();
        theRest.setPersistenceAdapter(createStore(deleteAllMessages));
        // default destination when not set is a match for all
        adapters.add(theRest);

        // separate store for FastQ
        FilteredKahaDBPersistenceAdapter fastQStore = new FilteredKahaDBPersistenceAdapter();
        fastQStore.setPersistenceAdapter(createStore(deleteAllMessages));
        fastQStore.setDestination(new ActiveMQQueue("FastQ"));
        adapters.add(fastQStore);

        multiKahaDBPersistenceAdapter.setFilteredPersistenceAdapters(adapters);
        brokerService  = createBroker(multiKahaDBPersistenceAdapter);
    }

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

    @Test
    public void testTransactedSendReceive() throws Exception {
        brokerService.start();
        sendMessages(true, "SlowQ", 1, 0);
        assertEquals("got one", 1, receiveMessages(true, "SlowQ", 1));
    }

    @Test
    public void testTransactedSendReceiveAcrossStores() throws Exception {
        brokerService.start();
        sendMessages(true, "SlowQ,FastQ", 1, 0);
        assertEquals("got one", 2, receiveMessages(true, "SlowQ,FastQ", 2));
    }

    @Test
    public void testCommitRecovery() throws Exception {
        doTestRecovery(true);
    }

     @Test
    public void testRollbackRecovery() throws Exception {
        doTestRecovery(false);
    }

    public void doTestRecovery(final boolean haveOutcome) throws Exception {
        final MultiKahaDBPersistenceAdapter persistenceAdapter =
                (MultiKahaDBPersistenceAdapter) brokerService.getPersistenceAdapter();
        MultiKahaDBTransactionStore transactionStore =
                new MultiKahaDBTransactionStore(persistenceAdapter) {
                    @Override
                    public void persistOutcome(Tx tx, TransactionId txid) throws IOException {
                        if (haveOutcome) {
                            super.persistOutcome(tx, txid);
                        }
                        try {
                            // IOExceptions will stop the broker
                            persistenceAdapter.stop();
                        } catch (Exception e) {
                            LOG.error("ex on stop ", e);
                            exceptions.add(e);
                        }
                    }
                };
        persistenceAdapter.setTransactionStore(transactionStore);
        brokerService.start();

        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    // commit will block
                    sendMessages(true, "SlowQ,FastQ", 1, 0);
                } catch(Exception expected) {
                    LOG.info("expected", expected);
                }
            }
        });

        brokerService.waitUntilStopped();
        // interrupt the send thread
        executorService.shutdownNow();

        // verify auto recovery
        prepareBrokerWithMultiStore(false);
        brokerService.start();

        assertEquals("expect to get the recovered message", haveOutcome ? 2 : 0, receiveMessages(false, "SlowQ,FastQ", 2));
        assertEquals("all transactions are complete", 0, brokerService.getBroker().getPreparedTransactions(null).length);
    }

    @Test
    public void testDirectoryDefault() throws Exception {
        MultiKahaDBPersistenceAdapter multiKahaDBPersistenceAdapter = new MultiKahaDBPersistenceAdapter();
        ArrayList<FilteredKahaDBPersistenceAdapter> adapters = new ArrayList<FilteredKahaDBPersistenceAdapter>();

        FilteredKahaDBPersistenceAdapter otherFilteredKahaDBPersistenceAdapter =
                new FilteredKahaDBPersistenceAdapter();
        PersistenceAdapter otherStore = createStore(false);
        File someOtherDisk = new File("target" + File.separator + "someOtherDisk");
        otherStore.setDirectory(someOtherDisk);
        otherFilteredKahaDBPersistenceAdapter.setPersistenceAdapter(otherStore);
        otherFilteredKahaDBPersistenceAdapter.setDestination(new ActiveMQQueue("Other"));
        adapters.add(otherFilteredKahaDBPersistenceAdapter);

        FilteredKahaDBPersistenceAdapter filteredKahaDBPersistenceAdapterDefault =
                new FilteredKahaDBPersistenceAdapter();
        PersistenceAdapter storeDefault = createStore(false);
        filteredKahaDBPersistenceAdapterDefault.setPersistenceAdapter(storeDefault);
        adapters.add(filteredKahaDBPersistenceAdapterDefault);

        multiKahaDBPersistenceAdapter.setFilteredPersistenceAdapters(adapters);

        assertEquals(multiKahaDBPersistenceAdapter.getDirectory(), storeDefault.getDirectory().getParentFile());
        assertEquals(someOtherDisk, otherStore.getDirectory().getParentFile());
    }

    @Test
    public void testSlowFastDestinationsStoreUsage() throws Exception {
        brokerService.start();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    sendMessages(false, "SlowQ", 50, 500);
                } catch (Exception e) {
                    exceptions.add(e);
                }
            }
        });

        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    sendMessages(false, "FastQ", numToSend, 0);
                } catch (Exception e) {
                    exceptions.add(e);
                }
            }
        });

        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    assertEquals("Got all sent", numToSend, receiveMessages(false, "FastQ", numToSend));
                } catch (Exception e) {
                    exceptions.add(e);
                }
            }
        });

        executorService.shutdown();
        assertTrue("consumers executor finished on time", executorService.awaitTermination(5*60, TimeUnit.SECONDS));
        final SystemUsage usage = brokerService.getSystemUsage();
        assertTrue("Store is not hogged", Wait.waitFor(new Wait.Condition() {

            @Override
            public boolean isSatisified() throws Exception {
                long storeUsage = usage.getStoreUsage().getUsage();
                LOG.info("Store Usage: " + storeUsage);
                return storeUsage < 5 * maxFileLength;
            }
        }));
        assertTrue("no exceptions", exceptions.isEmpty());
    }

    private void sendMessages(boolean transacted, String destName, int count, long sleep) throws Exception {
        ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory("vm://localhost");
        Connection connection = cf.createConnection();
        try {
            Session session = transacted ? connection.createSession(true, Session.SESSION_TRANSACTED) : connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            MessageProducer producer = session.createProducer(new ActiveMQQueue(destName));
            for (int i = 0; i < count; i++) {
                if (sleep > 0) {
                    TimeUnit.MILLISECONDS.sleep(sleep);
                }
                producer.send(session.createTextMessage(createContent(i)));
            }
            if (transacted) {
                session.commit();
            }
        } finally {
            connection.close();
        }
    }

    private int receiveMessages(boolean transacted, String destName, int max) throws JMSException {
        int rc = 0;
        ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory("vm://localhost");
        Connection connection = cf.createConnection();
        try {
            connection.start();
            Session session = transacted ? connection.createSession(true, Session.SESSION_TRANSACTED) : connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            MessageConsumer messageConsumer = session.createConsumer(new ActiveMQQueue(destName));
            while (rc < max && messageConsumer.receive(4000) != null) {
                rc++;

                if (transacted && rc % 200 == 0) {
                    session.commit();
                }
            }
            if (transacted) {
                session.commit();
            }
            return rc;
        } finally {
            connection.close();
        }
    }

    private String createContent(int i) {
        StringBuilder sb = new StringBuilder(i + ":");
        while (sb.length() < 1024) {
            sb.append("*");
        }
        return sb.toString();
    }

}