/*
 * Copyright (c) 2010, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package vm.mlvm.share;

import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

import nsk.share.ArgumentParser;
import nsk.share.Log;
import nsk.share.Log.TraceLevel;
import nsk.share.test.StressOptions;
import nsk.share.test.Stresser;
import vm.share.options.Option;
import vm.mlvm.share.ExceptionsOptionObjectFactory;

/**
 * The base class for MLVM tests.
 * Subclasses need to override {@link #run()} method to implement test logic.
 */
public abstract class MlvmTest {

    /**
     * MLVM tests are expected to implement this method to provide the logic.
     *
     * @return true if test passed, false if failed
     * @throws Throwable any subclass of Throwable to indicate test failure
     */
    public abstract boolean run() throws Throwable;

    /** Performs pre-run (prolog) actions in MlvmTest subclasses.
     * The default implementation does nothing.
     * Sublcasses may override this method to perform custom actions after test is initialized
     * (initialization order is described in MlvmTestExecutor class) but before {@link run()} method is invoked.
     * @throws Throwable in case of problem, which is interpreted as a test failure
     * @see MlvmTestExecutor
     */
    protected void initializeTest() throws Throwable {
    }

    /** Performs post-run (epilog) actions.
     * This method is executed after the {@link #run()} method.
     * Does nothing by default.
     * Subclasses may override this method when some finalization actions are required.
     * Test fails if this method throws exception.
     * @param result test execution status: true, if test passed, false otherwise
     * @throws Throwable may throw any subclass of Throwable to indicate test failure (regardless of run() method result)
     * @see MlvmTestExecutor
     */
    protected void finalizeTest(boolean result) throws Throwable {
    }

    /**
     * Resets the tests between runs.
     * You may override this method, especially if your test supports -stressRunsFactor option
     * @throws Throwable may throw any subclass of Throwable to indicate test failure (regardless of run() method result)
     * @see MlvmTestExecutor
     */
    protected void resetTest() throws Throwable {
        testMarkedFailed = false;
    }

    // Options for all MlvmTests
    @Option(name = "requireExceptions", default_value = "", factory = ExceptionsOptionObjectFactory.class,
            description = "Specifying this option turns test into negative one: "
                        + "the specified exception class names separated with commas have to be caught for the test to pass")
    private List<Class<? extends Throwable>> requiredExceptionClasses = new ArrayList<>();

    @Option(name = "seed", default_value = "0", description = "Initial random seed")
    private long _seed;

    @Option(name = "runs", default_value = "1", description = "How many times the test should be re-run")
    private int runs = 1;

    // Some internal stuff
    private static MlvmTest instance;

    /**
     * Sets internal static variable to instance of the test.
     * Used in debugger/debuggee tests.
     * Not intended to work if there are several MlvmTests created.
     * @param inst Instance of the test
     */
    public static void setInstance(MlvmTest inst) {
        instance = inst;
    }

    /**
     * Returns internal static variable holding instance of the test, which was set using {@link #setInstance()}.
     * Used in debugger/debuggee tests.
     * Not intended to work if there are several MlvmTests created.
     * @return Instance of the test
     */
    public static MlvmTest getInstance() {
        return instance;
    }

    private static String name = "Test";

    /**
     * Sets internal static variable to the name of the test.
     * Debugger/debuggee MLVM tests use this feature to differentiate logging from debugger and debuggee
     * Not intended to work if there are several MlvmTests created
     * @param n Name of the test
     */
    public static void setName(String n) {
        name = n;
    }

    /**
     * Returns internal static variable holding the name of the test.
     * Debugger/debuggee MLVM tests use this feature to differentiate logging from debugger and debuggee
     * Not intended to work if there are several MlvmTests created
     * @return Name of the test
     */
    public static String getName() {
        return name;
    }

    void initRNG() {
        if (_seed != 0) {
            Env.setRNGSeed(_seed);
        }
        Env.traceVerbose("Initial seed = " + _seed);
    }

    /**
     * Sets number of test runs
     * @param r Number of test runs
     */
    public void setRunsNumber(int r) {
        runs = r;
    }

    /**
     * Return number of test runs
     * @return Number of test runs
     */
    public int getRunsNumber() {
        return runs;
    }

    // Sugar...
    /**
     * Provides Random Number Generator for the test. The tests should always use this generator
     * to guarantee repeatability (using -seed option), especially in multi-threaded usages
     * @return Random number generator for this thread, seeded with command-line option, if provided
     */
    public static Random getRNG() {
        return Env.getRNG();
    }

    /**
     * Returns logger, which is used in all MLVM framework. This guarantees correct ordering of messages
     * @return Logger object
     */
    public static Log getLog() {
        return Env.getLog();
    }

    /**
     * ArgumentParser is the old implementation of command-line parser (the new tests should use
     * vm.share.options framework). However it is maintained, because nsk JDI/SAJDI framework is built
     * on ArgumentParser.
     * @return ArgumentParser object created with command-line options (see {@link MlvmTestExecutor}
     *         for details)
     */
    public static ArgumentParser getArgumentParser() {
        return Env.getArgParser();
    }

    // ...and spice

    /* Makes the test "negative": one of the specified exception classes has to be thrown by the test to pass.
     * Test fails if exception has not been thrown.
     * Boolean value returned by {@link run()} method is ignored.
     * Calling {@link #markTestFailed()} causes test to fail anyway.
     * <p>
     * Invoke this method BEFORE run() method (e.g., in prolog) to instruct launcher
     * to anticipate the exception instead of the positive (normal) mode.
     * @param classes The list of exception classes
     *                Empty list or null indicates that test is positive.
     */
    @SafeVarargs
    public final void setRequiredExceptions(Class<? extends Throwable>... classes) {
        setRequiredExceptions(Arrays.asList(classes));
    }

    /* Makes the test "negative": one of the specified exception classes has to be thrown by the test to pass.
     * Test fails if exception has not been thrown.
     * Boolean value returned by {@link run()} method is ignored.
     * Calling {@link #markTestFailed()} causes test to fail anyway.
     * <p>
     * Invoke this method BEFORE run() method (e.g., in prolog) to instruct launcher
     * @param classes The list of exception classes.
     *                Empty list or null indicates that test is positive (in its standard form)
     */
    public final void setRequiredExceptions(List<Class<? extends Throwable>> classes) {
        if (requiredExceptionClasses.size() > 0) {
            Env.traceNormal("Expected exceptions specified in the test are overridden in command-line");
            return;
        }

        requiredExceptionClasses = classes;
    }

    /**
     * Returns the list of required exceptions
     * (please see {@link #setRequiredExceptions(Class<? extends Throwable>... classes)} method for details.
     * @return The list of exception classes. Empty list or null indicates that test is positive (in its standard form)
     */
    public final List<Class<? extends Throwable>> getRequiredExceptions() {
        return requiredExceptionClasses;
    }

    private boolean testMarkedFailed = false;

    /**
     * Marks the test as failed.
     * Regardless of run() method return value, the test is considered failed. Operation is not reversible.
     * Can be called from multiple threads
     */
    protected final void markTestFailed() {
        markTestFailed(null, null);
    }

    /**
     * Marks the test as failed, indicating falure reason.
     * Regardless of run() method return value, the test is considered failed. Operation is not reversible.
     * Can be called from multiple threads
     * @param msg A message to log (using Log.complain() method)
     */
    protected final void markTestFailed(String msg) {
        markTestFailedImpl(msg, null);
    }

    /**
     * Marks the test as failed, indicating falure reason and exception, which caused it.
     * Regardless of run() method return value, the test is considered failed. Operation is not reversible.
     * Can be called from multiple threads
     * @param msg A message to log (using Log.complain() method)
     * @param t An exception to log
     */
    protected final void markTestFailed(String msg, Throwable t) {
        markTestFailedImpl(msg, t);
    }

    private synchronized void markTestFailedImpl(String msg, Throwable t) {
        testMarkedFailed = true;

        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        Env.complain(t, "%s marked failed at %s%s", getName(), stackTrace[3],
                     msg == null ? "" : ":\n" + msg);

    }

    /**
     * Checks if the test has marked failed.
     * @return true, if the test marked failed
     */
    protected final synchronized boolean isMarkedFailed() {
        return testMarkedFailed;
    }

    private static boolean dumpHeapAfter = false;

    /**
     * Checks if heap dump requestd after running the test.
     * @return true, if the test marked failed
     * @see MlvmTestExecutor for heap dumping details.
     */
    public static synchronized boolean getHeapDumpAfter() {
        return dumpHeapAfter;
    }

    /**
     * Sets or clears heap dumping request. Heap is dumped in MlvmTestExecutor after running the test.
     *
     * NB. heap dumping uses ProcessUtils libraries, so it should be added to library path in cfg-file:
     * {@code export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${COMMON_LIBS_LOCATION}/lib/${ARCH}/vm/share"}
     * @param enable true, if heap should be dumped, false if not
     * @see MlvmTestExecutor for heap dumping details.
     */
    public static synchronized void setHeapDumpAfter(boolean enable) {
        dumpHeapAfter = enable;
    }

    protected static Stresser createStresser() {
        Stresser s = new Stresser(getArgumentParser().getStressOptions());
        if (getLog().getTraceLevel() >= TraceLevel.TRACE_VERBOSE) {
            s.printStressInfo(getLog().getOutStream());
        }
        return s;
    }

    protected static StressOptions getStressOptions() {
        return getArgumentParser().getStressOptions();
    }

    // Launchers are left here for compatibility. Launching code has been moved to MlvmTestExecutor
    // TODO: A minor bug has to be filed to replace MlvmTest.launch() calls with MlvmTestExecutor.launch()

    protected static void launch(ArgumentParser argumentParser) {
        MlvmTestExecutor.launch(argumentParser);
    }

    protected static void launch(ArgumentParser argumentParser, Object[] constructorArgs) {
        MlvmTestExecutor.launch(argumentParser, constructorArgs);
    }

    protected static void launch(String[] args) {
        MlvmTestExecutor.launch(args, null);
    }

    protected static void launch(String[] args, Object[] constructorArgs) {
        MlvmTestExecutor.launch(args, constructorArgs);
    }

}
