/*
 * Copyright (c) 1998, 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.io.*;
import java.util.*;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.sessions.*;
import org.eclipse.persistence.sessions.server.*;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.testing.framework.*;
import org.eclipse.persistence.testing.framework.ui.SynchronizedTester;
import org.eclipse.persistence.testing.framework.ui.SynchronizedTestExecutor;

public class MultithreadTestCase extends AutoVerifyTestCase {
    protected TestCase[] test;
    protected int numberOfTests;
    protected boolean useSequenceConnectionPool = false;

    protected class TestEventListenerImpl implements junit.framework.TestListener {
        public static final int INITIAL = 0;
        public static final int STARTED = 1;
        public static final int FINISHED = 2;
        private int state;

        private TestEventListenerImpl() {
            state = INITIAL;
        }

        @Override
        public void addError(junit.framework.Test test, Throwable error) {
        }

        @Override
        public void addFailure(junit.framework.Test test, junit.framework.AssertionFailedError error) {
        }

        @Override
        public void endTest(junit.framework.Test test) {
            state = FINISHED;
        }

        @Override
        public void startTest(junit.framework.Test test) {
            state = STARTED;
        }

        public boolean isStarted() {
            return state == STARTED;
        }

        public boolean isFinished() {
            return state == FINISHED;
        }
    }
    ;

    protected TestEventListenerImpl[] testExecutorListener;
    protected TestExecutorWithClientSession[] testExecutorWithClientSession;
    protected SynchronizedTestExecutor[] testThread;

    protected class SynchronizedTesterImpl implements SynchronizedTester {
        public static final int INITIAL = 0;
        public static final int FINISHED = 1;
        private int state;
        private Throwable exception;

        private SynchronizedTesterImpl() {
            state = INITIAL;
        }

        @Override
        public void finishedTest() {
            state = FINISHED;
        }

        @Override
        public void notifyException(Throwable exception) {
            this.exception = exception;
        }

        public boolean isFinished() {
            return state == FINISHED;
        }

        public Throwable getException() {
            return exception;
        }
    }
    ;

    protected SynchronizedTesterImpl[] testThreadListener;
    protected Hashtable allowedExceptions;
    private Session originalSession;

    /**
     * Default constructor for MultithreadTestCase
     * Added to allow easier subclassing.  This constructor should only be used by subclasses.
     * addTests() must be called immediately after this constructor by the subclass
     */
    protected MultithreadTestCase() {
        super();
        setDescription("Runs several tests simultaneously");
        allowedExceptions = new Hashtable();
        addAllowedException("org.eclipse.persistence.exceptions.OptimisticLockException");
    }

    // MultithreadTestCase runs simultaneously each of the TestCases
    // passed in Vector tests.
    public MultithreadTestCase(Vector test) {
        this();
        setTests(test);
    }

    // If an exception occur in one of concurrently run tests,
    // MultithreadTestCase assigned this exception (and therefore status "Failed"),
    // unless the exception is on allowedExceptions list (see verify()).
    // By default, only org.eclipse.persistence.exceptions.OptimisticLockException
    // is on this list.
    // Use addAllowedException and removeAllowedException to add/remove
    // an exception to allowedExceptions list.
    public boolean addAllowedException(String exceptionClassName) {
        try {
            allowedExceptions.put(exceptionClassName, Class.forName(exceptionClassName));
            return true;
        } catch (ClassNotFoundException classNotFoundException) {
            return false;
        }
    }

    public void removeAllowedException(String exceptionClassName) {
        allowedExceptions.remove(exceptionClassName);
    }

    /**
     * Set the tests in this test case.  Refactored for easier subclassing.
     */
    public void setTests(Vector test) {
        if ((test != null) && !test.isEmpty()) {
            numberOfTests = test.size();

            this.test = new TestCase[numberOfTests];
            test.toArray(this.test);

            testExecutorWithClientSession = new TestExecutorWithClientSession[numberOfTests];
            testExecutorListener = new TestEventListenerImpl[numberOfTests];
            testThread = new SynchronizedTestExecutor[numberOfTests];
            testThreadListener = new SynchronizedTesterImpl[numberOfTests];
        }
    }

    @Override
    protected void setup() {
        getSession().getIdentityMapAccessor().initializeAllIdentityMaps();
        setupSession();

        for (int i = 0; i < numberOfTests; i++) {
            // Need this for proper idention of while printing test results
            test[i].setContainer(this);

            // To run a test we need an executor.
            // To run simultaneously several tests we need a personal
            // executor for each of them.
            // For each test an instance of a subclass of TestExecutor is created:
            // TestExecutorWithClientSession:
            //   carries clientSession;
            //   readdresses some method calls to its parent - TestExecutor, disallows other ones;
            testExecutorWithClientSession[i] = new TestExecutorWithClientSession(getExecutor());

            // That's an optional TestExecutorListener.
            // Currently not used.
            testExecutorListener[i] = new TestEventListenerImpl();
            testExecutorWithClientSession[i].setListener(testExecutorListener[i]);

            // That's a ThreadListener - it receives call backs from
            // the thread - currently used is testFinished notification.
            testThreadListener[i] = new SynchronizedTesterImpl();

            test[i].setExecutor(testExecutorWithClientSession[i]);

            // That's a personal thread for the test
            testThread[i] = new SynchronizedTestExecutor(testExecutorWithClientSession[i], test[i], testThreadListener[i]);
            testThread[i].setName("Test Thread " + i);
        }
    }

    protected void setupSession() {
        originalSession = getSession();
        Session newSession = setupNewSession(originalSession, useSequenceConnectionPool);
        getExecutor().setSession(newSession);
    }

    // In order for MultithreadTestCase to run, it needs
    // a ServerSession to be returned by TestExecutor.getSession().
    // This method takes a session originally held by TestExecutor
    // (typically DatabaseSession) - and returns the ServerSession
    // which should be set as a Session into TestExecutor.
    // The possible variants:
    //   case originalSession is a ServerSession - will use it as a new session;
    //   case originalSession is a ClientSession - will use its parent as a new session.
    //
    // This is a static method so that it could be used by other classes.
    //
    // Use resetOriginalSession before setting the originalSession
    // back into TestExecutor.
    static public Session setupNewSession(Session originalSession, boolean useSequenceConnectionPool) {
        // Note that because ServerSession.isDatabaseSession() returns true
        // it is important to call isServerSession() before
        // isDatabaseSession() is called
        if (originalSession.isServerSession()) {
            return originalSession;
        } else if (originalSession.isDatabaseSession()) {
            // The following piece mostly copied from ClientServerTestModel
            // The only thing added is initializePreallocatedSequences() -
            // don't want to re-use the sequence numbers allocated by originalSession.
            DatabaseSession databaseSession = (DatabaseSession)originalSession;
            DatabaseLogin login = (DatabaseLogin)databaseSession.getLogin().clone();
            databaseSession.getSequencingControl().initializePreallocated();
            Server serverSession = new ServerSession(login, 5, 5);
            serverSession.setSessionLog(databaseSession.getSessionLog());
            if (useSequenceConnectionPool) {
                serverSession.getSequencingControl().setShouldUseSeparateConnection(true);
            } else {
                serverSession.getSequencingControl().setShouldUseSeparateConnection(false);
            }
            serverSession.login();

            Vector descriptors = new Vector();
            for (Iterator<ClassDescriptor> iterator = databaseSession.getDescriptors().values().iterator();
                 iterator.hasNext();) {
                descriptors.addElement(iterator.next());
            }
            serverSession.addDescriptors(descriptors);

            return serverSession;
        } else if (originalSession.isClientSession()) {
            ClientSession clientSession = (ClientSession)originalSession;
            ServerSession serverSession = clientSession.getParent();
            return serverSession;
        } else {
            // Should never happen
            return null;
        }
    }

    // Note that currently there is nothing done
    // to resolve possible deadlocks.
    @Override
    protected void test() {
        //run test threads
        for (int i = 0; i < numberOfTests; i++) {
            testThread[i].start();
        }

        //waiting for test threads to complete
        int numberOfCompletedTests;
        do {
            numberOfCompletedTests = 0;
            for (int i = 0; i < numberOfTests; i++) {
                if (testThreadListener[i].isFinished()) {
                    numberOfCompletedTests++;
                }
            }
        } while (numberOfCompletedTests < numberOfTests);
    }

    @Override
    protected void verify() {
        EclipseLinkException exception = null;
        for (int i = 0; (i < numberOfTests) && (exception == null); i++) {
            exception = test[i].getTestResult().getException();
            if (exception != null) {
                for (Enumeration enumtr = allowedExceptions.elements();
                         enumtr.hasMoreElements() && (exception != null);) {
                    if (((Class)(enumtr.nextElement())).isInstance(exception)) {
                        exception = null;
                    }
                }
                if (exception != null) {
                    setTestException(exception);
                }
            }
        }
    }

    // the originalSession != null check is needed
    // in case reset is called more than once -
    // which may happen in case of test failure due to
    // the resent changes in AutoVerifyTestCase.
    @Override
    public void reset() {
        getSession().getIdentityMapAccessor().initializeAllIdentityMaps();
        if (originalSession != null) {
            resetSession();
        }
    }

    protected void resetSession() {
        resetOriginalSession(originalSession, getSession());
        getExecutor().setSession(originalSession);
        originalSession = null;
    }

    static public void resetOriginalSession(Session originalSession, Session newSession) {
        // Note that because ServerSession.isDatabaseSession() returns true
        // it is important to call isServerSession() before
        // isDatabaseSession() is called
        if (originalSession.isServerSession()) {
            // Assuming that originalSession == newSession
            // (see setNewSession(..))
            return;
        } else if (originalSession.isDatabaseSession()) {
            ((DatabaseSession)newSession).logout();

            DatabaseSession databaseSession = (DatabaseSession)originalSession;

            // Is this necessary? Don't know.
            // Just copied it from ClientServerTestModel
            databaseSession.logout();
            databaseSession.login();
        }
    }

    @Override
    public void logResult(Writer log) {
        super.logResult(log);

        for (int i = 0; i < numberOfTests; i++) {
            try {
                log.write(org.eclipse.persistence.internal.helper.Helper.cr() + Helper.getTabs(getNestedCounter() + 1) + "Test Thread " + i);
            } catch (IOException exception) {
            }
            test[i].logResult(log);
        }
    }

    public void useSequenceConnectionPool() {
        useSequenceConnectionPool = true;
    }
}
