/*
 *  Copyright 2024 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

// Based on Chromium's https://source.chromium.org/chromium/chromium/src/+/main:testing/android/native_test/java/src/org/chromium/native_test/NativeTest.java
// and Angle's https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/tests/test_utils/runner/android/java/src/com/android/angle/test/AngleNativeTest.java

package org.webrtc.native_test;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Process;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;

import org.jni_zero.JNINamespace;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;

import org.webrtc.native_test.StrictModeContext;
import org.webrtc.native_test.TestStatusReporter;
import org.chromium.build.gtest_apk.NativeTestIntent;

import java.io.File;

/** Helper to run tests inside Activity or NativeActivity. */
@JNINamespace("webrtc::test::android")
public class NativeTestWebrtc {
    private static final String TAG = "NativeTestWebrtc";

    private String mCommandLineFilePath;
    private final StringBuilder mCommandLineFlags = new StringBuilder();
    private TestStatusReporter mReporter;
    private boolean mRunInSubThread;
    private String mStdoutFilePath;

    private static class ReportingUncaughtExceptionHandler
            implements Thread.UncaughtExceptionHandler {

        private final TestStatusReporter mReporter;
        private final Thread.UncaughtExceptionHandler mWrappedHandler;

        public ReportingUncaughtExceptionHandler(
                TestStatusReporter reporter, Thread.UncaughtExceptionHandler wrappedHandler) {
            mReporter = reporter;
            mWrappedHandler = wrappedHandler;
        }

        @Override
        public void uncaughtException(Thread thread, Throwable ex) {
            mReporter.uncaughtException(Process.myPid(), ex);
            if (mWrappedHandler != null) mWrappedHandler.uncaughtException(thread, ex);
        }
    }

    public void preCreate(Activity activity) {
        String coverageDeviceFile =
                activity.getIntent().getStringExtra(NativeTestIntent.EXTRA_COVERAGE_DEVICE_FILE);
        if (coverageDeviceFile != null) {
            try {
                Os.setenv("LLVM_PROFILE_FILE", coverageDeviceFile, true);
            } catch (ErrnoException e) {
                Log.w(TAG, "failed to set LLVM_PROFILE_FILE" + e.toString());
            }
        }
        // Set TMPDIR to make perfetto_unittests not to use /data/local/tmp as a tmp directory.
        try {
            Os.setenv("TMPDIR", activity.getApplicationContext().getCacheDir().getPath(), false);
        } catch (ErrnoException e) {
            Log.w(TAG, "failed to set TMPDIR" + e.toString());
        }
    }

    public void postCreate(Activity activity) {
        parseArgumentsFromIntent(activity, activity.getIntent());
        mReporter = new TestStatusReporter(activity);
        mReporter.testRunStarted(Process.myPid());
        Thread.setDefaultUncaughtExceptionHandler(
                new ReportingUncaughtExceptionHandler(
                        mReporter, Thread.getDefaultUncaughtExceptionHandler()));
    }

    private void parseArgumentsFromIntent(Activity activity, Intent intent) {
        Log.i(TAG, "Extras:");
        Bundle extras = intent.getExtras();
        if (extras != null) {
            for (String s : extras.keySet()) {
                Log.i(TAG, "  " + s);
            }
        }

        mCommandLineFilePath = intent.getStringExtra(NativeTestIntent.EXTRA_COMMAND_LINE_FILE);
        if (mCommandLineFilePath == null) {
            mCommandLineFilePath = "";
        } else {
            File commandLineFile = new File(mCommandLineFilePath);
            if (!commandLineFile.isAbsolute()) {
                mCommandLineFilePath =
                        Environment.getExternalStorageDirectory() + "/" + mCommandLineFilePath;
            }
            Log.i(TAG, "command line file path: " + mCommandLineFilePath);
        }

        String commandLineFlags = intent.getStringExtra(NativeTestIntent.EXTRA_COMMAND_LINE_FLAGS);
        if (commandLineFlags != null) mCommandLineFlags.append(commandLineFlags);

        mRunInSubThread = intent.hasExtra(NativeTestIntent.EXTRA_RUN_IN_SUB_THREAD);

        String gtestFilter = intent.getStringExtra(NativeTestIntent.EXTRA_GTEST_FILTER);
        if (gtestFilter != null) {
            appendCommandLineFlags("--gtest_filter=" + gtestFilter);
        }

        mStdoutFilePath = intent.getStringExtra(NativeTestIntent.EXTRA_STDOUT_FILE);
    }

    public void appendCommandLineFlags(String flags) {
        mCommandLineFlags.append(" ").append(flags);
    }

    public void postStart(final Activity activity, boolean forceRunInSubThread) {
        final Runnable runTestsTask =
                new Runnable() {
                    @Override
                    public void run() {
                        runTests(activity);
                    }
                };

        if (mRunInSubThread || forceRunInSubThread) {
            // Post a task that posts a task that creates a new thread and runs tests on it.

            // On L and M, the system posts a task to the main thread that prints to stdout
            // from android::Layout (https://goo.gl/vZA38p). Chaining the subthread creation
            // through multiple tasks executed on the main thread ensures that this task
            // runs before we start running tests s.t. its output doesn't interfere with
            // the test output. See crbug.com/678146 for additional context.

            final Handler handler = new Handler();
            final Runnable startTestThreadTask =
                    new Runnable() {
                        @Override
                        public void run() {
                            new Thread(runTestsTask).start();
                        }
                    };
            final Runnable postTestStarterTask =
                    new Runnable() {
                        @Override
                        public void run() {
                            handler.post(startTestThreadTask);
                        }
                    };
            handler.post(postTestStarterTask);
        } else {
            // Post a task to run the tests. This allows us to not block
            // onCreate and still run tests on the main thread.
            new Handler().post(runTestsTask);
        }
    }

    private void runTests(Activity activity) {
        Natives jni = NativeTestWebrtcJni.get();
        String isolated_test_root = NativeTestWebrtc.getIsolatedTestRoot();
        Log.i(TAG, "Calling into native code");
        jni.runTests(
                        mCommandLineFlags.toString(),
                        mCommandLineFilePath,
                        mStdoutFilePath,
                        isolated_test_root);
        Log.i(TAG, "Call into native code returned");
        activity.finish();
        mReporter.testRunFinished(Process.myPid());
    }


    // @CalledByNative
    public static String getIsolatedTestRoot() {
        try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
            return Environment.getExternalStorageDirectory().getAbsolutePath()
                   + "/chromium_tests_root";
        }
    }

    // Signal a failure of the native test loader to python scripts
    // which run tests.  For example, we look for
    // RUNNER_FAILED build/android/test_package.py.
    @SuppressWarnings("UnusedMethod")
    private void nativeTestFailed() {
        Log.e(TAG, "[ RUNNER_FAILED ] could not load native library");
    }

    @NativeMethods
    interface Natives {
        void runTests(
                @JniType("std::string") String commandLineFlags,
                @JniType("std::string") String commandLineFilePath,
                @JniType("std::string") String stdoutFilePath,
                @JniType("std::string") String testDataDir);
    }
}
