import argparse
import enum
import os
import shlex
import sys

import lit.reports
import lit.util


@enum.unique
class TestOrder(enum.Enum):
    LEXICAL = "lexical"
    RANDOM = "random"
    SMART = "smart"


@enum.unique
class TestOutputLevel(enum.IntEnum):
    OFF = 0
    FAILED = 1
    ALL = 2

    @classmethod
    def create(cls, value):
        if value == "off":
            return cls.OFF
        if value == "failed":
            return cls.FAILED
        if value == "all":
            return cls.ALL
        raise ValueError(f"invalid output level {repr(value)} of type {type(value)}")


class TestOutputAction(argparse.Action):
    def __init__(self, option_strings, dest, **kwargs):
        super().__init__(option_strings, dest, nargs=None, **kwargs)

    def __call__(self, parser, namespace, value, option_string=None):
        TestOutputAction.setOutputLevel(namespace, self.dest, value)

    @classmethod
    def setOutputLevel(cls, namespace, dest, value):
        setattr(namespace, dest, value)
        if dest == "test_output" and TestOutputLevel.create(
            namespace.print_result_after
        ) < TestOutputLevel.create(value):
            setattr(namespace, "print_result_after", value)
        elif dest == "print_result_after" and TestOutputLevel.create(
            namespace.test_output
        ) > TestOutputLevel.create(value):
            setattr(namespace, "test_output", value)


class AliasAction(argparse.Action):
    def __init__(self, option_strings, dest, nargs=None, **kwargs):
        self.expansion = kwargs.pop("alias", None)
        if not self.expansion:
            raise ValueError("no aliases expansion provided")
        super().__init__(option_strings, dest, nargs=0, **kwargs)

    def __call__(self, parser, namespace, value, option_string=None):
        for e in self.expansion:
            if callable(e):
                e(namespace)
            else:
                dest, val = e
                setattr(namespace, dest, val)


def parse_args():
    parser = argparse.ArgumentParser(prog="lit", fromfile_prefix_chars="@")
    parser.add_argument(
        "test_paths",
        nargs="+",
        metavar="TEST_PATH",
        help="File or path to include in the test suite",
    )

    parser.add_argument(
        "--version", action="version", version="%(prog)s " + lit.__version__
    )

    parser.add_argument(
        "-j",
        "--threads",
        "--workers",
        dest="workers",
        metavar="N",
        help="Number of workers used for testing",
        type=_positive_int,
        default=os.getenv("LIT_MAX_WORKERS", lit.util.usable_core_count()),
    )
    parser.add_argument(
        "--config-prefix",
        dest="configPrefix",
        metavar="NAME",
        help="Prefix for 'lit' config files",
    )
    parser.add_argument(
        "-D",
        "--param",
        dest="user_params",
        metavar="NAME=VAL",
        help="Add 'NAME' = 'VAL' to the user defined parameters",
        action="append",
        default=[],
    )

    format_group = parser.add_argument_group("Output Format")
    format_group.add_argument(
        "--test-output",
        help="Control whether the executed commands and their outputs are printed after each test has executed (default off). "
        "If --print-result-after is set lower than the level given to --test-output, --print-result-after is raised to match.",
        choices=["off", "failed", "all"],
        default="off",
        action=TestOutputAction,
    )
    format_group.add_argument(
        "--print-result-after",
        help="Control which the executed test names and results are printed after each test has executed (default all). "
        "If --test-output is set higher than the level given to --print-result-after, --test-output is lowered to match.",
        choices=["off", "failed", "all"],
        default="all",
        action=TestOutputAction,
    )
    format_group.add_argument(
        "--diagnostic-level",
        help="Control how verbose lit diagnostics should be (default note)",
        choices=["error", "warning", "note"],
        default="note",
    )
    format_group.add_argument(
        "--terse-summary",
        help="Print the elapsed time and the number of passed tests after all tests have finished (default on)",
        action="store_true",
        dest="terse_summary",
    )
    format_group.add_argument(
        "--no-terse-summary",
        help="Don't show the elapsed time after all tests have finished, and only show the number of failed tests.",
        action="store_false",
        dest="terse_summary",
    )
    parser.set_defaults(terse_summary=False)
    format_group.add_argument(
        "-q",
        "--quiet",
        help="Alias for '--diagnostic-level=error --test-output=off --terse-summary'",
        action=AliasAction,
        alias=[
            lambda namespace: TestOutputAction.setOutputLevel(
                namespace, "print_result_after", "failed"
            ),
            lambda namespace: TestOutputAction.setOutputLevel(
                namespace, "test_output", "off"
            ),
            ("diagnostic_level", "error"),
            ("terse_summary", True),
        ],
    )
    format_group.add_argument(
        "-s",
        "--succinct",
        help="Alias for '--progress-bar --print-result-after=failed'",
        action=AliasAction,
        alias=[
            ("useProgressBar", True),
            lambda namespace: TestOutputAction.setOutputLevel(
                namespace, "print_result_after", "failed"
            ),
        ],
    )
    format_group.add_argument(
        "-v",
        "--verbose",
        help="For failed tests, show all output. For example, each command is"
        " printed before it is executed, so the last printed command is the one"
        " that failed. Alias for '--test-output=failed'",
        action=AliasAction,
        alias=[
            lambda namespace: TestOutputAction.setOutputLevel(
                namespace, "test_output", "failed"
            ),
        ],
    )
    format_group.add_argument(
        "-vv",
        "--echo-all-commands",
        help="Deprecated alias for -v.",
        action=AliasAction,
        alias=[
            lambda namespace: TestOutputAction.setOutputLevel(
                namespace, "test_output", "failed"
            ),
        ],
    )
    format_group.add_argument(
        "-a",
        "--show-all",
        help="Enable -v, but for all tests not just failed tests. Alias for '--test-output=all'",
        action=AliasAction,
        alias=[
            lambda namespace: TestOutputAction.setOutputLevel(
                namespace, "test_output", "all"
            ),
        ],
    )
    format_group.add_argument(
        "-r",
        "--relative-paths",
        dest="printPathRelativeCWD",
        help="Print paths relative to CWD",
        action="store_true",
    )
    format_group.add_argument(
        "-o",
        "--output",
        type=lit.reports.JsonReport,
        help="Write test results to the provided path",
        metavar="PATH",
    )
    format_group.add_argument(
        "--progress-bar",
        dest="useProgressBar",
        help="Show curses based progress bar",
        action="store_true",
    )
    format_group.add_argument(
        "--no-progress-bar",
        dest="useProgressBar",
        help="Do not use curses based progress bar (default)",
        action="store_false",
    )

    # Note: this does not generate flags for user-defined result codes.
    success_codes = [c for c in lit.Test.ResultCode.all_codes() if not c.isFailure]
    for code in success_codes:
        format_group.add_argument(
            "--show-{}".format(code.name.lower()),
            dest="shown_codes",
            help="Show {} tests ({})".format(code.label.lower(), code.name),
            action="append_const",
            const=code,
            default=[],
        )

    execution_group = parser.add_argument_group("Test Execution")
    execution_group.add_argument(
        "--gtest-sharding",
        help="Enable sharding for GoogleTest format",
        action="store_true",
        default=True,
    )
    execution_group.add_argument(
        "--no-gtest-sharding",
        dest="gtest_sharding",
        help="Disable sharding for GoogleTest format",
        action="store_false",
    )
    execution_group.add_argument(
        "--path",
        help="Additional paths to add to testing environment",
        action="append",
        default=[],
        type=os.path.abspath,
    )
    execution_group.add_argument(
        "--vg", dest="useValgrind", help="Run tests under valgrind", action="store_true"
    )
    execution_group.add_argument(
        "--vg-leak",
        dest="valgrindLeakCheck",
        help="Check for memory leaks under valgrind",
        action="store_true",
    )
    execution_group.add_argument(
        "--vg-arg",
        dest="valgrindArgs",
        metavar="ARG",
        help="Specify an extra argument for valgrind",
        action="append",
        default=[],
    )
    execution_group.add_argument(
        "--no-execute",
        dest="noExecute",
        help="Don't execute any tests (assume PASS)",
        action="store_true",
    )
    execution_group.add_argument(
        "--xunit-xml-output",
        type=lit.reports.XunitReport,
        help="Write XUnit-compatible XML test reports to the specified file",
    )
    execution_group.add_argument(
        "--report-failures-only",
        help="Only include unresolved, timed out, failed"
        " and unexpectedly passed tests in the report",
        action="store_true",
    )
    execution_group.add_argument(
        "--resultdb-output",
        type=lit.reports.ResultDBReport,
        help="Write LuCI ResultDB compatible JSON to the specified file",
    )
    execution_group.add_argument(
        "--time-trace-output",
        type=lit.reports.TimeTraceReport,
        help="Write Chrome tracing compatible JSON to the specified file",
    )
    # This option only exists for the benefit of LLVM's Buildkite CI pipelines.
    # As soon as it is not needed, it should be removed. Its help text would be:
    # When enabled, lit will add a unique element to the output file name,
    # before the extension. For example "results.xml" will become
    # "results.<something>.xml". The "<something>" is not ordered in any
    # way and is chosen so that existing files are not overwritten. [Default: Off]
    execution_group.add_argument(
        "--use-unique-output-file-name",
        help=argparse.SUPPRESS,
        action="store_true",
    )
    execution_group.add_argument(
        "--timeout",
        dest="maxIndividualTestTime",
        help="Maximum time to spend running a single test (in seconds). "
        "0 means no time limit. [Default: 0]",
        type=_non_negative_int,
    )
    execution_group.add_argument(
        "--max-retries-per-test",
        dest="maxRetriesPerTest",
        metavar="N",
        help="Maximum number of allowed retry attempts per test "
        "(NOTE: The config.test_retry_attempts test suite option and "
        "ALLOWED_RETRIES keyword always take precedence)",
        type=_positive_int,
    )
    execution_group.add_argument(
        "--max-failures",
        help="Stop execution after the given number of failures.",
        type=_positive_int,
    )
    execution_group.add_argument(
        "--allow-empty-runs",
        help="Do not fail the run if all tests are filtered out",
        action="store_true",
    )
    execution_group.add_argument(
        "--per-test-coverage",
        dest="per_test_coverage",
        action="store_true",
        help="Enable individual test case coverage",
    )
    execution_group.add_argument(
        "--ignore-fail",
        dest="ignoreFail",
        action="store_true",
        help="Exit with status zero even if some tests fail",
    )
    execution_group.add_argument(
        "--update-tests",
        dest="update_tests",
        action="store_true",
        help="Try to update regression tests to reflect current behavior, if possible",
    )
    execution_test_time_group = execution_group.add_mutually_exclusive_group()
    execution_test_time_group.add_argument(
        "--skip-test-time-recording",
        help="Do not track elapsed wall time for each test",
        action="store_true",
    )
    execution_test_time_group.add_argument(
        "--time-tests",
        help="Track elapsed wall time for each test printed in a histogram",
        action="store_true",
    )

    selection_group = parser.add_argument_group("Test Selection")
    selection_group.add_argument(
        "--max-tests",
        metavar="N",
        help="Maximum number of tests to run",
        type=_positive_int,
    )
    selection_group.add_argument(
        "--max-time",
        dest="timeout",
        metavar="N",
        help="Maximum time to spend testing (in seconds)",
        type=_positive_int,
    )
    selection_group.add_argument(
        "--order",
        choices=[x.value for x in TestOrder],
        default=TestOrder.SMART,
        help="Test order to use (default: smart)",
    )
    selection_group.add_argument(
        "--shuffle",
        dest="order",
        help="Run tests in random order (DEPRECATED: use --order=random)",
        action="store_const",
        const=TestOrder.RANDOM,
    )
    selection_group.add_argument(
        "-i",
        "--incremental",
        help="Run failed tests first (DEPRECATED: use --order=smart)",
        action="store_true",
    )
    selection_group.add_argument(
        "--filter",
        metavar="REGEX",
        type=_case_insensitive_regex,
        help="Only run tests with paths matching the given regular expression",
        default=os.environ.get("LIT_FILTER", ".*"),
    )
    selection_group.add_argument(
        "--filter-out",
        metavar="REGEX",
        type=_case_insensitive_regex,
        help="Filter out tests with paths matching the given regular expression",
        default=os.environ.get("LIT_FILTER_OUT", "^$"),
    )
    selection_group.add_argument(
        "--filter-failed",
        dest="filterFailed",
        help="Only run tests which failed in the previous run",
        action="store_true",
    )
    selection_group.add_argument(
        "--xfail",
        metavar="LIST",
        type=_semicolon_list,
        help="XFAIL tests with paths in the semicolon separated list",
        default=os.environ.get("LIT_XFAIL", ""),
    )
    selection_group.add_argument(
        "--xfail-not",
        metavar="LIST",
        type=_semicolon_list,
        help="do not XFAIL tests with paths in the semicolon separated list",
        default=os.environ.get("LIT_XFAIL_NOT", ""),
    )
    selection_group.add_argument(
        "--exclude-xfail",
        help="exclude XFAIL tests (unless they are in the --xfail-not list). "
        "Note: This option is implemented in "
        "lit.TestRunner.parseIntegratedTestScript and so will have no effect on "
        "test formats that do not call that and do not implement the option "
        "separately.",
        default=False,
        action="store_true",
    )
    selection_group.add_argument(
        "--num-shards",
        dest="numShards",
        metavar="M",
        help="Split testsuite into M pieces and only run one",
        type=_positive_int,
        default=os.environ.get("LIT_NUM_SHARDS"),
    )
    selection_group.add_argument(
        "--run-shard",
        dest="runShard",
        metavar="N",
        help="Run shard #N of the testsuite",
        type=_positive_int,
        default=os.environ.get("LIT_RUN_SHARD"),
    )

    debug_group = parser.add_argument_group("Debug and Experimental Options")
    debug_group.add_argument(
        "--debug", help="Enable debugging (for 'lit' development)", action="store_true"
    )
    debug_group.add_argument(
        "--show-suites",
        help="Show discovered test suites and exit",
        action="store_true",
    )
    debug_group.add_argument(
        "--show-tests", help="Show all discovered tests and exit", action="store_true"
    )
    debug_group.add_argument(
        "--show-used-features",
        help="Show all features used in the test suite (in XFAIL, UNSUPPORTED and REQUIRES) and exit",
        action="store_true",
    )

    # LIT is special: environment variables override command line arguments.
    env_args = shlex.split(os.environ.get("LIT_OPTS", ""))
    args = sys.argv[1:] + env_args
    opts = parser.parse_args(args)

    # Validate command line options
    if opts.incremental:
        print(
            "WARNING: --incremental is deprecated. Failing tests now always run first."
        )

    if opts.numShards or opts.runShard:
        if not opts.numShards or not opts.runShard:
            parser.error("--num-shards and --run-shard must be used together")
        if opts.runShard > opts.numShards:
            parser.error("--run-shard must be between 1 and --num-shards (inclusive)")
        opts.shard = (opts.runShard, opts.numShards)
    else:
        opts.shard = None

    opts.reports = list(
        filter(
            None,
            [
                opts.output,
                opts.xunit_xml_output,
                opts.resultdb_output,
                opts.time_trace_output,
            ],
        )
    )

    for report in opts.reports:
        report.use_unique_output_file_name = opts.use_unique_output_file_name

    return opts


def _positive_int(arg):
    return _int(arg, "positive", lambda i: i > 0)


def _non_negative_int(arg):
    return _int(arg, "non-negative", lambda i: i >= 0)


def _int(arg, kind, pred):
    desc = "requires {} integer, but found '{}'"
    try:
        i = int(arg)
    except ValueError:
        raise _error(desc, kind, arg)
    if not pred(i):
        raise _error(desc, kind, arg)
    return i


def _case_insensitive_regex(arg):
    import re

    try:
        return re.compile(arg, re.IGNORECASE)
    except re.error as reason:
        raise _error("invalid regular expression: '{}', {}", arg, reason)


def _semicolon_list(arg):
    return arg.split(";")


def _error(desc, *args):
    msg = desc.format(*args)
    return argparse.ArgumentTypeError(msg)
