/*
 * Copyright (c) 2011, 2021 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:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.testing.tests.simultaneous;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Vector;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.helper.Helper;
import org.eclipse.persistence.logging.SessionLog;

import org.eclipse.persistence.queries.DeleteAllQuery;
import org.eclipse.persistence.queries.SQLCall;
import org.eclipse.persistence.sequencing.DefaultSequence;
import org.eclipse.persistence.sequencing.NativeSequence;
import org.eclipse.persistence.sequencing.Sequence;
import org.eclipse.persistence.sequencing.TableSequence;
import org.eclipse.persistence.sessions.DatabaseSession;
import org.eclipse.persistence.sessions.Session;
import org.eclipse.persistence.sessions.UnitOfWork;
import org.eclipse.persistence.testing.framework.TestCase;
import org.eclipse.persistence.testing.framework.TestErrorException;
import org.eclipse.persistence.testing.framework.TestProblemException;
import org.eclipse.persistence.testing.framework.TestWarningException;
import org.eclipse.persistence.testing.models.interfaces.InterfaceHashtableProject;
import org.eclipse.persistence.testing.models.employee.domain.*;

/**
 * Tests several scenarios of adding sequences and descriptors, sequence preallocation; inserting objects concurrently.
 */
public class AddDescriptorsMultithreadedTest extends MultithreadTestCase {

    protected static boolean allTestsShouldStop;

    protected AddDescriptorsMultithreadedTest() {
        super();
        setDescription("Runs provided tests concurently with adding descriptors.");
    }

    @Override
    protected void setup() {
        super.setup();
        allTestsShouldStop = false;
    }

    protected static AddDescriptorsMultithreadedTest createMultithreadedTest(int nAddDescriptorsTests, long timeToSleepBetweenAddingDescriptors, String testName, int nTests, long timeToStopTests) {
        AddDescriptorsMultithreadedTest test = new AddDescriptorsMultithreadedTest();
        Vector tests = new Vector(nAddDescriptorsTests + nTests);
        InterfaceHashtableProject project = new InterfaceHashtableProject();
        for (int i=0; i < nAddDescriptorsTests; i++) {
            tests.add(new AddDescriptorsTest(i, nAddDescriptorsTests, timeToSleepBetweenAddingDescriptors, project));
        }
        for (int i=0; i < nTests; i++) {
            if (testName.equals("SequencePreallocationTest")) {
                tests.add(new SequencePreallocationTest(i));
            } else if (testName.equals("InsertTest")) {
                tests.add(new InsertTest(i, nTests));
            } else {
                throw new TestProblemException("Unknown test name: " + testName);
            }
        }
        if (nAddDescriptorsTests == 0 && timeToStopTests > 0) {
            tests.add(new Timer(timeToStopTests));
        }
        test.setTests(tests);
        test.generateTestName();
        return test;
    }

    void generateTestName() {
        ArrayList<String> testNames = new ArrayList();
        HashMap<String, Integer> map = new HashMap();
        for (int i=0; i < this.test.length; i++) {
            String testName = this.test[i].getName();
            if (testNames.contains(testName)) {
                int count = map.get(testName);
                map.put(testName, count + 1);
            } else {
                testNames.add(testName);
                map.put(testName, 1);
            }
        }
        String name = "";
        for(int k=0; k < testNames.size(); k++) {
            String testName = testNames.get(k);
            if (k > 0) {
                name += ";_";
            }
            name += testName;
            int count = map.get(testName);
            if (count > 1) {
                name += "_" + map.get(testName)+"threads";
            }
        }
        setName(name);
    }

    /*
     * If AddDescriptorsTest not used then use Timer to stop the tests after specified time.
     */
    public static class Timer extends TestCase {
        Timer(long timeToStopTests) {
            this.timeToStopTests = timeToStopTests;
            setName("Timer("+timeToStopTests+")");
        }
        long timeToStopTests;
        @Override
        public void test() {
            try {
                Thread.sleep(timeToStopTests);
            } catch (InterruptedException ex) {
                throw new TestProblemException("Thread.sleep(timeToStopTests) failed ", ex);
            }
            allTestsShouldStop = true;
        }
    }

    /*
     * Adds descriptors, if timeToSleepBetweenAddingDescriptors then sleeps between adding descriptors.
     */
    public static class AddDescriptorsTest extends TestCase {
        public AddDescriptorsTest(int testNumber, int numberOfTests, long timeToSleepBetweenAddingDescriptors, InterfaceHashtableProject project) {
            this.testNumber = testNumber;
            this.numberOfTests = numberOfTests;
            this.timeToSleepBetweenAddingDescriptors = timeToSleepBetweenAddingDescriptors;
            this.project = project;
            String name = "AddDescriptorsTest";
            if (timeToSleepBetweenAddingDescriptors > 0) {
                name += "(" + timeToSleepBetweenAddingDescriptors + ")";
            }
            setName(name);
        }
        int testNumber;
        int numberOfTests;
        long timeToSleepBetweenAddingDescriptors;
        InterfaceHashtableProject project;
        static int numberOfCompletedTests = 0;
        static Object lock = Boolean.TRUE;
        @Override
        public void test() {
            DatabaseSession dbSession;
            if (getSession().isDatabaseSession()) {
                dbSession = (DatabaseSession)getSession();
            } else {
                // must be ClientSession
                dbSession = (DatabaseSession)(getAbstractSession().getParent());
            }
            int nSize = project.getOrderedDescriptors().size();
            // if numberOfTests = 10 then the first test uses k = 0, 10, 20 etc; the second k = 1, 11, 21 etc.
            for (int k = testNumber; k < nSize; k = k + numberOfTests) {
                ClassDescriptor descriptor = project.getOrderedDescriptors().get(k);
                getAbstractSession().log(SessionLog.FINEST, SessionLog.MISC, "AddDescriptorsTest adding descriptor for class = " + Helper.getShortClassName(descriptor.getJavaClass()), new Object[]{}, null, false);
                DatabaseField sequenceNumberField = descriptor.getMappingForAttributeName("id").getField();
                descriptor.setSequenceNumberField(sequenceNumberField);
                String seqName = "SEQ_" + sequenceNumberField.getTableName();
                descriptor.setSequenceNumberName(seqName);
                int k3 = k % 3;
                // try adding different sequence types
                if (k3 == 0) {
                    dbSession.addSequence(new NativeSequence(seqName));
                } else if (k == 1) {
                    dbSession.addSequence(new TableSequence(seqName));
                } else {
                    // use default sequence - nothing to do
                }
                // try using both addDescriptor and addDescriptors methods
                if (k % 2 == 0) {
                    dbSession.addDescriptor(descriptor);
                } else {
                    ArrayList descriptors = new ArrayList();
                    descriptors.add(descriptor);
                    dbSession.addDescriptors(descriptors);
                }
                if (timeToSleepBetweenAddingDescriptors > 0) {
                    try {
                        Thread.sleep(timeToSleepBetweenAddingDescriptors);
                    } catch (InterruptedException ex) {
                        throw new TestProblemException("Thread.sleep(timeToSleepBetweenAddingDescriptors) failed ", ex);
                    }
                }
            }
            synchronized(lock) {
                numberOfCompletedTests++;
                if (numberOfCompletedTests == numberOfTests) {
                    allTestsShouldStop = true;

                    // get ready for the next run
                    numberOfCompletedTests = 0;
                }
            }
        }
        @Override
        protected void verify() {
            DatabaseSession dbSession;
            if (getSession().isDatabaseSession()) {
                dbSession = (DatabaseSession)getSession();
            } else {
                // must be ClientSession
                dbSession = (DatabaseSession)(getAbstractSession().getParent());
            }
            int nSize = project.getOrderedDescriptors().size();
            // if numberOfTests = 10 then the first test uses k = 0, 10, 20 etc; the second k = 1, 11, 21 etc.
            for (int k = testNumber; k < nSize; k = k + numberOfTests) {
                ClassDescriptor descriptor = project.getOrderedDescriptors().get(k);
                descriptor = dbSession.getDescriptor(descriptor.getJavaClass());
                if (descriptor == null) {
                    throw new TestErrorException(descriptor + " is not found in the session");
                }
                DatabaseField sequenceNumberField = descriptor.getMappingForAttributeName("id").getField();
                String seqName = "SEQ_" + sequenceNumberField.getTableName();

                Sequence sequence = dbSession.getPlatform().getSequence(seqName);
                if (sequence == null) {
                    throw new TestErrorException("Not found sequence " + seqName + " defined for class " + descriptor.getAlias());
                }
                int k3 = k % 3;
                // try adding different sequence types
                if (k3 == 0) {
                    if (!sequence.isNative()) {
                        throw new TestErrorException(sequence + " defined for class " + descriptor.getAlias() + " is wrong. NativeSequence was expected");
                    }
                } else if (k == 1) {
                    if (!sequence.isTable()) {
                        throw new TestErrorException(sequence + " defined for class " + descriptor.getAlias() + " is wrong. TableSequence was expected");
                    }
                } else {
                    if (!(sequence instanceof DefaultSequence)) {
                        throw new TestErrorException(sequence + " defined for class " + descriptor.getAlias() + " is wrong. DefaultSequence was expected");
                    }
                }
            }
        }
        /*
         * Concurrently runs nAddDescriptorsTests AddDescriptorsTests.
         * If timeToSleepBetweenAddingDescriptors > 0 then each AddDescriptorTest sleeps after adding each descriptor.
         */
        public static AddDescriptorsMultithreadedTest createMultithreadedTest(int nAddDescriptorsTests, long timeToSleepBetweenAddingDescriptors) {
            return AddDescriptorsMultithreadedTest.createMultithreadedTest(nAddDescriptorsTests, timeToSleepBetweenAddingDescriptors, "", 0, 0);
        }
    }

    /*
     * In a loop assigns sequence number for different types
     * (which causes sequence number preallocation) then rolls back throwing all the preallocated numbers away.
     * Does not have verify method - tests for deadlocks.
     * Stops when either AddDescriptorsTest or Timer set allTestsShouldStop flag to false.
     */
    public static class SequencePreallocationTest extends TestCase {
        public SequencePreallocationTest(int testNumber) {
            this.testNumber = testNumber;
            setName("SequencePreallocationTest");
        }
        int testNumber;
        @Override
        public void test() {
            // Test execution causes deadlock on SQL Server
            if (getAbstractSession().getParent().getPlatform().isSQLServer()) {
                throw new TestWarningException("Not supported on MS SQL Server");
            }

            int index = 0;
            while (!allTestsShouldStop) {
                getAbstractSession().beginTransaction();
                try {
                    UnitOfWork uow = getSession().acquireUnitOfWork();
                    Object obj;
                    if (index == 0) {
                        obj = new Address();
                        index ++;
                    } else if (index == 1) {
                        obj = new Employee();
                        index ++;
                    } else {
                        obj = new SmallProject();
                        index = 0;
                    }
                    uow.assignSequenceNumber(obj);
                } finally {
                    getAbstractSession().rollbackTransaction();
                }
            }
        }
        /*
         * Concurrently runs nAddDescriptorsTests AddDescriptorTests and nTests SequencePreallocationTests.
         * If timeToSleepBetweenAddingDescriptors > 0 then each AddDescriptorTest sleep afters adding each descriptor.
         */
        public static AddDescriptorsMultithreadedTest createMultithreadedTestWithAddDescriptors(int nAddDescriptorsTests, long timeToSleepBetweenAddingDescriptors, int nTests) {
            return AddDescriptorsMultithreadedTest.createMultithreadedTest(nAddDescriptorsTests, timeToSleepBetweenAddingDescriptors, "SequencePreallocationTest", nTests, 0);
        }
        /*
         * Concurrently runs nTests SequencePreallocationTests.
         * timeToStopTest > 0 must be specified, or the test will run forever.
         */
        public static AddDescriptorsMultithreadedTest createMultithreadedTest(int nTests, long timeToStopTests) {
            return AddDescriptorsMultithreadedTest.createMultithreadedTest(0, 0, "SequencePreallocationTest", nTests, timeToStopTests);
        }
    }

    /*
     * In a loop inserts objects of different types.
     * Stops when either AddDescriptorsTest or Timer set allTestsShouldStop flag to false.
     * Does not have verify method - tests for deadlocks.
     * The last test to reset deletes all the inserted objects.
     */
    public static class InsertTest extends TestCase {
        public InsertTest(int testNumber, int numberOfTests) {
            this.testNumber = testNumber;
            this.numberOfTests = numberOfTests;
            setName("InsertTest");
        }
        int testNumber;
        int numberOfTests;
        static int numberOfCompletedTests = 0;
        static Object lock = Boolean.TRUE;
        @Override
        public void test() {
            // Test execution causes deadlock on SQL Server
            if (getAbstractSession().getParent().getPlatform().isSQLServer()) {
                throw new TestWarningException("Not supported on MS SQL Server");
            }

            int index3 = testNumber % 3;
            String strTestNumber = Integer.toString(testNumber);
            while (!allTestsShouldStop) {
                UnitOfWork uow = getSession().acquireUnitOfWork();
                Object obj;
                if (index3 == 0) {
                    Address address;
                    address = new Address();
                    address.setCity(strTestNumber);
                    address.setCountry("InsertTest");
                    obj = address;
                    index3 = 1;
                } else if (index3 == 1) {
                    Employee emp = new Employee();
                    emp.setFirstName(strTestNumber);
                    emp.setLastName("InsertTest");
                    obj = emp;
                    index3 = 2;
                } else {
                    SmallProject project = new SmallProject();
                    project.setName(strTestNumber);
                    project.setDescription("InsertTest");
                    obj = project;
                    index3 = 0;
                }
                uow.registerObject(obj);
                uow.commit();

            }
        }
        @Override
        public void reset() {
            // the last test deletes the objects created by all tests
            synchronized(lock) {
                numberOfCompletedTests++;
                if (numberOfCompletedTests == numberOfTests) {
                    Session session = getSession();
                    if(session.getPlatform().isSymfoware()) {
                        // Symfoware doesn't support DeleteAllQuery. Therefore call DELETE statements directly instead.
                        session.executeNonSelectingCall(new SQLCall("DELETE FROM ADDRESS WHERE (COUNTRY = 'InsertTest')"));
                        session.executeNonSelectingCall(new SQLCall("DELETE FROM SALARY WHERE EXISTS(SELECT t0.EMP_ID FROM EMPLOYEE t0 WHERE (t0.L_NAME = 'InsertTest') AND (t0.EMP_ID = SALARY.EMP_ID))"));
                        session.executeNonSelectingCall(new SQLCall("DELETE FROM PROJ_EMP WHERE EXISTS(SELECT t0.EMP_ID FROM EMPLOYEE t0 WHERE (t0.L_NAME = 'InsertTest') AND (t0.EMP_ID = PROJ_EMP.EMP_ID))"));
                        session.executeNonSelectingCall(new SQLCall("DELETE FROM RESPONS WHERE EXISTS(SELECT t0.EMP_ID FROM EMPLOYEE t0 WHERE (t0.L_NAME = 'InsertTest') AND (t0.EMP_ID = RESPONS.EMP_ID))"));
                        session.executeNonSelectingCall(new SQLCall("DELETE FROM PHONE WHERE EXISTS(SELECT t0.EMP_ID FROM EMPLOYEE t0 WHERE (t0.L_NAME = 'InsertTest') AND (t0.EMP_ID = PHONE.EMP_ID))"));
                        session.executeNonSelectingCall(new SQLCall("DELETE FROM CHILD WHERE EXISTS(SELECT t0.EMP_ID FROM EMPLOYEE t0 WHERE (t0.L_NAME = 'InsertTest') AND (t0.EMP_ID = CHILD.PARENT_EMP_ID))"));
                        session.executeNonSelectingCall(new SQLCall("DELETE FROM EMPLOYEE WHERE (L_NAME = 'InsertTest')"));
                        session.executeNonSelectingCall(new SQLCall("DELETE FROM PROJECT WHERE (DESCRIP = 'InsertTest')"));
                    } else {
                        // delete all created objects
                        UnitOfWork uow = session.acquireUnitOfWork();

                        DeleteAllQuery deleteAddresses = new DeleteAllQuery(Address.class);
                        deleteAddresses.setSelectionCriteria(deleteAddresses.getExpressionBuilder().get("country").equal("InsertTest"));
                        uow.executeQuery(deleteAddresses);

                        DeleteAllQuery deleteEmployees = new DeleteAllQuery(Employee.class);
                        deleteEmployees.setSelectionCriteria(deleteEmployees.getExpressionBuilder().get("lastName").equal("InsertTest"));
                        uow.executeQuery(deleteEmployees);

                        DeleteAllQuery deleteProjects = new DeleteAllQuery(SmallProject.class);
                        deleteProjects.setSelectionCriteria(deleteProjects.getExpressionBuilder().get("description").equal("InsertTest"));
                        uow.executeQuery(deleteProjects);

                        uow.commit();
                    }
                    // get ready for the next run
                    numberOfCompletedTests = 0;
                }
            }
        }
        /*
         * Concurrently runs nAddDescriptorsTests AddDescriptorTests and nTests InsertTests.
         * If timeToSleepBetweenAddingDescriptors > 0 then each AddDescriptorTest sleep afters adding each descriptor.
         */
        public static AddDescriptorsMultithreadedTest createMultithreadedTestWithAddDescriptors(int nAddDescriptorsTests, long timeToSleepBetweenAddingDescriptors, int nTests) {
            return AddDescriptorsMultithreadedTest.createMultithreadedTest(nAddDescriptorsTests, timeToSleepBetweenAddingDescriptors, "InsertTest", nTests, 0);
        }
        /*
         * Concurrently runs nTests InsertTests.
         * timeToStopTest > 0 must be specified, or the test will run forever.
         */
        public static AddDescriptorsMultithreadedTest createMultithreadedTest(int nTests, long timeToStopTests) {
            return AddDescriptorsMultithreadedTest.createMultithreadedTest(0, 0, "InsertTest", nTests, timeToStopTests);
        }
    }
}
