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

import com.sleepycat.bind.tuple.TupleBinding;
import com.sleepycat.je.DatabaseEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.qpid.framing.AMQShortString;
import org.apache.qpid.framing.BasicContentHeaderProperties;
import org.apache.qpid.framing.ContentHeaderBody;
import org.apache.qpid.framing.MethodRegistry;
import org.apache.qpid.framing.ProtocolVersion;
import org.apache.qpid.framing.abstraction.MessagePublishInfo;
import org.apache.qpid.framing.abstraction.MessagePublishInfoImpl;
import org.apache.qpid.management.common.mbeans.ManagedQueue;
import org.apache.qpid.server.message.EnqueableMessage;
import org.apache.qpid.server.message.MessageMetaData;
import org.apache.qpid.server.store.MessageStore;
import org.apache.qpid.server.store.StoredMessage;
import org.apache.qpid.server.store.TransactionLogResource;
import org.apache.qpid.server.store.berkeleydb.keys.MessageContentKey_4;
import org.apache.qpid.server.store.berkeleydb.tuples.MessageContentKeyTupleBindingFactory;
import org.apache.qpid.server.store.berkeleydb.tuples.MessageMetaDataTupleBindingFactory;
import org.apache.qpid.test.utils.JMXTestUtils;
import org.apache.qpid.test.utils.QpidBrokerTestCase;
import org.apache.qpid.util.FileUtils;

import static org.apache.qpid.server.store.berkeleydb.BDBStoreUpgradeTestPreparer.QUEUE_NAME;
import static org.apache.qpid.server.store.berkeleydb.BDBStoreUpgradeTestPreparer.SUB_NAME;
import static org.apache.qpid.server.store.berkeleydb.BDBStoreUpgradeTestPreparer.TOPIC_NAME;
import static org.apache.qpid.server.store.berkeleydb.BDBStoreUpgradeTestPreparer.SELECTOR_SUB_NAME;
import static org.apache.qpid.server.store.berkeleydb.BDBStoreUpgradeTestPreparer.SELECTOR_TOPIC_NAME;

import javax.jms.Connection;
import javax.jms.DeliveryMode;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.Topic;
import javax.jms.TopicConnection;
import javax.jms.TopicPublisher;
import javax.jms.TopicSession;
import javax.jms.TopicSubscriber;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;

/**
 * Tests upgrading a BDB store and using it with the new broker 
 * after the required contents are entered into the store using 
 * an old broker with the BDBStoreUpgradeTestPreparer. The store
 * will then be used to verify that the upgraded is completed 
 * properly and that once upgraded it functions as expected with 
 * the new broker.
 */
public class BDBUpgradeTest extends QpidBrokerTestCase
{
    protected static final Logger _logger = LoggerFactory.getLogger(BDBUpgradeTest.class);

    private static final String STRING_1024 = BDBStoreUpgradeTestPreparer.generateString(1024);
    private static final String STRING_1024_256 = BDBStoreUpgradeTestPreparer.generateString(1024*256);
    private static final String QPID_WORK_ORIG = System.getProperty("QPID_WORK");
    private static final String QPID_HOME = System.getProperty("QPID_HOME");
    private static final int VERSION_4 = 4;

    private String _fromDir;
    private String _toDir;
    private String _toDirTwice;

    @Override
    public void setUp() throws Exception
    {
        assertNotNull("QPID_WORK must be set", QPID_WORK_ORIG);
        assertNotNull("QPID_HOME must be set", QPID_HOME);

        _fromDir = QPID_HOME + "/bdbstore-to-upgrade/test-store";
        _toDir = getWorkDirBaseDir() + "/bdbstore/test-store";
        _toDirTwice = getWorkDirBaseDir() + "/bdbstore-upgraded-twice";

        //Clear the two target directories if they exist.
        File directory = new File(_toDir);
        if (directory.exists() && directory.isDirectory())
        {
            FileUtils.delete(directory, true);
        }
        directory = new File(_toDirTwice);
        if (directory.exists() && directory.isDirectory())
        {
            FileUtils.delete(directory, true);
        }

        //Upgrade the test store.
        upgradeBrokerStore(_fromDir, _toDir);

        //override the broker config used and then start the broker with the updated store
        _configFile = new File("build/etc/config-systests-bdb.xml");
        setConfigurationProperty("management.enabled", "true");

        super.setUp();       
    }

    private String getWorkDirBaseDir()
    {
        return QPID_WORK_ORIG + (isInternalBroker() ? "" : "/" + getPort());
    }

    /**
     * Tests that the core upgrade method of the store upgrade tool passes through the exception
     * from the BDBMessageStore indicating that the data on disk can't be loaded as the previous
     * version because it has already been upgraded.
     * @throws Exception
     */
    public void testMultipleUpgrades() throws Exception
    {
        //stop the broker started by setUp() in order to allow the second upgrade attempt to proceed
        stopBroker();

        try
        {
            new BDBStoreUpgrade(_toDir, _toDirTwice, null, false, true).upgradeFromVersion(VERSION_4);
            fail("Second Upgrade Succeeded");
        }
        catch (Exception e)
        {
            System.err.println("Showing stack trace, we are expecting an 'Unable to load BDBStore' error");
            e.printStackTrace();
            assertTrue("Incorrect Exception Thrown:" + e.getMessage(),
                    e.getMessage().contains("Unable to load BDBStore as version 4. Store on disk contains version 5 data"));
        }
    }

    /**
     * Test that the selector applied to the DurableSubscription was successfully
     * transfered to the new store, and functions as expected with continued use
     * by monitoring message count while sending new messages to the topic and then
     * consuming them.
     */
    public void testSelectorDurability() throws Exception
    {
        JMXTestUtils jmxUtils = null;
        try
        {
            jmxUtils = new JMXTestUtils(this, "guest", "guest");
            jmxUtils.open();
        }
        catch (Exception e)
        {
            fail("Unable to establish JMX connection, test cannot proceed");
        }

        try
        {
            ManagedQueue dursubQueue = jmxUtils.getManagedQueue("clientid" + ":" + SELECTOR_SUB_NAME);
            assertEquals("DurableSubscription backing queue should have 1 message on it initially", 
                          new Integer(1), dursubQueue.getMessageCount());
            
            // Create a connection and start it
            TopicConnection connection = (TopicConnection) getConnection();
            connection.start();
            
            // Send messages which don't match and do match the selector, checking message count
            TopicSession pubSession = connection.createTopicSession(true, org.apache.qpid.jms.Session.SESSION_TRANSACTED);
            Topic topic = pubSession.createTopic(SELECTOR_TOPIC_NAME);
            TopicPublisher publisher = pubSession.createPublisher(topic);
            
            BDBStoreUpgradeTestPreparer.publishMessages(pubSession, publisher, topic, DeliveryMode.PERSISTENT, 1*1024, 1, "false");
            pubSession.commit();
            assertEquals("DurableSubscription backing queue should still have 1 message on it", 
                         Integer.valueOf(1), dursubQueue.getMessageCount());
            
            BDBStoreUpgradeTestPreparer.publishMessages(pubSession, publisher, topic, DeliveryMode.PERSISTENT, 1*1024, 1, "true");
            pubSession.commit();
            assertEquals("DurableSubscription backing queue should now have 2 messages on it", 
                         Integer.valueOf(2), dursubQueue.getMessageCount());

            TopicSubscriber durSub = pubSession.createDurableSubscriber(topic, SELECTOR_SUB_NAME,"testprop='true'", false);
            Message m = durSub.receive(2000);
            assertNotNull("Failed to receive an expected message", m);
            m = durSub.receive(2000);
            assertNotNull("Failed to receive an expected message", m);
            pubSession.commit();

            pubSession.close();
        }
        finally
        {
            jmxUtils.close();
        }
    }

    /**
     * Test that the DurableSubscription without selector was successfully
     * transfered to the new store, and functions as expected with continued use.
     */
    public void testDurableSubscriptionWithoutSelector() throws Exception
    {
        JMXTestUtils jmxUtils = null;
        try
        {
            jmxUtils = new JMXTestUtils(this, "guest", "guest");
            jmxUtils.open();
        }
        catch (Exception e)
        {
            fail("Unable to establish JMX connection, test cannot proceed");
        }

        try
        {
            ManagedQueue dursubQueue = jmxUtils.getManagedQueue("clientid" + ":" + SUB_NAME);
            assertEquals("DurableSubscription backing queue should have 1 message on it initially",
                          new Integer(1), dursubQueue.getMessageCount());

            // Create a connection and start it
            TopicConnection connection = (TopicConnection) getConnection();
            connection.start();

            // Send new message matching the topic, checking message count
            TopicSession session = connection.createTopicSession(true, org.apache.qpid.jms.Session.SESSION_TRANSACTED);
            Topic topic = session.createTopic(TOPIC_NAME);
            TopicPublisher publisher = session.createPublisher(topic);

            BDBStoreUpgradeTestPreparer.publishMessages(session, publisher, topic, DeliveryMode.PERSISTENT, 1*1024, 1, "indifferent");
            session.commit();
            assertEquals("DurableSubscription backing queue should now have 2 messages on it",
                        Integer.valueOf(2), dursubQueue.getMessageCount());

            TopicSubscriber durSub = session.createDurableSubscriber(topic, SUB_NAME);
            Message m = durSub.receive(2000);
            assertNotNull("Failed to receive an expected message", m);
            m = durSub.receive(2000);
            assertNotNull("Failed to receive an expected message", m);

            session.commit();
            session.close();
        }
        finally
        {
            jmxUtils.close();
        }
    }

    /**
     * Test that the backing queue for the durable subscription created was successfully
     * detected and set as being exclusive during the upgrade process, and that the
     * regular queue was not.
     */
    public void testQueueExclusivity() throws Exception
    {
        JMXTestUtils jmxUtils = null;
        try
        {
            jmxUtils = new JMXTestUtils(this, "guest", "guest");
            jmxUtils.open();
        }
        catch (Exception e)
        {
            fail("Unable to establish JMX connection, test cannot proceed");
        }

        try
        {
            ManagedQueue queue = jmxUtils.getManagedQueue(QUEUE_NAME);
            assertFalse("Queue should not have been marked as Exclusive during upgrade", queue.isExclusive());

            ManagedQueue dursubQueue = jmxUtils.getManagedQueue("clientid" + ":" + SUB_NAME);
            assertTrue("DurableSubscription backing queue should have been marked as Exclusive during upgrade", dursubQueue.isExclusive());
        }
        finally
        {
            jmxUtils.close();
        }
    }

    /**
     * Test that the upgraded queue continues to function properly when used
     * for persistent messaging and restarting the broker. 
     * 
     * Sends the new messages to the queue BEFORE consuming those which were
     * sent before the upgrade. In doing so, this also serves to test that 
     * the queue bindings were successfully transitioned during the upgrade.
     */
    public void testBindingAndMessageDurabability() throws Exception
    {
        // Create a connection and start it
        TopicConnection connection = (TopicConnection) getConnection();
        connection.start();

        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Queue queue = session.createQueue(QUEUE_NAME);
        MessageProducer messageProducer = session.createProducer(queue);

        // Send a new message
        BDBStoreUpgradeTestPreparer.sendMessages(session, messageProducer, queue, DeliveryMode.PERSISTENT, 256*1024, 1);

        session.close();

        // Restart the broker
        restartBroker();

        // Drain the queue of all messages
        connection = (TopicConnection) getConnection();
        connection.start();
        consumeQueueMessages(connection, true);
    }

    /**
     * Test that all of the committed persistent messages previously sent to 
     * the broker are properly received following update of the MetaData and
     * Content entries during the store upgrade process.
     */
    public void testConsumptionOfUpgradedMessages() throws Exception
    {
        // Create a connection and start it
        Connection connection = getConnection();
        connection.start();

        consumeDurableSubscriptionMessages(connection, true);
        consumeDurableSubscriptionMessages(connection, false);
        consumeQueueMessages(connection, false);
    }

    /**
     * Tests store migration containing messages for non-existing queue.
     *
     * @throws Exception
     */
    public void testMigrationOfMessagesForNonExistingQueues() throws Exception
    {
        stopBroker();

        // copy store data into a new location for adding of phantom message
        File storeLocation = new File(_fromDir);
        File target = new File(_toDirTwice);
        if (!target.exists())
        {
            target.mkdirs();
        }
        FileUtils.copyRecursive(storeLocation, target);

        // delete migrated data
        File directory = new File(_toDir);
        if (directory.exists() && directory.isDirectory())
        {
            FileUtils.delete(directory, true);
        }

        // test data
        String nonExistingQueueName = getTestQueueName();
        String messageText = "Test Phantom Message";

        // add message
        addMessageForNonExistingQueue(target, VERSION_4, nonExistingQueueName, messageText);

        String[] inputs = { "Yes", "Yes", "Yes" };
        upgradeBrokerStoreInInterractiveMode(_toDirTwice, _toDir, inputs);

        // start broker
        startBroker();

        // Create a connection and start it
        Connection connection = getConnection();
        connection.start();

        // consume a message for non-existing store
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Queue queue = session.createQueue(nonExistingQueueName);
        MessageConsumer messageConsumer = session.createConsumer(queue);
        Message message = messageConsumer.receive(1000);

        // assert consumed message
        assertNotNull("Message was not migrated!", message);
        assertTrue("Unexpected message received!", message instanceof TextMessage);
        String text = ((TextMessage) message).getText();
        assertEquals("Message migration failed!", messageText, text);
    }

    /**
     * An utility method to upgrade broker with simulation user interactions
     *
     * @param fromDir
     *            location of the store to migrate
     * @param toDir
     *            location of where migrated data will be stored
     * @param inputs
     *            user answers on upgrade tool questions
     * @throws Exception
     */
    private void upgradeBrokerStoreInInterractiveMode(String fromDir, String toDir, final String[] inputs)
            throws Exception
    {
        // save to restore system.in after data migration
        InputStream stdin = System.in;

        // set fake system in to simulate user interactions
        // FIXME: it is a quite dirty simulator of system input but it does the job
        System.setIn(new InputStream()
        {

            private int counter = 0;

            public synchronized int read(byte b[], int off, int len)
            {
                byte[] src = (inputs[counter] + "\n").getBytes();
                System.arraycopy(src, 0, b, off, src.length);
                counter++;
                return src.length;
            }

            @Override
            public int read() throws IOException
            {
                return -1;
            }
        });

        try
        {
            // Upgrade the test store.
            new BDBStoreUpgrade(fromDir, toDir, null, true, true).upgradeFromVersion(VERSION_4);
        }
        finally
        {
            // restore system in
            System.setIn(stdin);
        }
    }

    @SuppressWarnings("unchecked")
    private void addMessageForNonExistingQueue(File storeLocation, int storeVersion, String nonExistingQueueName,
            String messageText) throws Exception
    {
        final AMQShortString queueName = new AMQShortString(nonExistingQueueName);
        BDBMessageStore store = new BDBMessageStore(storeVersion);
        store.configure(storeLocation, false);
        try
        {
            store.start();

            // store message objects
            ByteBuffer completeContentBody = ByteBuffer.wrap(messageText.getBytes("UTF-8"));
            long bodySize = completeContentBody.limit();
            MessagePublishInfo pubInfoBody = new MessagePublishInfoImpl(new AMQShortString("amq.direct"), false,
                    false, queueName);
            BasicContentHeaderProperties props = new BasicContentHeaderProperties();
            props.setDeliveryMode(Integer.valueOf(BasicContentHeaderProperties.PERSISTENT).byteValue());
            props.setContentType("text/plain");
            props.setType("text/plain");
            props.setMessageId("whatever");
            props.setEncoding("UTF-8");
            props.getHeaders().setString("Test", "MST");
            MethodRegistry methodRegistry = MethodRegistry.getMethodRegistry(ProtocolVersion.v0_9);
            int classForBasic = methodRegistry.createBasicQosOkBody().getClazz();
            ContentHeaderBody contentHeaderBody = new ContentHeaderBody(classForBasic, 1, props, bodySize);

            // add content entry to database
            final long messageId = store.getNewMessageId();
            TupleBinding<MessageContentKey> contentKeyTB = new MessageContentKeyTupleBindingFactory(storeVersion).getInstance();
            MessageContentKey contentKey = null;
            if (storeVersion == VERSION_4)
            {
                contentKey = new MessageContentKey_4(messageId, 0);
            }
            else
            {
                throw new Exception(storeVersion + " is not supported");
            }
            DatabaseEntry key = new DatabaseEntry();
            contentKeyTB.objectToEntry(contentKey, key);
            DatabaseEntry data = new DatabaseEntry();
            ContentTB contentTB = new ContentTB();
            contentTB.objectToEntry(completeContentBody, data);
            store.getContentDb().put(null, key, data);

            // add meta data entry to database
            TupleBinding<Long> longTB = TupleBinding.getPrimitiveBinding(Long.class);
            TupleBinding<Object> metaDataTB = new MessageMetaDataTupleBindingFactory(storeVersion).getInstance();
            key = new DatabaseEntry();
            data = new DatabaseEntry();
            longTB.objectToEntry(new Long(messageId), key);
            MessageMetaData metaData = new MessageMetaData(pubInfoBody, contentHeaderBody, 1);
            metaDataTB.objectToEntry(metaData, data);
            store.getMetaDataDb().put(null, key, data);

            // add delivery entry to database
            TransactionLogResource mockQueue = new TransactionLogResource()
            {
                public String getResourceName()
                {
                    return queueName.asString();
                }
            };

            EnqueableMessage mockMessage = new EnqueableMessage()
            {
    
                public long getMessageNumber()
                {
                    return messageId;
                }

                public boolean isPersistent()
                {
                    return true;
                }

                public StoredMessage getStoredMessage()
                {
                    return null;
                }
            };

            MessageStore log = (MessageStore) store;
            MessageStore.Transaction txn = log.newTransaction();
            txn.enqueueMessage(mockQueue, mockMessage);
            txn.commitTran();
        }
        finally
        {
            // close store
            store.close();
        }
    }

    private void consumeDurableSubscriptionMessages(Connection connection, boolean selector) throws Exception
    {
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Topic topic = null;
        TopicSubscriber durSub = null;

        if(selector)
        {
            topic = session.createTopic(SELECTOR_TOPIC_NAME);
            durSub = session.createDurableSubscriber(topic, SELECTOR_SUB_NAME,"testprop='true'", false);
        }
        else
        {
            topic = session.createTopic(TOPIC_NAME);
            durSub = session.createDurableSubscriber(topic, SUB_NAME);
        }


        // Retrieve the matching message 
        Message m = durSub.receive(2000);
        assertNotNull("Failed to receive an expected message", m);
        if(selector)
        {
            assertEquals("Selector property did not match", "true", m.getStringProperty("testprop"));
        }
        assertEquals("ID property did not match", 1, m.getIntProperty("ID"));
        assertEquals("Message content was not as expected",BDBStoreUpgradeTestPreparer.generateString(1024) , ((TextMessage)m).getText());

        // Verify that no more messages are received
        m = durSub.receive(1000);
        assertNull("No more messages should have been recieved", m);

        durSub.close();
        session.close();
    }

    private void consumeQueueMessages(Connection connection, boolean extraMessage) throws Exception
    {
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Queue queue = session.createQueue(QUEUE_NAME);

        MessageConsumer consumer = session.createConsumer(queue);
        Message m;

        // Retrieve the initial pre-upgrade messages
        for (int i=1; i <= 5 ; i++)
        {
            m = consumer.receive(2000);
            assertNotNull("Failed to receive an expected message", m);
            assertEquals("ID property did not match", i, m.getIntProperty("ID"));
            assertEquals("Message content was not as expected", STRING_1024_256, ((TextMessage)m).getText());
        }
        for (int i=1; i <= 5 ; i++)
        {
            m = consumer.receive(2000);
            assertNotNull("Failed to receive an expected message", m);
            assertEquals("ID property did not match", i, m.getIntProperty("ID"));
            assertEquals("Message content was not as expected", STRING_1024, ((TextMessage)m).getText());
        }

        if(extraMessage)
        {
            //verify that the extra message is received
            m = consumer.receive(2000);
            assertNotNull("Failed to receive an expected message", m);
            assertEquals("ID property did not match", 1, m.getIntProperty("ID"));
            assertEquals("Message content was not as expected", STRING_1024_256, ((TextMessage)m).getText());
        }

        // Verify that no more messages are received
        m = consumer.receive(1000);
        assertNull("No more messages should have been recieved", m);

        consumer.close();
        session.close();
    }

    private void upgradeBrokerStore(String fromDir, String toDir) throws Exception
    {
        new BDBStoreUpgrade(_fromDir, _toDir, null, false, true).upgradeFromVersion(VERSION_4);
    }
}
