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

import java.io.*;
import java.util.*;
import javax.persistence.*;

import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.eclipse.persistence.sessions.*;
import org.eclipse.persistence.sessions.server.*;

/**
 * <p>
 * <b>Purpose</b>: Allow for a test or set of tests to be executed and manage their execution and results.
 * <p>
 * <b>Responsibilities</b>:
 * <ul>
 * <li> Execute the test entity.
 * <li> Sets a flag to either log the results of test or not.
 * <li> Handles the exception returned by the test entity.
 * </ul>
 */
public class TestExecutor {

    /** It is a Stream where the test entity results are logged into */
    protected Writer log;
    /** This attribute is added to migrate tests to Ora*Tst */
    protected Writer regressionLog;

    /** Session used to run tests. */
    protected Session session;

    /** EntityManagerFactory used to run JPA tests. */
    protected EntityManagerFactory entityManagerFactory;

    /** Allows the original test session to be stored and repalced. */
    protected Session originalSession;

    /** A boolean, when set to true would start handling the exceptions thrown by TopLink
     * and would not when set to false. */
    protected boolean shouldHandleErrors;

    /** When set to true would log the results */
    protected boolean shouldLogResults;

    protected Hashtable loadedModels;

    /** Contains a collection of all the configured systems */
    protected Vector configuredSystems;

    /** This is used to stop execution thread */
    protected boolean shouldStopExecution;

    /** Used for test event progress notification */
    protected junit.framework.TestListener listener;

    /** Hold a default executor.  Used to cache the executor for tests run in JUnit. */
    protected static TestExecutor executor;

    /** Hold a default JUnit TestResult.  Used to cache the result for JUnit tests run by the executor. */
    protected static junit.framework.TestResult defaultJUnitTestResult;

    /** Hold JUnit TestResult but test.  Used to store the results for JUnit tests run by the executor. */
    protected static Map junitTestResults;

    /** This is used to get rid pf tests running on server(OC4J) */
    public boolean isServer = false;

    /** Allow only errors to be logged. */
    protected boolean shouldLogOnlyErrors = false;

    public static String CR = org.eclipse.persistence.internal.helper.Helper.cr();

    /**
     * Return a default executor.  Used as the executor for tests run in JUnit, or by themselves.
     */
    public static TestExecutor getDefaultExecutor() {
        if (executor == null) {
            TestExecutor testExecutor = new TestExecutor();
            testExecutor.setSession((new TestSystem()).login());
            // Ensure connect successful before setting executor.
            executor = testExecutor;
        }
        return executor;
    }

    /**
     * Return if only errors should be logged.
     */
    public boolean shouldLogOnlyErrors() {
        return shouldLogOnlyErrors;
    }

    /**
     * Set if only errors should be logged.
     */
    public void setShouldLogOnlyErrors(boolean shouldLogOnlyErrors) {
        this.shouldLogOnlyErrors = shouldLogOnlyErrors;
    }

    /**
     * Set the default executor.  Used as the executor for tests run in JUnit, or by themselves.
     */
    public static void setDefaultExecutor(TestExecutor theExecutor) {
        executor = theExecutor;
    }

    /**
     * Set the default JUnit TestResult.  Used to cache the result for JUnit tests run by the executor.
     */
    public static void setDefaultJUnitTestResult(junit.framework.TestResult theTestResult) {
        defaultJUnitTestResult = theTestResult;
    }

    /**
     * Return the default JUnit TestResult.  Used to cache the result for JUnit tests run by the executor.
     */
    public static junit.framework.TestResult getDefaultJUnitTestResult() {
        return defaultJUnitTestResult;
    }

    /**
     * Set the JUnit TestResults.  Used to store the result for JUnit tests run by the executor.
     */
    public static void setJUnitTestResults(Map results) {
        junitTestResults = results;
    }

    /**
     * Return the JUnit TestResults.  Used to store the result for JUnit tests run by the executor.
     */
    public static Map getJUnitTestResults() {
        if (junitTestResults == null) {
            junitTestResults = new HashMap();
        }
        return junitTestResults;
    }

    public TestExecutor() {
        this.log = new OutputStreamWriter(System.out);
        this.shouldLogResults = true;
        this.shouldHandleErrors = false;
        this.shouldStopExecution = false;
        this.configuredSystems = new Vector();
    }

    /**
     * INTERNAL:
     * Add persistent system to the executor configuration.
     */
    public void addConfigureSystem(TestSystem system) {
        if (!configuredSystemsContainsInstanceOf(system)) {
            getConfiguredSystems().addElement(system);
        }
    }

    /**
     * The loaded models hold all model in use to allow test case
     * The access other model to reuse their to setup.
     */
    public void addLoadedModels(Vector models) {
        for (Enumeration theModels = models.elements(); theModels.hasMoreElements();) {
            TestModel model = (TestModel)theModels.nextElement();
            getLoadedModels().put(model.getName(), model);
        }
    }

    /**
     * If the system is not already configured then configure it and store it in the executor.
     */
    public void configureSystem(TestSystem system) throws Exception {
        if (!configuredSystemsContainsInstanceOf(system)) {
            system.run(getSession());
            getSession().getIdentityMapAccessor().initializeAllIdentityMaps();
            getConfiguredSystems().addElement(system);
        }
    }

    /**
     * Return true if the configuredSystems contains an instance of the class of the TestSystem parameter.
     */
    public boolean configuredSystemsContainsInstanceOf(TestSystem system) {
        for (Enumeration configuredSystemsEnum = getConfiguredSystems().elements();
                 configuredSystemsEnum.hasMoreElements();) {
            if (configuredSystemsEnum.nextElement().getClass().equals(system.getClass())) {
                return true;
            }
        }
        return false;
    }

    /**
     * PUBLIC:
     * The executor stops handling errors and throws them to the user. This is mainly
     * for testing purpose to actully know the place of problem.
     */
    public void doNotHandleErrors() {
        setShouldHandleErrors(false);
    }

    /**
     * PUBLIC:
     * The executor would stop logging error to the log stream.
     */
    public void doNotLogResults() {
        setShouldLogResults(false);
    }

    public void doNotStopExecution() {
        setShouldStopExecution(false);
    }

    /**
     * PUBLIC:
     * To execute any test entity.
     */
    public void execute(junit.framework.Test test) throws Throwable {
        if (shouldStopExecution()) {
            return;
        }

        try {
            getSession().logMessage("Begin " + test);
            if (getListener() != null) {
                getListener().startTest(test);
            }

            // If the suite was run through JUnit, or is a Junit suite,
            // run through the result, otherwise through the executor.
            if ((getDefaultJUnitTestResult() != null) || (!(test instanceof TestEntity))) {
                junit.framework.TestResult result = getDefaultJUnitTestResult();
                if (getDefaultJUnitTestResult() == null) {
                    result = new junit.framework.TestResult();
                    result.addListener(getListener());
                }
                getJUnitTestResults().put(test, result);
                test.run(result);
            } else {
                ((TestEntity)test).execute(this);
            }
            if (getListener() != null) {
                getListener().endTest(test);
            }
            getSession().logMessage("Finished " + test);
            if (getAbstractSession().isInTransaction()) {
                throw new TestProblemException(test + " is a faulty test, transaction was left open and must always be closed.");
            }
        } catch (Throwable exception) {
            // Always catch warnings, and handle errors if shouldHandleErrors set.
            if ((!(exception instanceof TestWarningException)) && (!shouldHandleErrors())) {
                throw exception;
            }
            if (getListener() != null) {
                getListener().endTest(test);
            }
            getSession().logMessage("Failed " + test);
            System.err.println(test + " FAILED");
        }
    }

    /**
     * Forced system to be configured even if it is already configured.
     */
    public void forceConfigureSystem(TestSystem system) throws Exception {
        removeConfigureSystem(system);
        system.run(getSession());
        addConfigureSystem(system);
    }

    /**
     * Return all the configured systems.
     */
    public Vector getConfiguredSystems() {
        return configuredSystems;
    }

    /**
     * Used for test event progress notifiaction.
     */
    public junit.framework.TestListener getListener() {
        return listener;
    }

    /**
     * Return the model by name.
     * If missing null is returned.
     */
    public TestModel getLoadedModel(String modelsName) {
        return (TestModel)getLoadedModels().get(modelsName);
    }

    /**
     * The loaded models hold all model in use to allow test case
     * The access other model to reuse their to setup.
     */
    public Hashtable getLoadedModels() {
        return loadedModels;
    }

    /**
     * Return the log stream to print the results on. Default is console.
     * This method is added to migrate to Ora*Tst
     */
    public Writer getRegressionLog() {
        return regressionLog;
    }

    /**
     * Return the log stream to print the results on. Default is console.
     */
    public Writer getLog() {
        if (getSession() == null) {
            return log;
        }
        return getSession().getLog();
    }

    /**
     * Create a new entity manager from the entity manager factory.
     * This entity manager is initialized from META-INF/persistence.xml.
     */
    public EntityManager createEntityManager() {
        return getEntityManagerFactory().createEntityManager();
    }

    /**
     * Return the executor entity manager factory.
     * This lazy initializes from the "performance" persistent unit using the default provider,
     * and configures the TopLink properties to connect to the executor's session's login.
     */
    public EntityManagerFactory getEntityManagerFactory() {
        if (entityManagerFactory == null) {
            Map properties = getEntityManagerProperties();
            entityManagerFactory = Persistence.createEntityManagerFactory("performance", properties);
        }
        return entityManagerFactory;
    }

    /**
     * Return the executor entity manager factory.
     * This lazy initializes from the "performance" persistent unit using the default provider,
     * and configures the TopLink properties to connect to the executor's session's login.
     */
    public Map getEntityManagerProperties() {
        Map properties = new HashMap();
        properties.put(PersistenceUnitProperties.JDBC_DRIVER, getSession().getLogin().getDriverClassName());
        properties.put(PersistenceUnitProperties.JDBC_URL, getSession().getLogin().getConnectionString());
        properties.put(PersistenceUnitProperties.JDBC_USER, getSession().getLogin().getUserName());
        properties.put(PersistenceUnitProperties.JDBC_PASSWORD, getSession().getLogin().getPassword());
        properties.put(PersistenceUnitProperties.LOGGING_LEVEL, getSession().getSessionLog().getLevelString());
        return properties;
    }

    /**
     * Set the executor entity manager factory.
     */
    public void setEntityManagerFactory(EntityManagerFactory entityManagerFactory) {
        this.entityManagerFactory = entityManagerFactory;
    }

    /**
     * Return the session cast to DatabaseSession.
     */
    public org.eclipse.persistence.sessions.DatabaseSession getDatabaseSession() {
        return (org.eclipse.persistence.sessions.DatabaseSession)getSession();
    }

    /**
     * Return the session cast to AbstractSession.
     */
    public org.eclipse.persistence.internal.sessions.AbstractSession getAbstractSession() {
        return (org.eclipse.persistence.internal.sessions.AbstractSession)getSession();
    }

    /**
     * Return the session.
     */
    public Session getSession() {
        return session;
    }

    /**
     * Return the original session.
     */
    public Session getOriginalSession() {
        return originalSession;
    }

    /**
     * Set the original session.
     */
    public void setOriginalSession(Session originalSession) {
        this.originalSession = originalSession;
    }

    /**
     * Swap the current session with the new session.
     * This allows a test or model to customize the session it uses.
     */
    public void swapSession(Session newSession) {
        setOriginalSession(getSession());
        setSession(newSession);
    }

    /**
     * Swap the current session with a new database session with the same login,
     * but no descriptors.
     */
    public void swapCleanDatabaseSession() {
        DatabaseSession session = new Project(getSession().getLogin()).createDatabaseSession();
        session.setSessionLog(getSession().getSessionLog());
        session.login();
        swapSession(session);
    }

    /**
     * Swap the current session with the new server session.
     */
    public void swapServerSession() {
        Server session = getSession().getProject().createServerSession();
        session.setSessionLog(getSession().getSessionLog());
        session.login();
        swapSession(session);
    }

    /**
     * Swap the current session with the new session.
     * This allows a test or model to customize the session it uses.
     */
    public void resetSession() {
        if(getOriginalSession() != null) {
            if(getDatabaseSession().isConnected()) {
                getDatabaseSession().logout();
            }
            setSession(getOriginalSession());
            setOriginalSession(null);
        }
    }

    /**
     * PUBLIC:
     * Starts handling errors. Even if some error is raised Executor just
     * catches it and goes on to execute the next test.
     */
    public void handleErrors() {
        setShouldHandleErrors(true);
    }

    public void initializeConfiguredSystems() {
        setConfiguredSystems(new Vector());
    }

    /**
     * This logs out from session.
     */
    protected void logout() {
        if (session != null) {
            ((DatabaseSession)session).logout();
        }
    }

    /**
     * Logs the result for the given test entity if logResults is true.
     * This method is added to migrate tests to Ora*Tst
     */
    public void logRegressionResultForTestEntity(junit.framework.Test test) {
        logResultForTestEntity(test, true);
    }

    /**
     * Logs the result for the given test entity if logResults is true.
     */
    public void logResultForTestEntity(junit.framework.Test test) {
        logResultForTestEntity(test, false);
    }

    /**
     * Logs the result for the given test entity if logResults is true.
     */
    public void logResultForTestEntity(junit.framework.Test test, boolean regression) {
        Writer log = getLog();
        if (regression) {
            log = getRegressionLog();
        }
        if (shouldStopExecution()) {
            try {
                log.write("!!! THE TEST EXECUTION WAS INTERRUPTED !!!");
                log.flush();
            } catch (IOException e) {
            }
        }

        if (shouldLogResults()) {
            if (test instanceof TestEntity) {
                TestEntity testEntity = (TestEntity)test;
                testEntity.resetNestedCounter();
                if (regression) {
                    testEntity.logRegressionResult(log);
                } else {
                    testEntity.logResult(log, shouldLogOnlyErrors());
                }
                testEntity.resetNestedCounter();
            } else {
                logJUnitResult(test, log, "");
            }
        }
        try {
            log.flush();
        } catch (IOException e) {
        }
    }

    /**
     * Log any JUnit results if present.
     */
    public static void logJUnitResult(junit.framework.Test test, Writer log, String indent) {
        try {
            log.write(CR);
            log.write(CR);
            log.write(indent + "TEST MODEL NAME: (JUnit test): " + test);
            log.write(CR);
            junit.framework.TestResult result = (junit.framework.TestResult) TestExecutor.getJUnitTestResults().get(test);
            if (result == null) {
                log.write(indent + "## SETUP FAILURE ## (no tests run)");
                log.write(CR);
                log.flush();
                return;
            }
            if ((result.failureCount() > 0) || (result.errorCount() > 0)) {
                log.write(indent + "###ERRORS###" + CR);
            }
            log.write(CR);
            log.write(indent + "Errors: (failures): " + result.failureCount());
            log.write(CR);
            log.write(indent + "Fatal Errors: (errors): " + result.errorCount());
            log.write(CR);
            log.write(indent + "Passed: " + (result.runCount() - result.errorCount() - result.failureCount()));
            log.write(CR);
            log.write(indent + "Total Tests: " + result.runCount());
            log.write(CR);
            if (result.failureCount() > 0) {
                log.write(CR);
                log.write(indent + "Failures:");
                log.write(CR);
                for (Enumeration failures = result.failures(); failures.hasMoreElements();) {
                    junit.framework.TestFailure failure = (junit.framework.TestFailure)failures.nextElement();
                    String testString = failure.failedTest().toString();
                    int startIndex = testString.indexOf("(");
                    if (startIndex != -1) {
                        log.write(indent + "TEST SUITE NAME: " + testString.substring(startIndex + 1, testString.length() - 1));
                        log.write(CR);
                    }
                    log.write(indent + "TEST NAME: " + testString);
                    log.write(CR);
                    log.write(indent + "##FAILURE##" + CR);
                    log.write(indent + "RESULT:      Error (failure)");
                    log.write(CR);
                    log.write(indent + failure.trace());
                    log.write(CR);
                }
            }
            if (result.errorCount() > 0) {
                log.write(CR);
                log.write(indent + "Errors:");
                log.write(CR);
                for (Enumeration errors = result.errors(); errors.hasMoreElements();) {
                    junit.framework.TestFailure error = (junit.framework.TestFailure)errors.nextElement();
                    String testString = error.failedTest().toString();
                    int startIndex = testString.indexOf("(");
                    if (startIndex != -1) {
                        log.write(indent + "TEST SUITE NAME: " + testString.substring(startIndex + 1, testString.length() - 1));
                        log.write(CR);
                    }
                    log.write(indent + "TEST NAME: " + testString);
                    log.write(CR);
                    log.write(indent + "##FAILURE##" + CR);
                    log.write(indent + "RESULT:      FatalError (error)");
                    log.write(CR);
                    log.write(indent + error.trace());
                    log.write(CR);
                }
            }
            log.write(CR);
            log.flush();
        } catch (IOException exception) {
        }
    }

    /**
     * Log the results to the log stream.
     */
    public void logResults() {
        setShouldLogResults(true);
    }

    /**
     * Public:
     * This method is used if we use dos promt to run our test cases.
     */
    public static void main(String[] arguments) {
        try {
            TestExecutor executor = new TestExecutor();

            //        executor.handleErrors();
            //        executor.doNotLogResults();
            executor.execute((TestEntity)Class.forName(arguments[0]).newInstance());
        } catch (Throwable exception) {
            System.out.println(exception.toString());
        }
    }

    public void removeConfigureSystem(TestSystem system) {
        removeFromConfiguredSystemsInstanceOf(system);
    }

    /**
     * If an instance of the same class as the parameter exists in the Vector of configuredSystems
     * then remove it.
     */
    public void removeFromConfiguredSystemsInstanceOf(TestSystem system) {
        // find and record the systems to remove
        Vector systemsToRemove = new Vector();
        for (Enumeration systemEnum = getConfiguredSystems().elements();
                 systemEnum.hasMoreElements();) {
            TestSystem aSystem = (TestSystem)systemEnum.nextElement();
            if (aSystem.getClass().equals(system.getClass())) {
                systemsToRemove.addElement(aSystem);
            }
        }

        // Do the removing
        for (Enumeration systemsToRemoveEnum = systemsToRemove.elements();
                 systemsToRemoveEnum.hasMoreElements();) {
            getConfiguredSystems().removeElement(systemsToRemoveEnum.nextElement());
        }
    }

    /**
     * The loaded models hold all model in use to allow test case
     * to access other model to reuse their to setup.
     */
    public void resetLoadedModels() {
        setLoadedModels(new Hashtable());
    }

    /**
     * PUBLIC:
     * This method executes the test entity. This method sets the session by using test
     * entity default login and once the execution is over it explicitily logs out.
     */
    public void runTest(junit.framework.Test test) throws Throwable {
        boolean hasSession = true;

        setShouldStopExecution(false);
        if ((getSession() == null) && (test instanceof TestEntity)) {
            TestEntity testEntity = (TestEntity)test;
            hasSession = false;
            if (shouldHandleErrors()) {
                try {
                    setSession(testEntity.defaultLogin());
                } catch (Exception exception) {
                    logout();
                    return;
                }
            } else {
                setSession(testEntity.defaultLogin());
            }
        }

        try {
            execute(test);
            //This line is added to migrate tests to Ora*Tst
            if (getRegressionLog() != null) {
                logRegressionResultForTestEntity(test);
            }
            logResultForTestEntity(test);
        } finally {
            if (!hasSession) {
                logout();
            }
        }
    }

    /**
     * Set configured systems.
     */
    public void setConfiguredSystems(Vector configuredSystems) {
        this.configuredSystems = configuredSystems;
    }

    /**
     * Used for test event progress notification.
     */
    public void setListener(junit.framework.TestListener listener) {
        this.listener = listener;
    }

    /**
     * The loaded models hold all model in use to allow test case
     * The access other model to reuse their to setup.
     */
    protected void setLoadedModels(Hashtable loadedModels) {
        this.loadedModels = loadedModels;
    }

    /**
     * PUBLIC:
     * Set the log stream to which test results should be logged on.
     * This method is added to migrate to Ora*Tst
     */
    public void setRegressionLog(Writer writer) {
        this.regressionLog = writer;
    }

    /**
     * PUBLIC:
     * Set the log stream to which test results should be logged on.
     */
    public void setLog(Writer writer) {
        this.log = writer;
    }

    /**
     * PUBLIC:
     * Set the session
     */
    public void setSession(Session theSession) {
        // Don't allow bad tests to set session to null.
        if (theSession == null) {
            return;
        }
        session = theSession;
    }

    public void setShouldHandleErrors(boolean aBoolean) {
        shouldHandleErrors = aBoolean;
    }

    public void setShouldLogResults(boolean aBoolean) {
        shouldLogResults = aBoolean;
    }

    public void setShouldStopExecution(boolean aBoolean) {
        shouldStopExecution = aBoolean;
    }

    /**
     * Returns if errors are to be handled or not.
     */
    public boolean shouldHandleErrors() {
        return shouldHandleErrors;
    }

    /**
     * Returns if results should be logged or not.
     */
    public boolean shouldLogResults() {
        return shouldLogResults;
    }

    /**
     * Returns if test entities should be execute or not.
     */
    public boolean shouldStopExecution() {
        return shouldStopExecution;
    }

    public void stopExecution() {
        setShouldStopExecution(true);
    }
}
