/*
 * Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     2010-10-27 - James Sutherland (Oracle) initial impl
package org.eclipse.persistence.testing.tests.jpa.partitioned;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.persistence.*;

import junit.framework.*;

import org.eclipse.persistence.config.ExclusiveConnectionMode;
import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.eclipse.persistence.descriptors.partitioning.CustomPartitioningPolicy;
import org.eclipse.persistence.descriptors.partitioning.PartitioningPolicy;
import org.eclipse.persistence.descriptors.partitioning.RangePartitioningPolicy;
import org.eclipse.persistence.descriptors.partitioning.RoundRobinPartitioningPolicy;
import org.eclipse.persistence.descriptors.partitioning.UnionPartitioningPolicy;
import org.eclipse.persistence.descriptors.partitioning.ValuePartitioningPolicy;
import org.eclipse.persistence.mappings.CollectionMapping;
import org.eclipse.persistence.testing.framework.junit.JUnitTestCase;
import org.eclipse.persistence.testing.framework.junit.JUnitTestCaseHelper;
import org.eclipse.persistence.testing.models.jpa.partitioned.*;
import org.eclipse.persistence.tools.schemaframework.PopulationManager;

public class PartitionedTestSuite extends JUnitTestCase {
    public static boolean validDatabase = true;
    public static boolean isRAC = false;

    public static Test suite() {
        TestSuite suite = new TestSuite("PartitioningTests");
        suite.addTest(new PartitionedTestSuite("testSetup"));
        suite.addTest(new PartitionedTestSuite("testReadEmployee"));
        suite.addTest(new PartitionedTestSuite("testReadAllEmployee"));
        suite.addTest(new PartitionedTestSuite("testPersistEmployee"));
        suite.addTest(new PartitionedTestSuite("testRemoveEmployee"));
        suite.addTest(new PartitionedTestSuite("testUpdateEmployee"));
        suite.addTest(new PartitionedTestSuite("testReadProject"));
        suite.addTest(new PartitionedTestSuite("testReadAllProject"));
        suite.addTest(new PartitionedTestSuite("testPersistProject"));
        suite.addTest(new PartitionedTestSuite("testRemoveProject"));
        suite.addTest(new PartitionedTestSuite("testUpdateProject"));
        suite.addTest(new PartitionedTestSuite("testPartitioning"));
        suite.addTest(new PartitionedTestSuite("testPersistPartitioning"));
        suite.addTest(new PartitionedTestSuite("testPersistOfficeWithLongName"));
        return suite;
    }

    public PartitionedTestSuite(String name) {
        super(name);
    }

    /**
     * Return the name of the persistence context this test uses.
     * This allow a subclass test to set this only in one place.
     */
    @Override
    public String getPersistenceUnitName() {
        return "partitioned";
    }

    /**
     * The setup is done as a test, both to record its failure, and to allow execution in the server.
     */
    public void testSetup() {
        Map properties = new HashMap(JUnitTestCaseHelper.getDatabaseProperties());
        if (getServerSession().getPlatform().isDerby()) {
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node2." + PersistenceUnitProperties.CONNECTION_POOL_MIN,
                    "2");
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node2." + PersistenceUnitProperties.CONNECTION_POOL_URL,
                    "jdbc:derby:node2;create=true");
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node3." + PersistenceUnitProperties.CONNECTION_POOL_URL,
                    "jdbc:derby:node3;create=true");
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node3." + PersistenceUnitProperties.CONNECTION_POOL_MAX,
                    "8");
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node3." + PersistenceUnitProperties.CONNECTION_POOL_FAILOVER,
                    "node2, node1");
        } else if (getServerSession().getPlatform().isH2()) {
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node2." + PersistenceUnitProperties.CONNECTION_POOL_MIN,
                    "2");
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node2." + PersistenceUnitProperties.CONNECTION_POOL_URL,
                    "jdbc:h2:test2");
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node3." + PersistenceUnitProperties.CONNECTION_POOL_URL,
                    "jdbc:h2:test3");
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node3." + PersistenceUnitProperties.CONNECTION_POOL_MAX,
                    "8");
        } else if (getServerSession().getPlatform().isHSQL()) {
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node2." + PersistenceUnitProperties.CONNECTION_POOL_MIN,
                    "2");
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node2." + PersistenceUnitProperties.CONNECTION_POOL_URL,
                    "jdbc:hsqldb:file:test2");
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node3." + PersistenceUnitProperties.CONNECTION_POOL_URL,
                    "jdbc:hsqldb:file:test3");
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node3." + PersistenceUnitProperties.CONNECTION_POOL_MAX,
                    "8");
        } else if (!isOnServer() && getServerSession().getPlatform().isOracle() && (getServerSession().getLogin().getURL().indexOf("ems56442") != -1)) {
            isRAC = true;
            // RAC testing (direct node).
            String url = getServerSession().getLogin().getURL();
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node2." + PersistenceUnitProperties.CONNECTION_POOL_MIN,
                    "2");
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node2." + PersistenceUnitProperties.CONNECTION_POOL_URL,
                    (url.substring(0, url.length() - 1)) + "3");
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node3." + PersistenceUnitProperties.CONNECTION_POOL_URL,
                    url);
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node3." + PersistenceUnitProperties.CONNECTION_POOL_MAX,
                    "8");
        } else if (!isOnServer() && getServerSession().getPlatform().isOracle() && (getServerSession().getLogin().getURL().indexOf("@(DESCRIPTION") != -1)) {
            isRAC = true;
            // UCP RAC callback testing.
            properties.put(
                    PersistenceUnitProperties.PARTITIONING_CALLBACK,
                    "org.eclipse.persistence.platform.database.oracle.ucp.UCPDataPartitioningCallback");
        } else if (isOnServer()) {
            isRAC = true;
            try {
                Class.forName("weblogic.jdbc.common.internal.DataSourceManager");
            } catch (Exception notWebLogic) {
                warning("Partitioning tests only run on WebLogic with GridLink.");
                return;
            }
        } else {
            // Do not run on Sybase as may hang.
            if (getServerSession().getPlatform().isSybase()) {
                this.validDatabase = false;
                return;
            }
            isRAC = true;
            // Simulate a RAC using multiple connection pools to the same database.
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node2." + PersistenceUnitProperties.CONNECTION_POOL_MIN,
                    "2");
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node2." + PersistenceUnitProperties.CONNECTION_POOL_URL,
                    getServerSession().getLogin().getURL());
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node3." + PersistenceUnitProperties.CONNECTION_POOL_URL,
                    getServerSession().getLogin().getURL());
            properties.put(
                    PersistenceUnitProperties.CONNECTION_POOL + "node3." + PersistenceUnitProperties.CONNECTION_POOL_MAX,
                    "8");
        }
        getEntityManagerFactory(getPersistenceUnitName(), properties);
        if (isRAC) {
            // Disable replication and unioning in RAC.
            for (PartitioningPolicy policy : getDatabaseSession().getProject().getPartitioningPolicies().values()) {
                if (policy instanceof RoundRobinPartitioningPolicy) {
                    ((RoundRobinPartitioningPolicy)policy).setReplicateWrites(false);
                } else if (policy instanceof UnionPartitioningPolicy) {
                    ((UnionPartitioningPolicy)policy).setReplicateWrites(false);
                } else if (policy instanceof CustomPartitioningPolicy) {
                    ((EmployeePartitioningPolicy)((CustomPartitioningPolicy)policy).getPolicy()).setReplicate(false);
                } else if (policy instanceof RangePartitioningPolicy) {
                    ((RangePartitioningPolicy)policy).setUnionUnpartitionableQueries(false);
                } else if (policy instanceof ValuePartitioningPolicy) {
                    ((ValuePartitioningPolicy)policy).setUnionUnpartitionableQueries(false);
                }
            }
            CollectionMapping mapping = (CollectionMapping)getDatabaseSession().getDescriptor(Employee.class).getMappingForAttributeName("projects");
            PartitioningPolicy policy = getDatabaseSession().getProject().getPartitioningPolicy("defaut");
            mapping.setPartitioningPolicy(policy);
            mapping.getSelectionQuery().setPartitioningPolicy(policy);
            mapping = (CollectionMapping)getDatabaseSession().getDescriptor(Employee.class).getMappingForAttributeName("managedEmployees");
            mapping.setPartitioningPolicy(policy);
            mapping.getSelectionQuery().setPartitioningPolicy(policy);
        }
        new PartitionedTableCreator().replaceTables(getDatabaseSession());

        EntityManager em = createEntityManager();
        try {
            PopulationManager.resetDefaultManager();
            beginTransaction(em);
            new EmployeePopulator().persistExample(em);
            commitTransaction(em);
        } finally {
            closeEntityManagerAndTransaction(em);
        }
        clearCache();
    }

    /**
     * Test reading.
     */
    public void testReadEmployee() {
        if (!this.validDatabase) {
            return;
        }
        EntityManager em = createEntityManager();
        try {
            beginTransaction(em);
            Query query = em.createQuery("Select e from Employee e where e.firstName = :name");
            query.setParameter("name", "Bob");
            Employee employee = (Employee)query.getSingleResult();
            for (int index = 0; index < 3; index++) {
                if (!((Employee)query.getSingleResult()).getFirstName().equals("Bob")) {
                    fail("Employee not correct.");
                }
            }
            commitTransaction(em);
            closeEntityManager(em);
            for (int index = 0; index < 3; index++) {
                clearCache();
                em = createEntityManager();
                if (!(em.find(Employee.class, employee.pk())).getFirstName().equals("Bob")) {
                    fail("Employee not correct.");
                }
                closeEntityManager(em);
            }
        }  catch (RuntimeException e) {
            if (isTransactionActive(em)){
                rollbackTransaction(em);
            }
            closeEntityManager(em);
            throw e;
        }
        verifyObject(new EmployeePopulator().employeeExample1());
        verifyObject(new EmployeePopulator().employeeExample2());
        verifyObject(new EmployeePopulator().employeeExample3());
    }

    /**
     * Test reading.
     */
    public void testReadProject() {
        if (!this.validDatabase) {
            return;
        }
        EntityManager em = createEntityManager();
        try {
            beginTransaction(em);
            Query query = em.createQuery("Select p from Project p where p.name = :name");
            query.setParameter("name", "Sales Reporting");
            Project project = (Project)query.getSingleResult();
            for (int index = 0; index < 3; index++) {
                if (!((Project)query.getSingleResult()).getName().equals("Sales Reporting")) {
                    fail("Project not correct.");
                }
            }
            commitTransaction(em);
            closeEntityManager(em);
            for (int index = 0; index < 3; index++) {
                clearCache();
                em = createEntityManager();
                if (!(em.find(Project.class, project.getId())).getName().equals("Sales Reporting")) {
                    fail("Project not correct.");
                }
                closeEntityManager(em);
            }
        }  catch (RuntimeException e) {
            if (isTransactionActive(em)){
                rollbackTransaction(em);
            }
            closeEntityManager(em);
            throw e;
        }
        verifyObject(new EmployeePopulator().largeProjectExample1());
        verifyObject(new EmployeePopulator().largeProjectExample2());
        verifyObject(new EmployeePopulator().smallProjectExample1());
    }

    /**
     * Test reading.
     */
    public void testReadAllEmployee() {
        if (!this.validDatabase) {
            return;
        }
        EntityManager em = createEntityManager();
        try {
            beginTransaction(em);
            int size = em.createQuery("Select e from Employee e").getResultList().size();
            for (int index = 0; index < 3; index++) {
                if (size != em.createQuery("Select e from Employee e").getResultList().size()) {
                    fail("Query result size does not match.");
                }
            }
            commitTransaction(em);
            closeEntityManager(em);
        }  catch (RuntimeException e) {
            if (isTransactionActive(em)){
                rollbackTransaction(em);
            }
            closeEntityManager(em);
            throw e;
        }
    }

    /**
     * Test reading.
     */
    public void testReadAllProject() {
        if (!this.validDatabase) {
            return;
        }
        EntityManager em = createEntityManager();
        try {
            beginTransaction(em);
            int size = em.createQuery("Select p from Project p").getResultList().size();
            for (int index = 0; index < 3; index++) {
                if (size != em.createQuery("Select p from Project p").getResultList().size()) {
                    fail("Query result size does not match.");
                }
            }
            commitTransaction(em);
            closeEntityManager(em);
        }  catch (RuntimeException e) {
            if (isTransactionActive(em)){
                rollbackTransaction(em);
            }
            closeEntityManager(em);
            throw e;
        }
    }

    /**
     * Test remove.
     */
    public void testRemoveEmployee() {
        if (!this.validDatabase) {
            return;
        }
        for (int index = 0; index < 3; index++) {
            verifyPersistAndRemove(new EmployeePopulator().basicEmployeeExample1());
        }
    }

    /**
     * Test remove.
     */
    public void testRemoveProject() {
        if (!this.validDatabase) {
            return;
        }
        for (int index = 0; index < 3; index++) {
            verifyPersistAndRemove(new EmployeePopulator().basicLargeProjectExample2());
        }
    }

    /**
     * Test persist.
     */
    public void testPersistEmployee() {
        if (!this.validDatabase) {
            return;
        }
        for (int index = 0; index < 3; index++) {
            verifyPersist(new EmployeePopulator().basicEmployeeExample2());
        }
    }

    /**
     * Test persist an Office with a long name to produce a potential negative hashcode.
     * Bug 371514 - fragile hashing in HashPartitioningPolicy
     */
    public void testPersistOfficeWithLongName() {
        if (!this.validDatabase) {
            return;
        }
        for (int index = 0; index < 25; index++) {
            String longName = "Office with a very, very, very long name indeed";
            longName += String.valueOf(UUID.randomUUID());
            verifyPersist(new Office(longName, index + 1));
        }
    }

    /**
     * Test persist.
     */
    public void testPersistProject() {
        if (!this.validDatabase) {
            return;
        }
        for (int index = 0; index < 3; index++) {
            verifyPersist(new EmployeePopulator().basicLargeProjectExample1());
            verifyPersist(new EmployeePopulator().basicSmallProjectExample3());
        }
    }

    /**
     * Test update.
     */
    public void testUpdateEmployee() {
        if (!this.validDatabase) {
            return;
        }
        for (int index = 0; index < 3; index++) {
            EntityManager em = createEntityManager();
            try {
                beginTransaction(em);
                List<Employee> employees = em.createQuery("Select e from Employee e").getResultList();
                for (Employee employee : employees) {
                    employee.setLastName(employee.getLastName() + "2");
                    employee.addResponsibility("new" + index);
                    employee.getAddress().setStreet(employee.getAddress().getStreet() + "2");
                }
                commitTransaction(em);
                for (Employee employee : employees) {
                    verifyObject(employee);
                    verifyObject(employee.getAddress());
                }
                clearCache();
                for (Employee employee : employees) {
                    verifyObject(employee);
                    verifyObject(employee.getAddress());
                }
                closeEntityManager(em);
            } catch (RuntimeException e) {
                if (isTransactionActive(em)){
                    rollbackTransaction(em);
                }
                closeEntityManager(em);
                throw e;
            }
        }
    }

    /**
     * Test update.
     */
    public void testUpdateProject() {
        if (!this.validDatabase) {
            return;
        }
        for (int index = 0; index < 3; index++) {
            EntityManager em = createEntityManager();
            try {
                beginTransaction(em);
                List<Project> projects = em.createQuery("Select p from Project p").getResultList();
                for (Project project : projects) {
                    project.setDescription(project.getDescription() + "2");
                }
                commitTransaction(em);
                for (Project project : projects) {
                    verifyObject(project);
                }
                clearCache();
                for (Project project : projects) {
                    verifyObject(project);
                }
                closeEntityManager(em);
            } catch (RuntimeException e) {
                if (isTransactionActive(em)){
                    rollbackTransaction(em);
                }
                closeEntityManager(em);
                throw e;
            }
        }
    }

    /**
     * Test that partitioning is being used.
     */
    public void testPartitioning() throws Exception {
        if (!this.validDatabase) {
            return;
        }
        boolean found = false;
        EntityManager em = createEntityManager();
        try {
            Project project = (Project)em.createQuery("Select p from Project p").getResultList().get(0);
            for (int index = 0; index < 3; index++) {
                beginTransaction(em);
                Query query = em.createNativeQuery("Select * from PART_PROJECT where PROJ_ID = ?");
                query.setParameter(1, project.getId());
                List results = query.getResultList();
                if (!results.isEmpty()) {
                    if (found && !isRAC) {
                        fail("Data found in more than one partition.");
                    }
                    found = true;
                } else if (isRAC) {
                    fail("Data not found in all partitions.");
                }
                commitTransaction(em);
            }
            if (!found) {
                fail("Data not found in any partition.");
            }
        } catch (Exception e) {
            if (isTransactionActive(em)){
                rollbackTransaction(em);
            }
            throw e;
        } finally {
            closeEntityManager(em);
        }
    }

    /**
     * Test exclusive partitioning with persist.
     * The persist should decide the connection, not the query.
     */
    public void testPersistPartitioning() throws Exception {
        if (!this.validDatabase) {
            return;
        }
        if (isOnServer()) {
            return;
        }
        for (int count = 0; count < 2; count++) {
            Map properties = new HashMap();
            properties.put(PersistenceUnitProperties.EXCLUSIVE_CONNECTION_MODE, ExclusiveConnectionMode.Always);
            EntityManager em = createEntityManager(properties);
            try {
                beginTransaction(em);
                Employee employee1 = new Employee();
                employee1.setLocation("Ottawa");
                em.persist(employee1);
                commitTransaction(em);
                closeEntityManager(em);
                clearCache();
                em = createEntityManager(properties);
                beginTransaction(em);
                Employee employee2 = new Employee();
                employee2.setLocation("Ottawa");
                em.persist(employee2);
                Employee result = em.find(Employee.class, new EmployeePK(employee1.getId(), "Toronto"));
                if (result != null) {
                    fail("Employee should not exist.");
                }
                result = em.find(Employee.class, new EmployeePK(employee1.getId(), "Ottawa"));
                rollbackTransaction(em);
                if (result == null) {
                    // Retry once, as sequence select could use wrong node.
                    if (count == 1) {
                        fail("Employee not found, wrong partition used.");
                    }
                } else {
                    return;
                }
            } finally {
                closeEntityManagerAndTransaction(em);
            }
        }
    }
}
