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

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;

import org.apache.qpid.server.model.Binding;
import org.apache.qpid.server.model.Exchange;
import org.apache.qpid.server.model.LifetimePolicy;
import org.apache.qpid.server.model.Queue;
import org.apache.qpid.server.model.UUIDGenerator;
import org.apache.qpid.server.model.VirtualHost;
import org.apache.qpid.server.util.MapJsonSerializer;

import com.sleepycat.bind.tuple.TupleBinding;
import com.sleepycat.bind.tuple.TupleInput;
import com.sleepycat.bind.tuple.TupleOutput;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.Transaction;

public class UpgradeFrom7To8Test extends AbstractUpgradeTestCase
{
    private static final String ARGUMENTS = "arguments";

    private static final String CONFIGURED_OBJECTS_DB_NAME = "CONFIGURED_OBJECTS";
    private static final String CONFIGURED_OBJECT_HIERARCHY_DB_NAME = "CONFIGURED_OBJECT_HIERARCHY";

    @Override
    public VirtualHost<?,?,?> getVirtualHost()
    {
        VirtualHost<?,?,?> virtualHost = mock(VirtualHost.class);
        when(virtualHost.getName()).thenReturn("test");
        return virtualHost;
    }

    @Override
    protected String getStoreDirectoryName()
    {
        return "bdbstore-v7";
    }

    public void testPerformUpgrade() throws Exception
    {
        UpgradeFrom7To8 upgrade = new UpgradeFrom7To8();
        upgrade.performUpgrade(_environment, UpgradeInteractionHandler.DEFAULT_HANDLER, getVirtualHost());

        assertDatabaseRecordCount(CONFIGURED_OBJECTS_DB_NAME, 11);
        assertDatabaseRecordCount(CONFIGURED_OBJECT_HIERARCHY_DB_NAME, 13);

        assertConfiguredObjects();
        assertConfiguredObjectHierarchy();
    }


    private void assertConfiguredObjectHierarchy()
    {
        Map<UpgradeHierarchyKey, UUID> hierarchy = loadConfiguredObjectHierarchy();
        assertEquals("Unexpected number of configured objects", 13, hierarchy.size());

        UUID vhUuid = UUIDGenerator.generateVhostUUID(getVirtualHost().getName());
        UUID myExchUuid = UUIDGenerator.generateExchangeUUID("myexch", getVirtualHost().getName());
        UUID amqDirectUuid = UUIDGenerator.generateExchangeUUID("amq.direct", getVirtualHost().getName());
        UUID queue1Uuid = UUIDGenerator.generateQueueUUID("queue1", getVirtualHost().getName());
        UUID queue1ToAmqDirectBindingUuid = UUIDGenerator.generateBindingUUID("amq.direct", "queue1", "queue1", getVirtualHost().getName());

        // myexch -> virtualhost
        UpgradeHierarchyKey myExchToVhParent = new UpgradeHierarchyKey(myExchUuid, VirtualHost.class.getSimpleName());
        assertExpectedHierarchyEntry(hierarchy, myExchToVhParent, vhUuid);

        // queue1 -> virtualhost
        UpgradeHierarchyKey queue1ToVhParent = new UpgradeHierarchyKey(queue1Uuid, VirtualHost.class.getSimpleName());
        assertExpectedHierarchyEntry(hierarchy, queue1ToVhParent, vhUuid);

        // queue1binding -> amq.direct
        // queue1binding -> queue1
        UpgradeHierarchyKey queue1BindingToAmqDirect = new UpgradeHierarchyKey(queue1ToAmqDirectBindingUuid, Exchange.class.getSimpleName());
        UpgradeHierarchyKey queue1BindingToQueue1 = new UpgradeHierarchyKey(queue1ToAmqDirectBindingUuid, Queue.class.getSimpleName());
        assertExpectedHierarchyEntry(hierarchy, queue1BindingToAmqDirect, amqDirectUuid);
        assertExpectedHierarchyEntry(hierarchy, queue1BindingToQueue1, queue1Uuid);

        String[] defaultExchanges = {"amq.topic", "amq.fanout", "amq.direct", "amq.match"};
        for (String exchangeName : defaultExchanges)
        {
            UUID id = UUIDGenerator.generateExchangeUUID(exchangeName, getVirtualHost().getName());
            UpgradeHierarchyKey exchangeParent = new UpgradeHierarchyKey(id, VirtualHost.class.getSimpleName());
            assertExpectedHierarchyEntry(hierarchy, exchangeParent, vhUuid);
        }

    }

    private void assertExpectedHierarchyEntry(
            Map<UpgradeHierarchyKey, UUID> hierarchy,
            UpgradeHierarchyKey childHierarchyKey, UUID parentUUID)
    {
        assertTrue("Expected hierarchy entry does not exist", hierarchy.containsKey(childHierarchyKey));
        assertEquals("Expected hierarchy entry does not exist", parentUUID, hierarchy.get(childHierarchyKey));
    }


    private void assertConfiguredObjects()
    {
        Map<UUID, UpgradeConfiguredObjectRecord> configuredObjects = loadConfiguredObjects();
        assertEquals("Unexpected number of configured objects", 11, configuredObjects.size());

        Map<UUID, Map<String, Object>> expected = new HashMap<UUID, Map<String, Object>>();

        String configVersion = "0.3";
        expected.putAll(createExpectedVirtualHost(configVersion));

        expected.putAll(createExpectedQueue("queue1", Boolean.FALSE, null, null));
        expected.putAll(createExpectedQueue("queue2", Boolean.FALSE, null, null));

        expected.putAll(createExpectedExchangeMap("myexch", "direct"));

        expected.putAll(createExpectedBindingMap("queue1", "queue1", "amq.direct", null));
        expected.putAll(createExpectedBindingMap("queue1", "queue1", "myexch", null));
        expected.putAll(createExpectedBindingMap("queue2", "queue2", "amq.fanout", null));

        expected.putAll(createExpectedExchangeMap("amq.direct", "direct"));
        expected.putAll(createExpectedExchangeMap("amq.fanout", "fanout"));
        expected.putAll(createExpectedExchangeMap("amq.match", "headers"));
        expected.putAll(createExpectedExchangeMap("amq.topic", "topic"));

        MapJsonSerializer jsonSerializer = new MapJsonSerializer();
        for (Entry<UUID, UpgradeConfiguredObjectRecord> entry : configuredObjects.entrySet())
        {
            UpgradeConfiguredObjectRecord object = entry.getValue();

            UUID actualKey = entry.getKey();
            String actualType = object.getType();
            String actualJson = object.getAttributes();
            Map<String, Object> actualDeserializedAttributes = jsonSerializer.deserialize(actualJson);

            assertTrue("Entry UUID " + actualKey + " of type " +  actualType + " is unexpected", expected.containsKey(actualKey));

            Map<String, Object> expectedDeserializedAttributes = expected.get(actualKey);

            assertEquals("Entry UUID " + actualKey + " of type " +  actualType + " has uenxpected deserialised value, json was: " + actualJson,
                         expectedDeserializedAttributes, actualDeserializedAttributes);
        }
    }

    private Map<UUID, Map<String, Object>> createExpectedVirtualHost(String modelVersion)
    {
        Map<String, Object> expectedVirtualHostEntry = new HashMap<String, Object>();
        expectedVirtualHostEntry.put("modelVersion", modelVersion);
        expectedVirtualHostEntry.put(VirtualHost.NAME, getVirtualHost().getName());

        UUID expectedUUID = UUIDGenerator.generateVhostUUID(getVirtualHost().getName());
        return Collections.singletonMap(expectedUUID, expectedVirtualHostEntry);
    }

    private Map<UUID, Map<String, Object>> createExpectedQueue(String queueName, boolean exclusiveFlag, String owner, Map<String, Object> argumentMap)
    {
        Map<String, Object> expectedQueueEntry = new HashMap<String, Object>();
        expectedQueueEntry.put(Queue.NAME, queueName);
        expectedQueueEntry.put(Queue.EXCLUSIVE, exclusiveFlag);
        expectedQueueEntry.put(Queue.OWNER, owner);
        expectedQueueEntry.put(Queue.TYPE, "standard");

        if (argumentMap != null)
        {
            expectedQueueEntry.put(ARGUMENTS, argumentMap);
        }
        UUID expectedUUID = UUIDGenerator.generateQueueUUID(queueName, getVirtualHost().getName());
        return Collections.singletonMap(expectedUUID, expectedQueueEntry);
    }

    private Map<UUID, Map<String, Object>> createExpectedExchangeMap(String exchangeName, String type)
    {
        Map<String, Object> expectedExchangeMap = new HashMap<String, Object>();
        expectedExchangeMap.put(Exchange.NAME, exchangeName);
        expectedExchangeMap.put(Exchange.TYPE, type);
        expectedExchangeMap.put(Exchange.LIFETIME_POLICY, LifetimePolicy.PERMANENT.name());
        UUID expectedUUID = UUIDGenerator.generateExchangeUUID(exchangeName, getVirtualHost().getName());
        return Collections.singletonMap(expectedUUID, expectedExchangeMap);
    }

    private Map<UUID, Map<String, Object>> createExpectedBindingMap(String queueName, String bindingName, String exchangeName, Map<String, String> argumentMap)
    {
        Map<String, Object> expectedBinding = new HashMap<String, Object>();
        expectedBinding.put(Binding.NAME, bindingName);
        expectedBinding.put(Binding.ARGUMENTS, argumentMap == null ? Collections.emptyMap() : argumentMap);

        UUID expectedUUID = UUIDGenerator.generateBindingUUID(exchangeName, queueName, bindingName, getVirtualHost().getName());
        return Collections.singletonMap(expectedUUID, expectedBinding);
    }

    private Map<UUID, UpgradeConfiguredObjectRecord> loadConfiguredObjects()
    {
        final Map<UUID, UpgradeConfiguredObjectRecord> configuredObjectsRecords = new HashMap<UUID, UpgradeConfiguredObjectRecord>();
        final UpgradeConfiguredObjectBinding binding = new UpgradeConfiguredObjectBinding();
        final UpgradeUUIDBinding uuidBinding = new UpgradeUUIDBinding();
        CursorOperation configuredObjectsCursor = new CursorOperation()
        {
            @Override
            public void processEntry(Database sourceDatabase, Database targetDatabase, Transaction transaction,
                    DatabaseEntry key, DatabaseEntry value)
            {
                UUID id = uuidBinding.entryToObject(key);
                UpgradeConfiguredObjectRecord object = binding.entryToObject(value);
                configuredObjectsRecords.put(id, object);
            }
        };
        new DatabaseTemplate(_environment, CONFIGURED_OBJECTS_DB_NAME, null).run(configuredObjectsCursor);
        return configuredObjectsRecords;
    }


    private Map<UpgradeHierarchyKey, UUID> loadConfiguredObjectHierarchy()
    {
        final Map<UpgradeHierarchyKey, UUID> hierarchyRecords = new HashMap<UpgradeHierarchyKey, UUID>();
        final UpgradeHierarchyKeyBinding hierarchyKeyBinding = new UpgradeHierarchyKeyBinding();
        final UpgradeUUIDBinding uuidParentBinding = new UpgradeUUIDBinding();
        CursorOperation hierarchyCursor = new CursorOperation()
        {
            @Override
            public void processEntry(Database sourceDatabase, Database targetDatabase, Transaction transaction,
                    DatabaseEntry key, DatabaseEntry value)
            {
                UpgradeHierarchyKey hierarchyKey = hierarchyKeyBinding.entryToObject(key);
                UUID parentId = uuidParentBinding.entryToObject(value);
                hierarchyRecords.put(hierarchyKey, parentId);
            }
        };
        new DatabaseTemplate(_environment, CONFIGURED_OBJECT_HIERARCHY_DB_NAME, null).run(hierarchyCursor);
        return hierarchyRecords;
    }

    private static class UpgradeConfiguredObjectBinding extends TupleBinding<UpgradeConfiguredObjectRecord>
    {
        @Override
        public UpgradeConfiguredObjectRecord entryToObject(TupleInput tupleInput)
        {
            String type = tupleInput.readString();
            String json = tupleInput.readString();
            UpgradeConfiguredObjectRecord configuredObject = new UpgradeConfiguredObjectRecord(type, json);
            return configuredObject;
        }

        @Override
        public void objectToEntry(UpgradeConfiguredObjectRecord object, TupleOutput tupleOutput)
        {
            throw new UnsupportedOperationException();
        }
    }

    private static class UpgradeConfiguredObjectRecord
    {
        private final String _attributes;
        private final String _type;

        public UpgradeConfiguredObjectRecord(String type, String attributes)
        {
            super();
            _attributes = attributes;
            _type = type;
        }

        public String getAttributes()
        {
            return _attributes;
        }

        public String getType()
        {
            return _type;
        }

    }

    private static class UpgradeUUIDBinding extends TupleBinding<UUID>
    {
        @Override
        public UUID entryToObject(final TupleInput tupleInput)
        {
            return new UUID(tupleInput.readLong(), tupleInput.readLong());
        }

        @Override
        public void objectToEntry(final UUID uuid, final TupleOutput tupleOutput)
        {
            throw new UnsupportedOperationException();
        }
    }

    private static class UpgradeHierarchyKeyBinding extends TupleBinding<UpgradeHierarchyKey>
    {
        @Override
        public UpgradeHierarchyKey entryToObject(TupleInput tupleInput)
        {
            UUID childId = new UUID(tupleInput.readLong(), tupleInput.readLong());
            String parentType = tupleInput.readString();

            return new UpgradeHierarchyKey(childId, parentType);
        }

        @Override
        public void objectToEntry(UpgradeHierarchyKey hk, TupleOutput tupleOutput)
        {
            throw new UnsupportedOperationException();
        }
    }

    private static class UpgradeHierarchyKey
    {
        private final UUID _childId;
        private final String _parentType;

        public UpgradeHierarchyKey(final UUID childId, final String parentType)
        {
            _childId = childId;
            _parentType = parentType;
        }

        @Override
        public boolean equals(final Object o)
        {
            if (this == o)
            {
                return true;
            }
            if (o == null || getClass() != o.getClass())
            {
                return false;
            }

            final UpgradeHierarchyKey that = (UpgradeHierarchyKey) o;

            if (!_childId.equals(that._childId))
            {
                return false;
            }
            if (!_parentType.equals(that._parentType))
            {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode()
        {
            int result = _childId.hashCode();
            result = 31 * result + _parentType.hashCode();
            return result;
        }

    }

}
