/*
 * Copyright 2017 Google Inc.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

package org.skia.skqp;

import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.RuntimeShader;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Pattern;
import org.junit.runner.Description;
import org.junit.runner.RunWith;
import org.junit.runner.Runner;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.Filterable;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;

@RunWith(SkQPRunner.class)
public class SkQPRunner extends Runner implements Filterable {
    private Description[] mUnitTestDesc;
    private Description[] mSkSLErrorTestDesc;
    private Description mSuiteDesc;
    private String mOutputDirectory;
    private SkQP mImpl;
    private static final String TAG = SkQP.LOG_PREFIX;

    private interface TestExecutor {
        public int numTests();
        public String name(int index);
        public Description desc(int index);
        public boolean run(RunNotifier notifier, int index);
    }

    private static void Fail(Description desc, RunNotifier notifier, String failure) {
        notifier.fireTestFailure(new Failure(desc, new SkQPFailure(failure)));
    }

    ////////////////////////////////////////////////////////////////////////////

    public SkQPRunner(Class testClass) {
        mImpl = new SkQP();
        Context context = InstrumentationRegistry.getTargetContext();
        String now = (new SimpleDateFormat("yyyy-MM-dd'T'HHmmss")).format(new Date());
        File reportPath = new File(context.getExternalFilesDir(null), "skqp_report_" + now);
        reportPath.mkdirs();
        mOutputDirectory = reportPath.getAbsolutePath();
        Log.i(TAG, String.format("output written to \"%s\"", mOutputDirectory));

        AssetManager assetManager = context.getResources().getAssets();
        mImpl.nInit(assetManager, mOutputDirectory);

        mUnitTestDesc = new Description[mImpl.mUnitTests.length];
        for (int index = 0; index < mUnitTestDesc.length; ++index) {
            mUnitTestDesc[index] = Description.createTestDescription(
                    SkQPRunner.class, "UnitTest_" + mImpl.mUnitTests[index]);
        }

        mSkSLErrorTestDesc = new Description[mImpl.mSkSLErrorTestName.length];
        for (int index = 0; index < mSkSLErrorTestDesc.length; ++index) {
            mSkSLErrorTestDesc[index] = Description.createTestDescription(
                    SkQPRunner.class, "SkSLErrorTest_" + mImpl.mSkSLErrorTestName[index]);
        }

        this.applyFilter(null);
    }

    private void applyFilter(Filter filter) {
        mSuiteDesc = Description.createSuiteDescription(SkQP.class);
        addFilteredTestsToSuite(mUnitTestDesc, filter);
        addFilteredTestsToSuite(mSkSLErrorTestDesc, filter);
    }

    private void addFilteredTestsToSuite(Description[] tests, Filter filter) {
        for (int i = 0; i < tests.length; ++i) {
            if (filter == null || filter.shouldRun(tests[i])) {
                mSuiteDesc.addChild(tests[i]);
            } else {
                tests[i] = Description.EMPTY;
            }
        }
    }

    @Override
    public void filter(Filter filter) throws NoTestsRemainException {
        this.applyFilter(filter);
        if (mSuiteDesc.isEmpty()) {
            throw new NoTestsRemainException();
        }
    }

    @Override
    public Description getDescription() {
        return mSuiteDesc;
    }

    @Override
    public void run(RunNotifier notifier) {
        int testNumber = 0;
        testNumber = runTests(notifier, new SkSLErrorTestExecutor(), testNumber);
        testNumber = runTests(notifier, new UnitTestExecutor(), testNumber);

        mImpl.nMakeReport();
        Log.i(TAG, String.format("output written to \"%s\"", mOutputDirectory));
    }

    private int runTests(RunNotifier notifier, TestExecutor executor, int testNumber) {
        for (int index = 0; index < executor.numTests(); index++) {
            Description desc = executor.desc(index);
            if (desc.isEmpty()) {
                // This test was filtered out and can be skipped.
                continue;
            }

            ++testNumber;
            notifier.fireTestStarted(desc);

            String result = executor.run(notifier, index) ? "pass" : "FAIL";

            notifier.fireTestFinished(desc);
            Log.i(TAG, String.format("Test '%s' complete (%d/%d). [%s]", executor.name(index),
                                     testNumber, mSuiteDesc.testCount(), result));
        }
        return testNumber;
    }

    class UnitTestExecutor implements TestExecutor {
        public int numTests() {
            return mUnitTestDesc.length;
        }
        public String name(int index) {
            return desc(index).getMethodName();
        }
        public Description desc(int index) {
            return mUnitTestDesc[index];
        }
        public boolean run(RunNotifier notifier, int index) {
            String[] errors = mImpl.nExecuteUnitTest(index);
            if (errors != null && errors.length > 0) {
                Log.w(TAG, String.format("[FAIL] Test '%s' had %d failures.",
                                         name(index), errors.length));
                for (String error : errors) {
                    SkQPRunner.Fail(desc(index), notifier, error);
                    Log.w(TAG, String.format("[FAIL] '%s': %s", name(index), error));
                }
                return false;
            }

            return true;
        }
    }

    class SkSLErrorTestExecutor implements TestExecutor {
        public int numTests() {
            return mSkSLErrorTestDesc.length;
        }
        public String name(int index) {
            return mImpl.mSkSLErrorTestName[index];
        }
        public Description desc(int index) {
            return mSkSLErrorTestDesc[index];
        }
        public boolean run(RunNotifier notifier, int index) {
            String shaderText = mImpl.mSkSLErrorTestShader[index];
            try {
                new RuntimeShader(shaderText);
                // Because this is an error test, we expected an exception to be thrown.
                // If we reach this point, no exception occurred; report this as an error.
                SkQPRunner.Fail(desc(index), notifier, "Shader did not generate any errors.");
                Log.w(TAG, String.format("[FAIL] '%s': Shader did not generate any errors",
                                         name(index)));
                return false;
            }
            catch (Exception ex) {
                // Verify that RuntimeShader actually emitted the expected error messages.
                // The list of expectations isn't necessarily exhaustive, though.
                String errorText = ex.getMessage();
                String[] block = shaderText.split(Pattern.quote("*%%*"));
                if (block.length >= 3) {
                    // We only intend to support a single /%**%/ section.
                    // Because we are splitting on *%%*, expectations should always be in block[1].
                    String[] expectations = block[1].split("\n");
                    for (String expectation : expectations) {
                        if (expectation.length() == 0) {
                            continue;
                        }
                        int errIndex = errorText.indexOf(expectation);
                        // If this error wasn't reported, trigger an error.
                        if (errIndex < 0) {
                            String failMessage = String.format("Expected error '%s', got '%s'",
                                                               expectation, ex.getMessage());
                            SkQPRunner.Fail(desc(index), notifier, failMessage);
                            Log.w(TAG, String.format("[FAIL] '%s': %s", name(index), failMessage));
                            return false;
                        }
                        // We found the error that we expected to have. Remove that error from our
                        // text, and everything preceding it as well. This ensures that we don't
                        // match the same error twice, and that errors are reported in the order
                        // we expect.
                        errorText = errorText.substring(errIndex + expectation.length());
                    }
                }
            }
            return true;
        }
    }
}
