/*******************************************************************************
 * Copyright (c) 2015, 2016 Oracle and/or its affiliates.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     05/11/2015-2.7 Tomas Kraus
 *       - Initial API and implementation.
 ******************************************************************************/
package org.eclipse.persistence.testing.tests.jpa;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.persistence.spi.PersistenceUnitInfo;

import junit.framework.Test;
import junit.framework.TestSuite;

import org.eclipse.persistence.internal.databaseaccess.Accessor;
import org.eclipse.persistence.internal.helper.JPAClassLoaderHolder;
import org.eclipse.persistence.internal.jpa.IsolatedHashMap;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.platform.server.ServerPlatform;
import org.eclipse.persistence.platform.server.ServerPlatformUtils;
import org.eclipse.persistence.sessions.DatabaseSession;
import org.eclipse.persistence.sessions.factories.SessionManager;
import org.eclipse.persistence.testing.framework.ReflectionHelper;
import org.eclipse.persistence.testing.framework.junit.JUnitTestCase;

/**
 * Tests partition isolated {@link HashMap}.
 */
public class IsolatedHashMapTest extends JUnitTestCase {

    /**
     * {@link ServerPlatform} mocking class. Allows changing current partition ID
     * during the test.
     */
    private static class TestServerPlatform implements ServerPlatform {

        /** Current partition ID. */
        private String partitionId;

        /**
         * Created an instance of {@code ServerPlatform} mocking class.
         * Current partition ID is set to default value.
         */
        private TestServerPlatform() {
            partitionId = DEFAULT_PARTITION_ID;
        }

        /**
         * Check whether this platform uses partitions.
         * @return Always returns {@code true}.
         */
        @Override
        public boolean usesPartitions() {
            return true;
        }

        /**
         * Get current partition ID.
         * @return Current partition ID.
         */
        @Override
        public String getPartitionID() {
            return partitionId;
        }

        /**
         * Set partition ID to be used as current partition context.
         * @param id New partition ID.
         */
        private void setPartitionID(final String id) {
            partitionId = id;
        }

        @Override
        public DatabaseSession getDatabaseSession() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public String getServerNameAndVersion() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public String getModuleName() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public Class getExternalTransactionControllerClass() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public void setExternalTransactionControllerClass(final Class newClass) {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public void initializeExternalTransactionController() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public boolean isJTAEnabled() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public boolean isRuntimeServicesEnabledDefault() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public void disableJTA() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public boolean isRuntimeServicesEnabled() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public void disableRuntimeServices() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public void registerMBean() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public void unregisterMBean() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public void shutdown() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public int getThreadPoolSize() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public void setThreadPoolSize(final int threadPoolSize) {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public Connection unwrapConnection(final Connection connection) {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public void launchContainerRunnable(final Runnable runnable) {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public SessionLog getServerLog() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public boolean shouldUseDriverManager() {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public boolean wasFailureCommunicationBased(
                final SQLException exception, final Accessor connection, final AbstractSession sessionForProfile) {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public JPAClassLoaderHolder getNewTempClassLoader(final PersistenceUnitInfo puInfo) {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public void clearStatementCache(Connection connection) {
            throw new UnsupportedOperationException("Not implemented");
        }

        @Override
        public int getJNDIConnectorLookupType() {
            throw new UnsupportedOperationException("Not implemented");
        }

    }

    /** Session name and partition ID separator. */
    private static final char SEPARATOR = '$';

    /** Default short enough partition ID when server does not support partitions.*/
    private static final String DEFAULT_PARTITION_ID = "0";

    /** Tested map shall be stared trough whole JVM to run test against the same map for all configured partitions. */
    private static final Map<String, String> map = IsolatedHashMap.newMap();

    /** Detected server platform. */
    private static final ServerPlatform serverPlatform;

    /** Does platform support partitions? */
    private static final boolean supportPartitions;

    /** Session name templates used to generate session names in the tests. */
    private static final String[] sessionNameTemplates = {
        "", "0", "mySession", "EclipseLink"
    };

    /** Class initialization code. */
    static {
        serverPlatform = ServerPlatformUtils.createServerPlatform(
                null, ServerPlatformUtils.detectServerPlatform(null), SessionManager.class.getClassLoader());
        // False value also handles case when serverPlatform is null to avoid NPE.
        supportPartitions = serverPlatform != null ? serverPlatform.usesPartitions() : false;
    }

    /**
     * Creates jUnit test suite for partition isolated {@link HashMap} tests.
     * @return new jUnit test suite containing all the tests.
     */
    public static Test suite() {
        TestSuite suite = new TestSuite();
        suite.setName("IsolatedHashMapTest");
        suite.addTest(new IsolatedHashMapTest("testIsolationInRealContext"));
        suite.addTest(new IsolatedHashMapTest("testIsolationInMockedContext"));
        return suite;
    }

    /**
     * Initialize session names in current partition context.
     * Test initialization helper.
     * @param partitionId Current partition ID. Shall not be {@code null}.
     */
     private static String[] initSessionNames(final String partitionId) {
         final int count = sessionNameTemplates.length;
         final String[] sessionNames = new String[count];
         int maxLength = 0;
         // Calculate StringBuilder internal storage size to avoid resizing.
         for (int i = 0; i < count; i++) {
             final int sessionNameTemplateLength = sessionNameTemplates[i].length();
             if (sessionNameTemplateLength > maxLength) {
                 maxLength = sessionNameTemplateLength;
             }
         }
         maxLength += 1 + partitionId.length();
         final StringBuilder sb = new StringBuilder(maxLength);
         for (int i = 0; i < count; i++) {
             if (i > 0) {
                 sb.delete(0, sb.length() - 1);
             }
             sb.append(sessionNameTemplates[i]);
             sb.append(SEPARATOR);
             sb.append(partitionId);
             sessionNames[i] = sb.toString();
         }
         return sessionNames;
     }

     /**
     * Creates an instance of partition isolated {@link HashMap} test.
     */
    public IsolatedHashMapTest() {
        super();
    }

    /**
     * Creates an instance of partition isolated {@link HashMap} test.
     * @param name jUnit test name.
     */
    public IsolatedHashMapTest(final String name) {
        super(name);
    }

    /**
     * Do partition isolation check in current partition context.
     * @param partitionId Current partition ID that should be visible in {@code map}.
     */
    private static void doIsolationCheck(final Map<String, String> map, final String partitionId, final SessionLog log) {
        final String[] sessionNames = initSessionNames(partitionId);
        final Set<String> namesSet = new HashSet<>(sessionNames.length);
        log.log(SessionLog.INFO, "  Partition ID: " + partitionId);
        for (String sessionName : sessionNames) {
            log.log(SessionLog.INFO, "    Adding session name: " + sessionName);
            map.put(sessionName, sessionName);
            namesSet.add(sessionName);
        }
        // Map shall contain session names in current partition context only.
        final int mapSize = map.size();
        log.log(SessionLog.INFO, "  Checking map size: session names count = "
                + sessionNames.length + ", map size = " + mapSize );
        assertEquals("Map size does not match session names count.", sessionNames.length, mapSize);
        // Verify individual session names.
        for (String sessionName : map.keySet()) {
            log.log(SessionLog.INFO, "    Getting and checking session name: " + sessionName);
            assertTrue("Session name " + sessionName + "was not stored in current partition context.",
                    namesSet.contains(sessionName));
        }
    }

    /**
     * Test partition isolation in real context with real {@link ServerPlatform}.
     */
    public void testIsolationInRealContext() {
        final SessionLog log = getServerSession().getSessionLog();
        log.log(SessionLog.INFO, "IsolatedHashMapTest.testIsolationInRealContext()");
        doIsolationCheck(map, supportPartitions ? serverPlatform.getPartitionID() : DEFAULT_PARTITION_ID, log);
    }

    /**
     * Test partition isolation in mocked partition context.
     */
    public void testIsolationInMockedContext() {
        final SessionLog log = getServerSession().getSessionLog();
        log.log(SessionLog.INFO, "IsolatedHashMapTest.testIsolationInMockedContext()");
        // Replace serverPlatform with mocked instance in IsolatedHashMap.
        final TestServerPlatform testPlatform = new TestServerPlatform();
        Field serverPlatformField;
        Field supportPartitions;
        try {
            final ServerPlatform originalPlatform
                    = (ServerPlatform)ReflectionHelper.getPrivateStatic(IsolatedHashMap.class, "serverPlatform");
            final boolean originalsupport
                    = ReflectionHelper.getPrivateStatic(IsolatedHashMap.class, "supportPartitions");
            log.log(SessionLog.INFO,
                    "Original platform field instance: " + originalPlatform.getClass().getName());
            log.log(SessionLog.INFO, "Original partitions support flag: " + Boolean.toString(originalsupport));
            ReflectionHelper.setPrivateStaticFinal(IsolatedHashMap.class, "serverPlatform", testPlatform);
            ReflectionHelper.setPrivateStaticFinal(
                    IsolatedHashMap.class, "supportPartitions", testPlatform.usesPartitions());
            log.log(SessionLog.INFO,
                    "Mocked platform field instance: " + testPlatform.getClass().getName());
            log.log(SessionLog.INFO, "Mocked partitions support flag: "
                    + Boolean.toString(testPlatform.usesPartitions()));
            // Run the test in mocked partition context.
            final Map<String, String> map = IsolatedHashMap.newMap();
            log.log(SessionLog.INFO, "Created " + map.getClass().getName() + "instance");
            final String[] partitionIds = {"first", "second", "third"};
            for (String partitionId : partitionIds) {
                testPlatform.setPartitionID(partitionId);
                doIsolationCheck(map, partitionId, log);
            }
            // Return original serverPlatform instance into IsolatedHashMap.
            ReflectionHelper.setPrivateStaticFinal(IsolatedHashMap.class, "serverPlatform", originalPlatform);
            ReflectionHelper.setPrivateStaticFinal(IsolatedHashMap.class, "supportPartitions", originalsupport);
        } catch (ReflectiveOperationException | SecurityException e) {
            log.logThrowable(SessionLog.WARNING, e);
            e.printStackTrace();
        }
    }

}
