/*******************************************************************************
 * Copyright (c) 1998, 2015 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 v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
 ******************************************************************************/
package org.eclipse.persistence.testing.framework;

import java.io.*;
import java.lang.ref.WeakReference;

import javax.persistence.*;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.internal.databaseaccess.DatabasePlatform;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.sessions.*;
import org.eclipse.persistence.sessions.server.Server;
import org.eclipse.persistence.sessions.server.ServerSession;

/**
 * <p>Purpose<b></b>:
 * All the test cases are subclassed from this class.
 * Each test case tests single feature of TopLink. Ideally a test case consists of five steps.
 * Setup: Performs all the initial setup that is required by the test,
 * such as setting up database to some state on which test would run.
 * Test: The actual test to be performed, such as writing an object.
 * Verify: Verify the test if it was performed well or not.
 * Reset: Reset the database to the state from where the test started
 * Reset Verify: Check if reset performed well or not.
 */
public abstract class TestCase extends junit.framework.TestCase implements TestEntity {

    /** Store the name to allow serialization, for some reason JUnit name does not serialize. */
    private String name;

    /** The result of the test. */
    private TestResult testResult;

    /** The executor used to execute the test. */
    private transient TestExecutor executor;

    /** To provide small description of the test case */
    private String description;

    /** The test collection that contains this test */
    private TestEntity container;

    /** This is used only for printing test results with proper indentation */
    private int nestedCounter;

    /** The indentation string that is added to each line of result for printing. */
    private String indentationString;

    public TestCase() {
        description = "";
        nestedCounter = INITIAL_VALUE;
        testResult = new TestResult(this);
        setName(getClass().getName().substring(getClass().getName().lastIndexOf('.') + 1));
    }

    /**
     * Reset the JUnit name in case serialization looses it.
     */
    public String getName() {
        if (super.getName() == null) {
            setName(this.name);
        }
        return super.getName();
    }

    /**
     * Store the test name locally to ensure it can serialize.
     */
    public void setName(String name) {
       super.setName(name);
       this.name = name;
    }

    /**
     * Append test case result to the test results summary.
     */
    public void appendTestResult(TestResultsSummary summary) {
        summary.appendTestCaseResult(this);
    }

    /**
     * Computes the level for indentation.
     */
    public void computeNestedLevel() {
        TestEntity testContainer = getContainer();

        if ((testContainer != null) && (testContainer.getNestedCounter() != INITIAL_VALUE)) {
            setNestedCounter(testContainer.getNestedCounter() + 1);
        } else {
            incrementNestedCounter();
        }
    }

    /**
     * Return if the two objects match.
     */
    public boolean compareObjects(Object source, Object target) {
        return getAbstractSession().compareObjects(source, target);
    }

    /**
     * Check the database and ensure the object and its parts have been fully deleted.
     */
    public boolean verifyDelete(Object object) {
        return getAbstractSession().verifyDelete(object);
    }

    /**
     * The session is initialized to the default login from the Persistent System
     * if no explicit login is done for testing. This method must be overridden in
     * the subclasses if different login is required.
     */
    public Session defaultLogin() {
        return (new TestSystem()).login();
    }

    /**
     * Executes this test case.
     * Note:
     * Only RuntimeExceptions are caught because all EclipseLink Exceptions are derived from
     * RuntimeException. This takes care of other java runtime exceptions also.
     */
    public void execute(TestExecutor executor) {
        boolean executeFailed = false;
        setTestResult(new TestResult(this, "Passed"));
        setExecutor(executor);

        long startTime = System.currentTimeMillis();
        try {
            try {
                setUp();
            } catch (EclipseLinkException exception) {
                executeFailed = true;
                setTestException(exception);
                throw exception;
            } catch (Throwable exception) {
                executeFailed = true;
                TestProblemException problem = new TestProblemException("Problem in the setup method of the test: " + getName());
                problem.setInternalException(exception);
                setTestException(problem);
                throw problem;
            }

            try {
                executeTest();
                verify();
            } catch (EclipseLinkException exception) {
                executeFailed = true;
                if (getTestException() == null) {
                    setTestException(exception);
                    throw exception;
                }
            } catch (Throwable runtimeException) {
                executeFailed = true;
                TestErrorException topLinkException = new TestErrorException("Fatal error occurred.", runtimeException);
                if (getTestException() == null) {
                    setTestException(topLinkException);
                    throw topLinkException;
                }
            }
        } finally {
            try {
                tearDown();
            } catch (EclipseLinkException exception) {
                executeFailed = true;
                if (getTestException() == null) {
                    setTestException(exception);
                    throw exception;
                }
            } catch (Throwable exception) {
                executeFailed = true;
                TestProblemException problem = new TestProblemException("Problem in the reset method of the test");
                problem.setInternalException(exception);
                if (getTestException() == null) {
                    setTestException(problem);
                    throw problem;
                }
            } finally {
                long endTime = System.currentTimeMillis();
                getTestResult().setTotalTime(endTime - startTime);

                // If a failure occurred allow recreation of the database and initialize the identity maps.
                if (executeFailed) {
                    // If this test is not local allow for cleanup.
                    if (!isLocalTest()) {
                        cleanAfterExecuteFailed();
                    }
                }

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

    /**
     * Sets up the fixture, for example, open a network connection.
     * This method is called before a test is executed.
     * Calls old setup method by default.
     */
    protected void setUp() throws Exception {
        try {
            setup();
        } catch (Throwable exception) {
            if (exception instanceof Exception) {
                throw (Exception)exception;
            } else {
                throw new TestErrorException("Fatal errored in setUp.", exception);
            }
        }
    }

    /**
     * Tears down the fixture, for example, close a network connection.
     * This method is called after a test is executed.
     * Calls old reset method by default.
     */
    protected void tearDown() throws Exception {
        try {
            reset();
            resetVerify();
        } catch (Throwable exception) {
            if (exception instanceof Exception) {
                throw (Exception)exception;
            } else {
                throw new TestErrorException("Fatal errored in tearDown.", exception);
            }
        }
    }

    /**
     * If there is no executor,
     * Create a default executor and run the test.
     */
    public void runBare() throws Throwable {
        TestExecutor executor = getExecutor();
        if (executor == null) {
            executor = TestExecutor.getDefaultExecutor();
        }
        try {
            execute(executor);
        } catch (TestWarningException exception) {
            System.out.println("WARNING: " + exception);
        }
    }

    /**
     * Return test collection which contains this test entity.
     */
    public TestEntity getContainer() {
        return container;
    }

    /**
     * Return the description of the test.
     */
    public String getDescription() {
        return description;
    }

    /**
     * Return the executor.
     */
    public TestExecutor getExecutor() {
        return executor;
    }

    /**
     * Get the indentaitonString
     */
    public String getIndentationString() {
        return indentationString;
    }

    /**
     * Return the nested counter value
     */
    public int getNestedCounter() {
        return nestedCounter;
    }

    /**
     * Return the test result. The testResult stores the result of this
     * test.
     */
    public ResultInterface getReport() {
        return getTestResult();
    }

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

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

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

    /**
     * Return the database session.
     */
    public org.eclipse.persistence.sessions.Session getSession() {
        return getExecutor().getSession();
    }

    /**
     * Return and cast the session to a Server.
     */
    public Server getServerSession() {
        return (Server)getExecutor().getSession();
    }

    /**
     * Begin a transaction, cast to AbstractSession to work with all session types.
     */
    public void beginTransaction() {
        getAbstractSession().beginTransaction();
    }

    /**
     * Commit a transaction, cast to AbstractSession to work with all session types.
     */
    public void commitTransaction() {
        getAbstractSession().commitTransaction();
    }

    /**
     * Rollback a transaction, cast to AbstractSession to work with all session types.
     */
    public void rollbackTransaction() {
        getAbstractSession().rollbackTransaction();
    }

    public EclipseLinkException getTestException() {
        return getTestResult().getException();
    }

    /**
     * Return the test result. The testResult stores the result of this
     * test.
     */
    public TestResult getTestResult() {
        return testResult;
    }

    public void incrementNestedCounter() {
        setNestedCounter(getNestedCounter() + 1);
    }

    /**
     * The result of the test is logged on to the specified print stream.
     * This method is added to migrate tests to Ora*Tst
     */
    public void logRegressionResult(Writer log) {
        computeNestedLevel();
        setIndentationString(Helper.getTabs(getNestedCounter()));

        try {
            log.write(org.eclipse.persistence.internal.helper.Helper.cr() + getIndentationString() + "TEST NAME:   " + getName() + org.eclipse.persistence.internal.helper.Helper.cr());
            log.write(getIndentationString() + "TEST DESCRIPTION: " + getDescription() + org.eclipse.persistence.internal.helper.Helper.cr());
            log.flush();
        } catch (IOException exception) {
        }
        getTestResult().logRegressionResult(log);
    }

    /**
     * The result of the test is logged on to the specified print stream.
     */
    public void logResult(Writer log, boolean shouldLogOnlyErrors) {
        logResult(log);
    }

    /**
     * The result of the test is logged on to the specified print stream.
     */
    public void logResult(Writer log) {
        computeNestedLevel();
        setIndentationString(Helper.getTabs(getNestedCounter()));

        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 NAME:   " + getName() + org.eclipse.persistence.internal.helper.Helper.cr());
            log.write(getIndentationString() + "TEST DESCRIPTION: " + getDescription() + org.eclipse.persistence.internal.helper.Helper.cr());
            log.flush();
        } catch (IOException exception) {
        }
        getTestResult().logResult(log);
    }

    public boolean requiresDatabase() {
        return true;
    }

    /**
     * This is a optional method in the test cases. It should be overridden only if somthing has to
     * be reset back to the state from where the test started.
     */
    public void reset() throws Throwable {
        return;
    }

    /**
     * Reset the entity.
     */
    public void resetEntity() {
        try {
            reset();
        } catch (Throwable runtimeException) {
            TestProblemException validationException = new TestProblemException("Reset problem occurred.", runtimeException);
            if (getTestException() == null) {
                setTestException(validationException);
            }
            throw validationException;
        }
    }

    public void resetNestedCounter() {
        setNestedCounter(INITIAL_VALUE);
    }

    /**
     * This is a mandatory method in the test cases only if reset has been overridden.
     * The method should check if reset method really reset the databse back to the state
     * from where the test started.
     */
    protected void resetVerify() throws Throwable {
        return;
    }

    public void setContainer(TestEntity testEntity) {
        container = testEntity;
    }

    /**
     * Set the description of the test.
     */
    public void setDescription(String description) {
        this.description = description;
    }

    /**
     * Set the executor.
     */
    public void setExecutor(TestExecutor anExecutor) {
        executor = anExecutor;
    }

    /**
     * Set the indentaitonString
     */
    public void setIndentationString(String indentationString) {
        this.indentationString = indentationString;
    }

    /**
     * Set the nested counter value.
     */
    public void setNestedCounter(int level) {
        this.nestedCounter = level;
    }

    /**
     * The exception raised by the test is stored in the result. Eventually the result
     * decides the outcome of the test depending upon the kind of exception. No exception is
     * stored if test runs well.
     */
    public void setTestException(EclipseLinkException exception) {
        getTestResult().setException(exception);
    }

    /**
     * Set the test result.
     */
    public void setReport(ResultInterface testResult) {
        setTestResult((TestResult)testResult);
    }

    /**
     * Set the test result. The testResult stores the result of the
     * test.
     */
    public void setTestResult(TestResult testResult) {
        this.testResult = testResult;
        testResult.setTestCase(this);
    }

    /**
     * The first step in testing process. The method is overridden if their is something
     * that test should perform before running the actual test.
     */
    protected void setup() throws Throwable {
        return;
    }

    /**
     * Override to run the test and assert its state.
     * @exception Throwable if any exception is thrown
     */
    protected void runTest() throws Throwable {
        // Nothing by default.
    }

    /**
     * Allow for test to intercept running of test.
     */
    public void executeTest() throws Throwable {
        test();
    }

    /**
     * Test can be define as test, or runTest,
     * or if the name is a method on this class,
     * this method is run reflectively by default.
     */
    protected void test() throws Throwable {
        runTest();
    }

    /**
     * Print the test as its name.
     */
    public String toString() {
        return getName();
    }

    /**
     * Verification of test is done here if the test ran properly or not. Subclasses should
     * override it.
     */
    protected void verify() throws Throwable {
        return;
    }

    /**
     * Throw a test error exception.
     * Errors indicate the test failed.
     */
    public void throwError(String message) {
        throw new TestErrorException(message);
    }

    /**
     * Throw a test error exception.
     * Errors indicate the test failed.
     */
    public void throwError(String message, Throwable exception) {
        throw new TestErrorException(message, exception);
    }

    /**
     * Throw a test warning exception.
     * Warning indicate the test did not fail, but did not pass either,
     * the test could not be run or the result could not be verified.
     */
    public void throwWarning(String message) {
        throw new TestWarningException(message);
    }

    /**
     * Sometimes a test prefers to perform a number of small checks/assertions
     * in the test() method, rather than calling a single test() followed
     * by a single verify().
     * <p>
     * In line with JUNIT style testing, would be called assert if assert
     * did not become a reserved keyword in JDK 1.4.2.
     */
    public void strongAssert(boolean assertion, String errorMessage) {
        if (!assertion) {
            throw new TestErrorException(errorMessage);
        }
    }

    /**
     * Same as strongAssert but only throws a warning.  I.e. if the check
     * is how the feature is implemented but not really crucial to how it works,
     * just throw a warning and fix the test later.
     * @param assertion
     * @param warningMessage
     */
    public void weakAssert(boolean assertion, String warningMessage) {
        if (!assertion) {
            throw new TestWarningException(warningMessage);
        }
    }

    /**
     * Answer true to a local test check if the class name ends in "Local"
     */
    private boolean isLocalTest() {
        return getClass().getName().endsWith("Local");
    }

    /**
     * If the execute fails, do the necessary cleanup to ensure the next test gets no residue
     */
    public void cleanAfterExecuteFailed() {
    }

    /**
     * Throws a warning of pessimistic locking/select for update is not supported for this test platform.
     * Currently testing supports select for update on Oracle, MySQL, SQLServer, TimesTen.
     * Some of the other platforms may have some support for select for update, but the databases we test with
     * for these do not have sufficient support to pass the tests.
     * Derby, Firebird and Symfoware (bug 304903) have some support, but does not work with joins (2008-12-01).
     */
    public void checkSelectForUpateSupported() {
        DatabasePlatform platform = getSession().getPlatform();
        if (platform.isFirebird() || platform.isAccess() || platform.isSybase() || platform.isSQLAnywhere() || platform.isDerby() || platform.isHSQL() || platform.isSymfoware()) {
            throw new TestWarningException("This database does not support FOR UPDATE");
        }
    }

    /**
     * Throws a warning of pessimistic locking/select for update nowait is not supported for this test platform.
     * Currently testing supports nowait on Oracle, SQLServer, PostgreSQL.
     */
    public void checkNoWaitSupported() {
        DatabasePlatform platform = getSession().getPlatform();
        if (!(platform.isOracle() || platform.isSQLServer() || platform.isPostgreSQL())) {
            throw new TestWarningException("This database does not support NOWAIT");
        }
    }


    /**
     * Throws a warning if the test database is using serializable transaction isolation.
     */
    public SessionEventAdapter checkTransactionIsolation() {
        final SessionEventAdapter listener;
        DatabasePlatform platform = getSession().getPlatform();
        if (platform.isSybase()) {
            if (SybaseTransactionIsolationListener.isDatabaseVersionSupported((ServerSession) getAbstractSession().getParent())) {
                listener = new SybaseTransactionIsolationListener();
            } else {
                throw new TestWarningException("The test requires Sybase version " + SybaseTransactionIsolationListener.requiredVersion + " or higher");
            }
        } else if (platform.isSQLServer()) {
            throw new TestWarningException("This test requires transaction isolation setup on SQLServer database which is currently not set in tlsvrdb6");
        } else if (platform.isSQLAnywhere()) {
            throw new TestWarningException("This test requires transaction isolation setup on SQLAnywhere database which is currently not set");
        } else if (platform.isDB2()) {
            throw new TestWarningException("This test requires transaction isolation setup on DB2 database which is currently not set");
        } else if (platform.isSymfoware()) {
            listener = new TransactionIsolationLevelSwitchListener();
        } else if (platform.isMaxDB()) {
            listener = new JDBCIsoLevelSwitchListener();
        } else {
            return null;
        }
        getAbstractSession().getParent().getEventManager().addListener(listener);
        return listener;
    }

    /**
     * Return if stored procedures are supported for the database platform for the test database.
     */
    public static boolean supportsStoredProcedures(Session session) {
        DatabasePlatform platform = session.getPlatform();
        return platform.isOracle() || platform.isSybase() || platform.isMySQL() || platform.isSQLServer() || platform.isSymfoware();
    }

    /**
     * Force a garbage collection.
     */
    public void forceGC() {
        WeakReference ref = new WeakReference(new Object());
        for (int loops = 0; loops < 10; loops++) {
            //List junk = new ArrayList (10);
            for (int i = 0; i < 10; i++) {
                //junk.add(new java.math.BigDecimal(i));
            }

            // Force garbage collection.
            System.gc();
            System.runFinalization();
        }
        // Check if a garbage collect really occurred.
        if (ref.get() != null) {
            System.out.println("WARNING: gc did not occur");
        }
    }
}
