/*
 * 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.util.*;

import org.eclipse.persistence.sessions.*;

/**
 * Used to compare the performance of two different task/processes.
 * Defines a test and testBaseLine method and compares the performance of the two.
 * Use the static REPEATS for number of repeats, and computes the averge difference/etc.
 * Note that setup should do any setup and the test should only do what is being compared.
 */
public abstract class PerformanceComparisonTestCase extends TestCase implements PerformanceComparisonTest {

    /**
     * The allowable percentage difference for test compared to the baseline.
     */
    public double allowableDecrease;

    /**
     * The total time (ms) to run each of the test runs.
     * This is used to compute the number of test iterations to use,
     * the tests are then still timed based on that number of iterations.
     */
    public long testRunTime;

    /**
     * List of performance tests, the source test is the baseline,
     * n other tests can be added to compare with.
     */
    protected List tests;

    /**
     * The count of the iterations.
     * This is maintained by the test to allow concurrent tests to manage the count.
     */
    protected volatile int iterations;

    public PerformanceComparisonTestCase() {
        this.testRunTime = DEFAULT_TEST_TIME;
        this.allowableDecrease = DEFAULT_ALLOWABLE_DECREASE;
        this.tests = new ArrayList();
    }

    /**
     * Return the count of the iterations.
     * This is maintained by the test to allow concurrent tests to manage the count.
     */
    public int getIterations() {
        return iterations;
    }

    /**
     * Increment the iteration count.
     * This is maintained by the test to allow concurrent tests to manage the count.
     */
    public void incrementIterations() {
        this.iterations++;
    }

    /**
     * Reset the iteration count.
     * This is maintained by the test to allow concurrent tests to manage the count.
     */
    public void resetIterations() {
        this.iterations = 0;
    }

    /**
     * Allows any test specific setup before starting the test run.
     */
    public void startTest() {
    }

    /**
     * Allows any test specific setup before starting the test run.
     */
    public void endTest() {
    }

    /**
     * Return the total time (ms) to run each of the test runs.
     * This is used to compute the number of test iterations to use,
     * the tests are then still timed based on that number of iterations.
     */
    public long getTestRunTime() {
        return testRunTime;
    }

    /**
     * Set the total time (ms) to run each of the test runs.
     * This is used to compute the number of test iterations to use,
     * the tests are then still timed based on that number of iterations.
     */
    public void setTestRunTime(long testRunTime) {
        this.testRunTime = testRunTime;
    }

    /**
     * Return the allowable percentage difference for test compared to the baseline.
     */
    public double getAllowableDecrease() {
        return allowableDecrease;
    }

    /**
     * Set the allowable percentage difference for test compared to the baseline.
     */
    public void setAllowableDecrease(double allowableDecrease) {
        this.allowableDecrease = allowableDecrease;
    }

    /**
     * Return the performance tests to compare the base-line with.
     */
    public List getTests() {
        return tests;
    }

    /**
     * Add a performance tests to compare the base-line with.
     */
    public void addTest(TestCase test) {
        getTests().add(test);
    }


    /**
     * Executes this test case.
     * Log some debug info as perf tests sometimes take a long time or hang.
     */
    public void execute(TestExecutor executor) {
        System.out.println("Begin:" + getName());
        try {
            super.execute(executor);
        } finally {
            System.out.println("End:" + getName() + ":" + getTestResult().getTotalTime());
        }
    }

    /**
     * Executes this test comparison with the base-line.
     * Static allow reuse with EJB tests.
     */
    public void executeTest() throws Throwable {
        executeTest(this);
    }

    /**
     * Executes this test comparison with the base-line.
     * Static allow reuse with EJB tests.
     */
    public static void executeTest(PerformanceComparisonTest performanceTest) throws Throwable {
        PerformanceComparisonTestResult result = new PerformanceComparisonTestResult((TestCase)performanceTest, "Passed");
        ((TestCase)performanceTest).setTestResult(result);
        try {
            // Repeat the test and baseline for the number of repeats.
            for (int index = 0; index < REPEATS; index++) {
                long startTime, endTime;
                int iterations = 0;
                try {
                    System.gc();
                    Thread.sleep(1000);
                    performanceTest.resetIterations();
                    startTime = System.currentTimeMillis();
                    performanceTest.startTest();
                    endTime = startTime;
                    // Count how many times the test can be invoked in the run time.
                    // This allows for the test run time to be easily changed.
                    while ((startTime + performanceTest.getTestRunTime()) >= endTime) {
                        ((TestCase)performanceTest).test();
                        performanceTest.incrementIterations();
                        endTime = System.currentTimeMillis();
                    }
                    iterations = performanceTest.getIterations();
                } finally {
                    performanceTest.endTest();
                }
                result.addTestCount(iterations, 0);

                for (int testIndex = 0; testIndex < performanceTest.getTests().size(); testIndex++) {
                    PerformanceComparisonTest test = (PerformanceComparisonTest)performanceTest.getTests().get(testIndex);
                    ((TestCase)test).setExecutor(((TestCase)performanceTest).getExecutor());
                    try {
                        System.gc();
                        Thread.sleep(1000);
                        performanceTest.resetIterations();
                        startTime = System.currentTimeMillis();
                        test.startTest();
                        endTime = startTime;
                        // Count how many times the test can be invoked in the run time.
                        // This allows for the test run time to be easily changed.
                        while ((startTime + performanceTest.getTestRunTime()) >= endTime) {
                            ((TestCase)test).test();
                            performanceTest.incrementIterations();
                            endTime = System.currentTimeMillis();
                        }
                        iterations = performanceTest.getIterations();
                    } finally {
                        test.endTest();
                    }
                    result.addTestCount(iterations, testIndex + 1);
                }
            }
        } finally {
            result.computeResults();
        }
    }

    /**
     * Verify each test comparison is within the set limits.
     * Static allow reuse with EJB tests.
     */
    public void verify() {
        verify(this);
    }

    /**
     * Verify each test comparison is within the set limits.
     * Static allow reuse with EJB tests.
     */
    public static void verify(PerformanceComparisonTest performanceTest) {
        PerformanceComparisonTestResult result = (PerformanceComparisonTestResult)((TestCase)performanceTest).getTestResult();
        for (int index = 0; index < result.percentageDifferences.size(); index++) {
            double difference = ((Number)result.percentageDifferences.get(index)).doubleValue();
            PerformanceComparisonTest test = (PerformanceComparisonTest)performanceTest.getTests().get(index);
            double allowable = test.getAllowableDecrease();
            if (difference < allowable) {
                throw new TestErrorException("Decrease lower than allowable detected, decrease: " + difference +
                                             " allowed: " + allowable + " test: " + test.getName());
            }
        }
    }

    /**
     * Build and return an emulated session for isolated Java performance from the database.
     */
    public Session buildEmulatedSession() {
        try {
            Class.forName(getSession().getLogin().getDriverClassName());
        } catch (Exception ignore) {}
        Project project = getSession().getProject().clone();
        DatabaseLogin login = (DatabaseLogin)project.getLogin().clone();
        try {
            Class.forName(login.getDriverClassName());
        } catch (Exception ignore) {}
        //login.useDirectDriverConnect();
        login.setDriverClass(org.eclipse.persistence.testing.tests.performance.emulateddb.EmulatedDriver.class);
        login.setConnectionString("emulate:" + login.getConnectionString());
        project.setLogin(login);
        DatabaseSession session = project.createDatabaseSession();
        session.setSessionLog(getSession().getSessionLog());
        session.login();

        return session;
    }
}
