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

import org.eclipse.persistence.internal.helper.ConversionManager;
import org.eclipse.persistence.sessions.DatabaseSession;
import org.eclipse.persistence.sessions.Project;
import org.eclipse.persistence.sessions.Session;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.sessions.DatabaseLogin;
import org.eclipse.persistence.sessions.Login;
import org.eclipse.persistence.testing.framework.TestExecutor;
import org.eclipse.persistence.sessions.factories.SessionManager;

/**
 * <p><b>Purpose</b>: Test model is a collection of test suites and/or sub test models. When a
 * test model is executed all the test suites and models registered with it are
 * executed one by one.
 */
public class TestModel extends TestCollection {

    /** Configurations that must be set before this model is run */
    private Vector requiredSystems;

    /** The model will force these Configurations to be set even if they are set before */
    private Vector forcedRequiredSystems;

    /** Used to store tests added when the model is built. */
    private Vector originalTests;

    /** Keep track of setup state. */
    private boolean isSetup;

    /** Ensure the login and log are not corrupted if the session is changed. */
    private Login login;
    private SessionLog sessionLog;

    /**
     * Flag used to determine if tables should be recreated/populated before each test model.
     * true means tests will take longer to run, but run more consistently as bad tests will not effect other models.
     * Uses system property "org.eclipse.persistence.testing.reset-system".
     */
    private static Boolean shouldResetSystemAfterEachTestModel;

    /**
     * Flag used to determine if tables should be recreated/populated before each test model.
     * true means tests will take longer to run, but run more consistently as bad tests will not effect other models.
     * Uses system property "org.eclipse.persistence.testing.reset-system".
     */
    public static boolean shouldResetSystemAfterEachTestModel() {
        if (shouldResetSystemAfterEachTestModel == null) {
            String systemProperty = System.getProperty("org.eclipse.persistence.testing.reset-system");
            if (systemProperty == null) {
                shouldResetSystemAfterEachTestModel = Boolean.TRUE;
            } else {
                if (systemProperty.equals("false")) {
                    shouldResetSystemAfterEachTestModel = Boolean.FALSE;
                } else {
                    shouldResetSystemAfterEachTestModel = Boolean.TRUE;
                }
            }
        }
        return shouldResetSystemAfterEachTestModel.booleanValue();
    }

    /**
     * Flag used to determine if tables should be recreated/populated before each test model.
     * true means tests will take longer to run, but run more consistently as bad tests will not effect other models.
     * Uses system property "org.eclipse.persistence.testing.reset-system".
     */
    public static void setShouldResetSystemAfterEachTestModel(boolean value) {
        if (value) {
            shouldResetSystemAfterEachTestModel = Boolean.TRUE;
        } else {
            shouldResetSystemAfterEachTestModel = Boolean.FALSE;
        }
    }

    public TestModel() {
        this.requiredSystems = new Vector();
        this.forcedRequiredSystems = new Vector();
        this.isSetup = false;
        this.originalTests = new Vector();
    }

    /**
     * The system add will be forced to be initialized again even if it is already configured.
     * Basically this means that required system will recreate new database.
     */
    public final void addForcedRequiredSystem(TestSystem requiredSystem) {
        getForcedRequiredSystems().addElement(requiredSystem);
    }

    /**
     * The system add will be forced to be initialized again even if it is already configured.
     * Basically this means that required system will recreate new database. This method should always
     * be overwritten in the subclasses if the model needs some persistence system to be already configured
     * before it is run.
     */
    public void addForcedRequiredSystems() {
    }

    /**
     * The system add will be configured if it has not been already configured.
     * Basically this means that required system will recreate new database.
     */
    public final void addRequiredSystem(TestSystem requiredSystem) {
        getRequiredSystems().addElement(requiredSystem);
    }

    /**
     * The system add will be configured if it has not been already configured.
     * Basically this means that required system will recreate new database.
     * This method should always be overwritten in the subclasses if the model
     * needs some persistence system to be already configured before it is run.
     */
    public void addRequiredSystems() {
    }

    /**
     * The subclasses must overwrite this method. To add tests to the model.
     * It could be collection of test suites or test models themselves.
     */
    public void addTests() {
    }

    /**
     * The subclasses must overwrite this method. To add tests to the model.
     * It could be collection of test suites or test models themselves.
     */
    public void addSRGTests() {
    }

    /**
     * Build the required systems, but ensure that the variable is not modified.
     */
    public Vector buildForcedRequiredSystems() {
        Vector constructedSystems = (Vector)getForcedRequiredSystems().clone();
        addForcedRequiredSystems();
        Vector allSystems = getForcedRequiredSystems();
        setForcedRequiredSystems(constructedSystems);
        return allSystems;
    }

    /**
     * Build the required systems, but ensure that the variable is not modified.
     */
    public Vector buildRequiredSystems() {
        Vector constructedSystems = (Vector)getRequiredSystems().clone();
        addRequiredSystems();
        Vector allSystems = getRequiredSystems();
        setRequiredSystems(constructedSystems);
        return allSystems;
    }

    /**
     * Goes through each systems and configures them.
     */
    private void configure() throws Exception {
        Vector systems = buildRequiredSystems();

        for (Enumeration enumtr = systems.elements(); enumtr.hasMoreElements();) {
            TestSystem system = (TestSystem)enumtr.nextElement();

            // To improve test consistency always force systems to be reset.
            if (shouldResetSystemAfterEachTestModel()) {
                getExecutor().forceConfigureSystem(system);
            } else {
                getExecutor().configureSystem(system);
            }
        }

        systems = buildForcedRequiredSystems();

        for (Enumeration enumtr = systems.elements(); enumtr.hasMoreElements();) {
            TestSystem system = (TestSystem)enumtr.nextElement();
            getExecutor().forceConfigureSystem(system);
        }
    }

    /**
     * Executes all the test entities in the collection.
     */
    public void execute(TestExecutor executor) throws Throwable {
        setSummary(new TestResultsSummary(this));
        setExecutor(executor);
        long startTime = System.nanoTime();
        try {
            setupEntity();
            setFinishedTests(new Vector());
            try {
                for (Enumeration tests = getTests().elements(); tests.hasMoreElements();) {
                    junit.framework.Test test = (junit.framework.Test)tests.nextElement();
                    if ((TestExecutor.getDefaultJUnitTestResult() != null) && TestExecutor.getDefaultJUnitTestResult().shouldStop()) {
                            break;
                    }
                    executor.execute(test);
                    getFinishedTests().addElement(test);
                }
            } catch (Throwable exception) {
                try {
                    resetEntity();
                } catch (Throwable ignore) {
                }
                throw exception;
            }
            resetEntity();
        } finally {
            long endTime = System.nanoTime();
            getSummary().setTotalTime(endTime - startTime);
        }
    }

    /**
     * Return all the required systems that need to be configured even if they are already configured.
     */
    public Vector getForcedRequiredSystems() {
        return forcedRequiredSystems;
    }

    /**
     * Return test that existed before setup.
     */
    protected Vector getOriginalTests() {
        return originalTests;
    }

    /**
     * Return all the required systems that need to be configured if they are not already configured.
     */
    public Vector getRequiredSystems() {
        return requiredSystems;
    }

    public boolean isSetup() {
        return isSetup;
    }

    /**
     * Format the test output on the print stream.
     */
    protected void logFootNote(Writer log) {
        try {
            log.write(org.eclipse.persistence.internal.helper.Helper.cr() + getIndentationString() + "RESULTS OF TEST MODEL: " + getName() + org.eclipse.persistence.internal.helper.Helper.cr());
        } catch (IOException exception) {
        }
    }

    /**
     * Format the test output on the print stream.
     * This method is added to migrate tests to Ora*Tst
     */
    protected void logRegressionHeadNote(Writer log) {
        try {
            log.write(org.eclipse.persistence.internal.helper.Helper.cr() + getIndentationString() + "TEST MODEL NAME: " + getName() + org.eclipse.persistence.internal.helper.Helper.cr());
            log.write(getIndentationString() + "MODEL DESCRIPTION: " + getDescription() + org.eclipse.persistence.internal.helper.Helper.cr());
        } catch (IOException exception) {
        }
    }

    /**
     * Format the test output on the print stream.
     */
    protected void logHeadNote(Writer log) {
        try {
            log.write(org.eclipse.persistence.internal.helper.Helper.cr() + getIndentationString() + "VERSION: " + org.eclipse.persistence.sessions.DatabaseLogin.getVersion());
            log.write(org.eclipse.persistence.internal.helper.Helper.cr() + getIndentationString() + "TEST MODEL NAME: " + getName() + org.eclipse.persistence.internal.helper.Helper.cr());
            log.write(getIndentationString() + "MODEL DESCRIPTION: " + getDescription() + org.eclipse.persistence.internal.helper.Helper.cr());
        } catch (IOException exception) {
        }
    }

    /**
     * This is a optional method and it should be overridden if their is something
     * that test collection should perform after running itself.
     */
    public void reset() {
        return;
    }

    /**
     * If setupEntity has been called then this must be called to reset the model again.
     */
    public void resetEntity() {
        if (isSetup()) {
            setTests(getOriginalTests());
            setIsSetup(false);
        }
        if(this.getSummary().didSetupWarn()) {
            return;
        }
        reset();
        // To improve test consistency cleanup the session and executor better.
        getSession().getIdentityMapAccessor().initializeIdentityMaps();
        if (shouldResetSystemAfterEachTestModel()) {
            getExecutor().setConfiguredSystems(new Vector());

            // Logout and clean/reset the session in case test model failed ungracefully.
            if (getSession().isDatabaseSession()) {
                try {
                    getDatabaseSession().logout();
                } catch (Exception ignore) {
                }
            }
            if (this.login == null) {
                this.login = getSession().getDatasourceLogin();
                this.sessionLog = getSession().getSessionLog();
            }
            // Check if login or log were corrupted.
            if (this.login.getClass() != getSession().getDatasourceLogin().getClass()) {
                System.out.println("Login changed by test model:" + this);
            }
            if (this.sessionLog.getLevel() != getSession().getSessionLog().getLevel()) {
                System.out.println("Log level changed by test model:" + this);
            }
            if (this.login instanceof DatabaseLogin) {
                DatabaseLogin login = (DatabaseLogin)this.login;
                if (login.shouldBindAllParameters() != getSession().getLogin().shouldBindAllParameters()) {
                    System.out.println("Binding changed by test model:" + this);
                }
                if (login.shouldCacheAllStatements() != getSession().getLogin().shouldCacheAllStatements()) {
                    System.out.println("Statement caching changed by test model:" + this);
                }
                if (login.shouldUseBatchWriting() != getSession().getLogin().shouldUseBatchWriting()) {
                    System.out.println("Batch writing changed by test model:" + this);
                }
                if (login.shouldUseJDBCBatchWriting() != getSession().getLogin().shouldUseJDBCBatchWriting()) {
                    System.out.println("JDBC batch writing changed by test model:" + this);
                }
                if (login.shouldUseNativeSQL() != getSession().getLogin().shouldUseNativeSQL()) {
                    System.out.println("Native SQL changed by test model:" + this);
                }
                if (login.getTableQualifier() != getSession().getLogin().getTableQualifier()) {
                    System.out.println("Table qualifier changed by test model:" + this);
                }
            }
            DatabaseSession session = new Project(this.login).createDatabaseSession();
            session.setSessionLog(this.sessionLog);
            getExecutor().setSession(session);
            // Check if default conversion manager was corrupted.
            if (!ConversionManager.getDefaultManager().shouldUseClassLoaderFromCurrentThread()) {
                System.out.println("ConversionManager corrupted by test model:" + this);
            }
            ConversionManager.setDefaultManager(null);
            getSession().getDatasourceLogin().getDatasourcePlatform().setConversionManager(null);
            SessionManager.getManager().setSessions(new ConcurrentHashMap<String, Session>());
            getDatabaseSession().login();
        }
        setIsSetup(false);
    }

    /**
     * Set all the required systems that need to be configured even if they are already configured.
     */
    public void setForcedRequiredSystems(Vector systems) {
        this.forcedRequiredSystems = systems;
    }

    protected void setIsSetup(boolean isSetup) {
        this.isSetup = isSetup;
    }

    /**
     * Set the test that existed before setup.
     */
    protected void setOriginalTests(Vector originalTests) {
        this.originalTests = originalTests;
    }

    /**
     * Set all the required sytems that need to be configured if they are not already configured.
     */
    public void setRequiredSystems(Vector systems) {
        this.requiredSystems = systems;
    }

    /**
     * This is a optional method and it should be overridden if their is something
     * that test collection should perform before running itself.
     */
    public void setup() {
        return;
    }

    /**
     * To set up the model also look at resetEntity
     */
    public void setupEntity() throws Throwable {
        if (isSetup()) {
            return;
        }
        if (getSession() != null) {
            // Force field names to uppercase for postgres.
            if (getSession().getDatasourcePlatform().isPostgreSQL()) {
                getSession().getPlatform().setShouldForceFieldNamesToUpperCase(true);
            }
            this.login = getSession().getDatasourceLogin().clone();
            this.sessionLog = (SessionLog)getSession().getSessionLog().clone();
        }
        try {
            setOriginalTests((Vector)getTests().clone());
            configure();
            setup();
            if (isSRG) {
                addSRGTests();
            } else {
                addTests();
            }

            // Check for faulty models leaving transaction open.
            if ((getAbstractSession() != null) && getAbstractSession().isInTransaction()) {
                try {
                    int count = 0;
                    while (getAbstractSession().isInTransaction() && (count < 10)) {
                        getAbstractSession().rollbackTransaction();
                        count++;
                    }
                } catch (Throwable ignore) {
                }
                TestProblemException exception = new TestProblemException(this + " is a faulty test, transaction was left open and must always be closed.");
                throw exception;
            }
        } catch (Throwable exception) {
            getSummary().setSetupException(exception);

            try {
                resetEntity();
            } catch (Throwable resetException) {
            }

            throw exception;
        }

        setIsSetup(true);
    }

    /**
     * Returns the number of tests in this suite.
     * If not setup, return the finished tests.
     */
    public int testCount() {
        if (isSetup() || (!getTests().isEmpty())) {
            return super.testCount();
        }
        return getFinishedTests().size();
    }

    /**
     * Returns the tests as an enumeration.
     * If not setup, return the finished tests.
     */
    public Enumeration tests() {
        if (isSetup() || (!getTests().isEmpty())) {
            return super.tests();
        }
        return getFinishedTests().elements();
    }

    /**
     * Returns the test at the given index.
     * If not setup, return the finished tests.
     */
    public junit.framework.Test testAt(int index) {
        if (isSetup() || (!getTests().isEmpty())) {
            return super.testAt(index);
        }
        return (junit.framework.Test)getFinishedTests().elementAt(index);
    }
}
