import argparse
import os
import sys
from collections import OrderedDict
from urllib.parse import urlparse

import mozinfo
import mozlog

here = os.path.abspath(os.path.dirname(__file__))


class ReftestArgumentsParser(argparse.ArgumentParser):
    def __init__(self, **kwargs):
        super(ReftestArgumentsParser, self).__init__(**kwargs)

        # Try to import a MozbuildObject. Success indicates that we are
        # running from a source tree. This allows some defaults to be set
        # from the source tree.
        try:
            from mozbuild.base import MozbuildObject

            self.build_obj = MozbuildObject.from_environment(cwd=here)
        except ImportError:
            self.build_obj = None

        self.add_argument(
            "--xre-path",
            action="store",
            type=str,
            dest="xrePath",
            # individual scripts will set a sane default
            default=None,
            help="absolute path to directory containing XRE (probably xulrunner)",
        )

        self.add_argument(
            "--symbols-path",
            action="store",
            type=str,
            dest="symbolsPath",
            default=None,
            help="absolute path to directory containing breakpad symbols, "
            "or the URL of a zip file containing symbols",
        )

        self.add_argument(
            "--debugger",
            action="store",
            dest="debugger",
            help="use the given debugger to launch the application",
        )

        self.add_argument(
            "--debugger-args",
            action="store",
            dest="debuggerArgs",
            help="pass the given args to the debugger _before_ "
            "the application on the command line",
        )

        self.add_argument(
            "--debugger-interactive",
            action="store_true",
            dest="debuggerInteractive",
            help="prevents the test harness from redirecting "
            "stdout and stderr for interactive debuggers",
        )

        self.add_argument(
            "--appname",
            action="store",
            type=str,
            dest="app",
            default=None,
            help="absolute path to application, overriding default",
        )

        self.add_argument(
            "--extra-profile-file",
            action="append",
            dest="extraProfileFiles",
            default=[],
            help="copy specified files/dirs to testing profile",
        )

        self.add_argument(
            "--timeout",
            action="store",
            dest="timeout",
            type=int,
            default=300,  # 5 minutes per bug 479518
            help="reftest will timeout in specified number of seconds. "
            "[default %(default)s].",
        )

        self.add_argument(
            "--leak-threshold",
            action="store",
            type=int,
            dest="defaultLeakThreshold",
            default=0,
            help="fail if the number of bytes leaked in default "
            "processes through refcounted objects (or bytes "
            "in classes with MOZ_COUNT_CTOR and MOZ_COUNT_DTOR) "
            "is greater than the given number",
        )

        self.add_argument(
            "--utility-path",
            action="store",
            type=str,
            dest="utilityPath",
            default=self.build_obj.bindir if self.build_obj else None,
            help="absolute path to directory containing utility "
            "programs (xpcshell, ssltunnel, certutil)",
        )

        self.add_argument(
            "--total-chunks",
            type=int,
            dest="totalChunks",
            help="how many chunks to split the tests up into",
        )

        self.add_argument(
            "--this-chunk",
            type=int,
            dest="thisChunk",
            help="which chunk to run between 1 and --total-chunks",
        )

        self.add_argument(
            "--log-file",
            action="store",
            type=str,
            dest="logFile",
            default=None,
            help="file to log output to in addition to stdout",
        )

        self.add_argument(
            "--skip-slow-tests",
            dest="skipSlowTests",
            action="store_true",
            default=False,
            help="skip tests marked as slow when running",
        )

        self.add_argument(
            "--ignore-window-size",
            dest="ignoreWindowSize",
            action="store_true",
            default=False,
            help="ignore the window size, which may cause spurious "
            "failures and passes",
        )

        self.add_argument(
            "--install-extension",
            action="append",
            dest="extensionsToInstall",
            default=[],
            help="install the specified extension in the testing profile. "
            "The extension file's name should be <id>.xpi where <id> is "
            "the extension's id as indicated in its install.rdf. "
            "An optional path can be specified too.",
        )

        self.add_argument(
            "--marionette",
            default=None,
            help="host:port to use when connecting to Marionette",
        )

        self.add_argument(
            "--marionette-socket-timeout", default=None, help=argparse.SUPPRESS
        )

        self.add_argument(
            "--marionette-startup-timeout", default=None, help=argparse.SUPPRESS
        )

        self.add_argument(
            "--setenv",
            action="append",
            type=str,
            default=[],
            dest="environment",
            metavar="NAME=VALUE",
            help="sets the given variable in the application's " "environment",
        )

        self.add_argument(
            "--filter",
            action="store",
            type=str,
            dest="filter",
            help="specifies a regular expression (as could be passed to the JS "
            "RegExp constructor) to test against URLs in the reftest manifest; "
            "only test items that have a matching test URL will be run.",
        )

        self.add_argument(
            "--shuffle",
            action="store_true",
            default=False,
            dest="shuffle",
            help="run reftests in random order",
        )

        self.add_argument(
            "--run-until-failure",
            action="store_true",
            default=False,
            dest="runUntilFailure",
            help="stop running on the first failure. Useful for RR recordings.",
        )

        self.add_argument(
            "--repeat",
            action="store",
            type=int,
            default=0,
            dest="repeat",
            help="number of times the select test(s) will be executed. Useful for "
            "finding intermittent failures.",
        )

        self.add_argument(
            "--focus-filter-mode",
            action="store",
            type=str,
            dest="focusFilterMode",
            default="all",
            help="filters tests to run by whether they require focus. "
            "Valid values are `all', `needs-focus', or `non-needs-focus'. "
            "Defaults to `all'.",
        )

        self.add_argument(
            "--disable-e10s",
            action="store_false",
            default=True,
            dest="e10s",
            help="disables content processes",
        )

        self.add_argument(
            "--disable-fission",
            action="store_true",
            default=False,
            dest="disableFission",
            help="Run tests with fission (site isolation) disabled.",
        )

        self.add_argument(
            "--setpref",
            action="append",
            type=str,
            default=[],
            dest="extraPrefs",
            metavar="PREF=VALUE",
            help="defines an extra user preference",
        )

        self.add_argument(
            "--reftest-extension-path",
            action="store",
            dest="reftestExtensionPath",
            help="Path to the reftest extension",
        )

        self.add_argument(
            "--special-powers-extension-path",
            action="store",
            dest="specialPowersExtensionPath",
            help="Path to the special powers extension",
        )

        self.add_argument(
            "--suite",
            choices=["reftest", "crashtest", "jstestbrowser"],
            default=None,
            help=argparse.SUPPRESS,
        )

        self.add_argument(
            "--cleanup-crashes",
            action="store_true",
            dest="cleanupCrashes",
            default=False,
            help="Delete pending crash reports before running tests.",
        )

        self.add_argument(
            "--max-retries",
            type=int,
            dest="maxRetries",
            default=4,
            help="The maximum number of attempts to try and recover from a "
            "crash before aborting the test run [default 4].",
        )

        self.add_argument(
            "tests",
            metavar="TEST_PATH",
            nargs="*",
            help="Path to test file, manifest file, or directory containing "
            "tests. For jstestbrowser, the relative path can be either from "
            "topsrcdir or the staged area "
            "($OBJDIR/dist/test-stage/jsreftest/tests)",
        )

        self.add_argument(
            "--sandbox-read-whitelist",
            action="append",
            dest="sandboxReadWhitelist",
            default=[],
            help="Path to add to the sandbox whitelist.",
        )

        self.add_argument(
            "--verify",
            action="store_true",
            default=False,
            help="Run tests in verification mode: Run many times in different "
            "ways, to see if there are intermittent failures.",
        )

        self.add_argument(
            "--verify-max-time",
            type=int,
            default=3600,
            help="Maximum time, in seconds, to run in --verify mode..",
        )

        self.add_argument(
            "--enable-webrender",
            action="store_true",
            dest="enable_webrender",
            default=False,
            help="Enable the WebRender compositor in Gecko.",
        )

        self.add_argument(
            "--headless",
            action="store_true",
            dest="headless",
            default=False,
            help="Run tests in headless mode.",
        )

        self.add_argument(
            "--topsrcdir",
            action="store",
            type=str,
            dest="topsrcdir",
            default=None,
            help="Path to source directory",
        )

        mozlog.commandline.add_logging_group(self)

    def get_ip(self):
        import moznetwork

        return moznetwork.get_ip()

    def set_default_suite(self, options):
        manifests = OrderedDict(
            [
                ("reftest.list", "reftest"),
                ("crashtests.list", "crashtest"),
                ("jstests.list", "jstestbrowser"),
            ]
        )

        for test_path in options.tests:
            file_name = os.path.basename(test_path)
            if file_name in manifests:
                options.suite = manifests[file_name]
                return

        for test_path in options.tests:
            for manifest_file, suite in manifests.items():
                if os.path.exists(os.path.join(test_path, manifest_file)):
                    options.suite = suite
                    return

        self.error(
            "Failed to determine test suite; supply --suite to set this explicitly"
        )

    def validate(self, options, reftest):
        if not options.tests:
            # Can't just set this in the argument parser because mach will set a default
            self.error(
                "Must supply at least one path to a manifest file, "
                "test directory, or test file to run."
            )

        if options.suite is None:
            self.set_default_suite(options)

        if options.totalChunks is not None and options.thisChunk is None:
            self.error("thisChunk must be specified when totalChunks is specified")

        if options.totalChunks:
            if not 1 <= options.thisChunk <= options.totalChunks:
                self.error("thisChunk must be between 1 and totalChunks")

        if not options.disableFission and not options.e10s:
            self.error("Fission is not supported without e10s.")

        if options.logFile:
            options.logFile = reftest.getFullPath(options.logFile)

        if options.xrePath is not None:
            if not os.access(options.xrePath, os.F_OK):
                self.error("--xre-path '%s' not found" % options.xrePath)
            if not os.path.isdir(options.xrePath):
                self.error("--xre-path '%s' is not a directory" % options.xrePath)
            options.xrePath = reftest.getFullPath(options.xrePath)

        if options.reftestExtensionPath is None:
            if self.build_obj is not None:
                reftestExtensionPath = os.path.join(
                    self.build_obj.distdir, "xpi-stage", "reftest"
                )
            else:
                reftestExtensionPath = os.path.join(here, "reftest")
            options.reftestExtensionPath = os.path.normpath(reftestExtensionPath)

        if options.specialPowersExtensionPath is None:
            if self.build_obj is not None:
                specialPowersExtensionPath = os.path.join(
                    self.build_obj.distdir, "xpi-stage", "specialpowers"
                )
            else:
                specialPowersExtensionPath = os.path.join(here, "specialpowers")
            options.specialPowersExtensionPath = os.path.normpath(
                specialPowersExtensionPath
            )

        options.leakThresholds = {
            "default": options.defaultLeakThreshold,
            "tab": options.defaultLeakThreshold,
        }

        if mozinfo.isWin:
            if mozinfo.info["bits"] == 32:
                # See bug 1408554.
                options.leakThresholds["tab"] = 3000
            else:
                # See bug 1404482.
                options.leakThresholds["tab"] = 100

        if options.topsrcdir is None:
            if self.build_obj:
                options.topsrcdir = self.build_obj.topsrcdir
            else:
                options.topsrcdir = os.getcwd()


class DesktopArgumentsParser(ReftestArgumentsParser):
    def __init__(self, **kwargs):
        super(DesktopArgumentsParser, self).__init__(**kwargs)

        self.add_argument(
            "--run-tests-in-parallel",
            action="store_true",
            default=False,
            dest="runTestsInParallel",
            help="run tests in parallel if possible",
        )

    def validate(self, options, reftest):
        super(DesktopArgumentsParser, self).validate(options, reftest)

        if options.runTestsInParallel:
            if options.logFile is not None:
                self.error("cannot specify logfile with parallel tests")
            if options.totalChunks is not None or options.thisChunk is not None:
                self.error(
                    "cannot specify thisChunk or totalChunks with parallel tests"
                )
            if options.focusFilterMode != "all":
                self.error("cannot specify focusFilterMode with parallel tests")
            if options.debugger is not None:
                self.error("cannot specify a debugger with parallel tests")

        if options.debugger:
            # valgrind and some debuggers may cause Gecko to start slowly. Make sure
            # marionette waits long enough to connect.
            options.marionette_startup_timeout = 900
            options.marionette_socket_timeout = 540

        if not options.tests:
            self.error("No test files specified.")

        if options.app is None:
            if (
                self.build_obj
                and self.build_obj.substs["MOZ_BUILD_APP"] != "mobile/android"
            ):
                from mozbuild.base import BinaryNotFoundException

                try:
                    bin_dir = self.build_obj.get_binary_path()
                except BinaryNotFoundException as e:
                    print(f"{e}\n\n{e.help()}\n", file=sys.stderr)
                    sys.exit(1)
            else:
                bin_dir = None

            if bin_dir:
                options.app = bin_dir

        if options.symbolsPath and len(urlparse(options.symbolsPath).scheme) < 2:
            options.symbolsPath = reftest.getFullPath(options.symbolsPath)

        options.utilityPath = reftest.getFullPath(options.utilityPath)


class RemoteArgumentsParser(ReftestArgumentsParser):
    def __init__(self, **kwargs):
        super(RemoteArgumentsParser, self).__init__()

        # app, xrePath and utilityPath variables are set in main function
        self.set_defaults(
            logFile="reftest.log", app="", xrePath="", utilityPath="", localLogName=None
        )

        self.add_argument(
            "--adbpath",
            action="store",
            type=str,
            dest="adb_path",
            default=None,
            help="Path to adb binary.",
        )

        self.add_argument(
            "--deviceSerial",
            action="store",
            type=str,
            dest="deviceSerial",
            help="adb serial number of remote device. This is required "
            "when more than one device is connected to the host. "
            "Use 'adb devices' to see connected devices.",
        )

        self.add_argument(
            "--remote-webserver",
            action="store",
            type=str,
            dest="remoteWebServer",
            help="IP address of the remote web server.",
        )

        self.add_argument(
            "--http-port",
            action="store",
            type=str,
            dest="httpPort",
            help="http port of the remote web server.",
        )

        self.add_argument(
            "--ssl-port",
            action="store",
            type=str,
            dest="sslPort",
            help="ssl port of the remote web server.",
        )

        self.add_argument(
            "--remoteTestRoot",
            action="store",
            type=str,
            dest="remoteTestRoot",
            help="Remote directory to use as test root "
            "(eg. /data/local/tmp/test_root).",
        )

        self.add_argument(
            "--httpd-path",
            action="store",
            type=str,
            dest="httpdPath",
            help="Path to the httpd.js file.",
        )

        self.add_argument(
            "--no-install",
            action="store_true",
            default=False,
            help="Skip the installation of the APK.",
        )

    def validate_remote(self, options):
        DEFAULT_HTTP_PORT = 8888
        DEFAULT_SSL_PORT = 4443

        if options.remoteWebServer is None:
            options.remoteWebServer = self.get_ip()

        if options.remoteWebServer == "127.0.0.1":
            self.error(
                "ERROR: Either you specified the loopback for the remote webserver or ",
                "your local IP cannot be detected.  "
                "Please provide the local ip in --remote-webserver",
            )

        if not options.httpPort:
            options.httpPort = DEFAULT_HTTP_PORT

        if not options.sslPort:
            options.sslPort = DEFAULT_SSL_PORT

        if options.xrePath is None:
            self.error(
                "ERROR: You must specify the path to the controller xre directory"
            )
        else:
            # Ensure xrepath is a full path
            options.xrePath = os.path.abspath(options.xrePath)

        # httpd-path is specified by standard makefile targets and may be specified
        # on the command line to select a particular version of httpd.js. If not
        # specified, try to select the one from hostutils.zip, as required in
        # bug 882932.
        if not options.httpdPath:
            options.httpdPath = os.path.join(options.utilityPath, "components")

        return options
