# ===----------------------------------------------------------------------===##
#
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
# ===----------------------------------------------------------------------===##
import sys
import re
import shlex
from pathlib import Path

from libcxx.test.dsl import *
from libcxx.test.features import _isClang, _isAppleClang, _isGCC, _isMSVC


_warningFlags = [
    "-Werror",
    "-Wall",
    "-Wctad-maybe-unsupported",
    "-Wextra",
    "-Wshadow",
    "-Wundef",
    "-Wunused-template",
    "-Wno-unused-command-line-argument",
    "-Wno-attributes",
    "-Wno-pessimizing-move",
    "-Wno-noexcept-type",
    "-Wno-aligned-allocation-unavailable",
    "-Wno-atomic-alignment",
    "-Wno-reserved-module-identifier",
    '-Wdeprecated-copy',
    '-Wdeprecated-copy-dtor',
    # GCC warns about places where we might want to add sized allocation/deallocation
    # functions, but we know better what we're doing/testing in the test suite.
    "-Wno-sized-deallocation",
    # Turn off warnings about user-defined literals with reserved suffixes. Those are
    # just noise since we are testing the Standard Library itself.
    "-Wno-literal-suffix",  # GCC
    "-Wno-user-defined-literals",  # Clang
    # GCC warns about this when TEST_IS_CONSTANT_EVALUATED is used on a non-constexpr
    # function. (This mostly happens in C++11 mode.)
    # TODO(mordante) investigate a solution for this issue.
    "-Wno-tautological-compare",
    # -Wstringop-overread and -Wstringop-overflow seem to be a bit buggy currently
    "-Wno-stringop-overread",
    "-Wno-stringop-overflow",
    # These warnings should be enabled in order to support the MSVC
    # team using the test suite; They enable the warnings below and
    # expect the test suite to be clean.
    "-Wsign-compare",
    "-Wunused-variable",
    "-Wunused-parameter",
    "-Wunreachable-code",
    "-Wno-unused-local-typedef",

    # Disable warnings for extensions used in C++03
    "-Wno-local-type-template-args",
    "-Wno-c++11-extensions",

    # TODO(philnik) This fails with the PSTL.
    "-Wno-unknown-pragmas",
    # Don't fail compilation in case the compiler fails to perform the requested
    # loop vectorization.
    "-Wno-pass-failed",

    # TODO: Find out why GCC warns in lots of places (is this a problem with always_inline?)
    "-Wno-dangling-reference",
    "-Wno-mismatched-new-delete",
    "-Wno-redundant-move",

    # This doesn't make sense in real code, but we have to test it because the standard requires us to not break
    "-Wno-self-move",
]

_allStandards = ["c++03", "c++11", "c++14", "c++17", "c++20", "c++23", "c++26"]


def getStdFlag(cfg, std):
    if hasCompileFlag(cfg, "-std=" + std):
        return "-std=" + std
    # TODO(LLVM-19) Remove the fallbacks needed for Clang 16.
    fallbacks = {
        "c++23": "c++2b",
    }
    if std in fallbacks and hasCompileFlag(cfg, "-std=" + fallbacks[std]):
        return "-std=" + fallbacks[std]
    return None


def getDefaultStdValue(cfg):
    viable = [s for s in reversed(_allStandards) if getStdFlag(cfg, s)]

    if not viable:
        raise RuntimeError(
            "Unable to successfully detect the presence of any -std=c++NN flag. This likely indicates an issue with your compiler."
        )

    return viable[0]


def getSpeedOptimizationFlag(cfg):
    if _isClang(cfg) or _isAppleClang(cfg) or _isGCC(cfg):
        return "-O3"
    elif _isMSVC(cfg):
        return "/O2"
    else:
        raise RuntimeError(
            "Can't figure out what compiler is used in the configuration"
        )


def getSizeOptimizationFlag(cfg):
    if _isClang(cfg) or _isAppleClang(cfg) or _isGCC(cfg):
        return "-Os"
    elif _isMSVC(cfg):
        return "/O1"
    else:
        raise RuntimeError(
            "Can't figure out what compiler is used in the configuration"
        )


def testClangTidy(cfg, version, executable):
    try:
        if version in commandOutput(cfg, [f"{executable} --version"]):
            return executable
    except ConfigurationRuntimeError:
        return None


def getSuitableClangTidy(cfg):
    # If we didn't build the libcxx-tidy plugin via CMake, we can't run the clang-tidy tests.
    if (
        runScriptExitCode(
            cfg, ["stat %{test-tools-dir}/clang_tidy_checks/libcxx-tidy.plugin"]
        )
        != 0
    ):
        return None

    version = "{__clang_major__}.{__clang_minor__}.{__clang_patchlevel__}".format(
        **compilerMacros(cfg)
    )
    exe = testClangTidy(
        cfg, version, "clang-tidy-{__clang_major__}".format(**compilerMacros(cfg))
    )

    if not exe:
        exe = testClangTidy(cfg, version, "clang-tidy")

    return exe


# fmt: off
DEFAULT_PARAMETERS = [
    Parameter(
        name="compiler",
        type=str,
        help="The path of the compiler to use for testing.",
        actions=lambda cxx: [
            AddSubstitution("%{cxx}", shlex.quote(cxx)),
        ],
    ),
    Parameter(
        name="target_triple",
        type=str,
        help="The target triple to compile the test suite for. This must be "
        "compatible with the target that the tests will be run on.",
        actions=lambda triple: filter(
            None,
            [
                AddFeature("target={}".format(triple)),
                AddFlagIfSupported("--target={}".format(triple)),
                AddSubstitution("%{triple}", triple),
            ],
        ),
    ),
    Parameter(
        name="std",
        choices=_allStandards,
        type=str,
        help="The version of the standard to compile the test suite with.",
        default=lambda cfg: getDefaultStdValue(cfg),
        actions=lambda std: [
            AddFeature(std),
            AddSubstitution("%{cxx_std}", re.sub(r"\+", "x", std)),
            AddCompileFlag(lambda cfg: getStdFlag(cfg, std)),
        ],
    ),
    Parameter(
        name="optimization",
        choices=["none", "speed", "size"],
        type=str,
        help="The optimization level to use when compiling the test suite.",
        default="none",
        actions=lambda opt: filter(None, [
            AddCompileFlag(lambda cfg: getSpeedOptimizationFlag(cfg)) if opt == "speed" else None,
            AddCompileFlag(lambda cfg: getSizeOptimizationFlag(cfg)) if opt == "size" else None,
            AddFeature(f'optimization={opt}'),
        ]),
    ),
    Parameter(
        name="enable_modules",
        choices=["none", "clang", "clang-lsv"],
        type=str,
        help="Whether to build the test suite with modules enabled. "
             "Select `clang` for Clang modules, and 'clang-lsv' for Clang modules with Local Submodule Visibility.",
        default="none",
        actions=lambda modules: filter(None, [
            AddFeature("clang-modules-build")           if modules in ("clang", "clang-lsv") else None,

            # Note: AppleClang disregards -fmodules entirely when compiling C++, so we also pass -fcxx-modules
            #       to enable modules for C++.
            AddCompileFlag("-fmodules -fcxx-modules")   if modules in ("clang", "clang-lsv") else None,

            # Note: We use a custom modules cache path to make sure that we don't reuse
            #       the default one, which can be shared across CI builds with different
            #       configurations.
            AddCompileFlag(lambda cfg: f"-fmodules-cache-path={cfg.test_exec_root}/ModuleCache") if modules in ("clang", "clang-lsv") else None,

            AddCompileFlag("-Xclang -fmodules-local-submodule-visibility") if modules == "clang-lsv" else None,
        ])
    ),
    Parameter(
        name="enable_exceptions",
        choices=[True, False],
        type=bool,
        default=True,
        help="Whether to enable exceptions when compiling the test suite.",
        actions=lambda exceptions: [] if exceptions else [
            AddFeature("no-exceptions"),
            AddCompileFlag("-fno-exceptions")
        ],
    ),
    Parameter(
        name="enable_rtti",
        choices=[True, False],
        type=bool,
        default=True,
        help="Whether to enable RTTI when compiling the test suite.",
        actions=lambda rtti: [] if rtti else [
            AddFeature("no-rtti"),
            AddCompileFlag("-fno-rtti")
        ],
    ),
    Parameter(
        name="stdlib",
        choices=["llvm-libc++", "apple-libc++", "libstdc++", "msvc"],
        type=str,
        default="llvm-libc++",
        help="""The C++ Standard Library implementation being tested.

                 Note that this parameter can also be used to encode different 'flavors' of the same
                 standard library, such as libc++ as shipped by a different vendor, if it has different
                 properties worth testing.

                 The Standard libraries currently supported are:
                 - llvm-libc++: The 'upstream' libc++ as shipped with LLVM.
                 - apple-libc++: libc++ as shipped by Apple. This is basically like the LLVM one, but
                                 there are a few differences like installation paths, the use of
                                 universal dylibs and the existence of availability markup.
                 - libstdc++: The GNU C++ library typically shipped with GCC.
                 - msvc: The Microsoft implementation of the C++ Standard Library.
                """,
        actions=lambda stdlib: filter(
            None,
            [
                AddFeature("stdlib={}".format(stdlib)),
                # Also add an umbrella feature 'stdlib=libc++' for all flavors of libc++, to simplify
                # the test suite.
                AddFeature("stdlib=libc++") if re.match(r".+-libc\+\+", stdlib) else None,
            ],
        ),
    ),
    Parameter(
        name="using_system_stdlib",
        choices=[True, False],
        type=bool,
        default=False,
        help="""Whether the Standard Library being tested is the one that shipped with the system by default.

                This is different from the 'stdlib' parameter, which describes the flavor of libc++ being
                tested. 'using_system_stdlib' describes whether the target system passed with 'target_triple'
                also corresponds to the version of the library being tested.
             """,
        actions=lambda is_system: [AddFeature("stdlib=system")] if is_system else [],
    ),
    Parameter(
        name="enable_warnings",
        choices=[True, False],
        type=bool,
        default=True,
        help="Whether to enable warnings when compiling the test suite.",
        actions=lambda warnings: [] if not warnings else
            [AddOptionalWarningFlag(w) for w in _warningFlags] +
            [AddCompileFlag("-D_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER")],
    ),
    Parameter(
        name="use_sanitizer",
        choices=[
            "",
            "Address",
            "HWAddress",
            "Undefined",
            "Memory",
            "MemoryWithOrigins",
            "Thread",
            "DataFlow",
            "Leaks",
        ],
        type=str,
        default="",
        help="An optional sanitizer to enable when building and running the test suite.",
        actions=lambda sanitizer: filter(
            None,
            [
                AddFlag("-g -fno-omit-frame-pointer") if sanitizer else None,

                AddFlag("-fsanitize=undefined -fno-sanitize=float-divide-by-zero -fno-sanitize-recover=all") if sanitizer == "Undefined" else None,
                AddFeature("ubsan")                                                                          if sanitizer == "Undefined" else None,

                AddFlag("-fsanitize=address") if sanitizer == "Address" else None,
                AddFeature("asan")            if sanitizer == "Address" else None,

                AddFlag("-fsanitize=hwaddress") if sanitizer == "HWAddress" else None,
                AddFeature("hwasan")            if sanitizer == "HWAddress" else None,

                AddFlag("-fsanitize=memory")               if sanitizer in ["Memory", "MemoryWithOrigins"] else None,
                AddFeature("msan")                         if sanitizer in ["Memory", "MemoryWithOrigins"] else None,
                AddFlag("-fsanitize-memory-track-origins") if sanitizer == "MemoryWithOrigins" else None,

                AddFlag("-fsanitize=thread") if sanitizer == "Thread" else None,
                AddFeature("tsan")           if sanitizer == "Thread" else None,

                AddFlag("-fsanitize=dataflow") if sanitizer == "DataFlow" else None,
                AddFlag("-fsanitize=leaks")    if sanitizer == "Leaks" else None,

                AddFeature("sanitizer-new-delete") if sanitizer in ["Address", "HWAddress", "Memory", "MemoryWithOrigins", "Thread"] else None,
                AddFeature("lsan") if sanitizer in ["Address", "HWAddress", "Leaks"] else None,
            ]
        )
    ),
    Parameter(
        name="enable_experimental",
        choices=[True, False],
        type=bool,
        default=True,
        help="Whether to enable tests for experimental C++ Library features.",
        actions=lambda experimental: [
            # When linking in MSVC mode via the Clang driver, a -l<foo>
            # maps to <foo>.lib, so we need to use -llibc++experimental here
            # to make it link against the static libc++experimental.lib.
            # We can't check for the feature 'msvc' in available_features
            # as those features are added after processing parameters.
            AddFeature("c++experimental"),
            PrependLinkFlag(lambda cfg: "-llibc++experimental" if _isMSVC(cfg) else "-lc++experimental"),
            AddCompileFlag("-D_LIBCPP_ENABLE_EXPERIMENTAL"),
        ]
        if experimental
        else [
            AddFeature("libcpp-has-no-incomplete-pstl"),
            AddFeature("libcpp-has-no-experimental-tzdb"),
            AddFeature("libcpp-has-no-experimental-syncstream"),
        ],
    ),
    Parameter(
        name="long_tests",
        choices=[True, False],
        type=bool,
        default=True,
        help="Whether to enable tests that take longer to run. This can be useful when running on a very slow device.",
        actions=lambda enabled: [] if not enabled else [AddFeature("long_tests")],
    ),
    Parameter(
        name="large_tests",
        choices=[True, False],
        type=bool,
        default=True,
        help="Whether to enable tests that use a lot of memory. This can be useful when running on a device with limited amounts of memory.",
        actions=lambda enabled: [] if not enabled else [AddFeature("large_tests")],
    ),
    Parameter(
        name="hardening_mode",
        choices=["none", "fast", "extensive", "debug", "undefined"],
        type=str,
        default="undefined",
        help="Whether to enable one of the hardening modes when compiling the test suite. This is only "
        "meaningful when running the tests against libc++. By default, no hardening mode is specified "
        "so the default hardening mode of the standard library will be used (if any).",
        actions=lambda hardening_mode: filter(
            None,
            [
                AddCompileFlag("-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_NONE")      if hardening_mode == "none" else None,
                AddCompileFlag("-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST")      if hardening_mode == "fast" else None,
                AddCompileFlag("-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE") if hardening_mode == "extensive" else None,
                AddCompileFlag("-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG")     if hardening_mode == "debug" else None,
                AddFeature("libcpp-hardening-mode={}".format(hardening_mode))               if hardening_mode != "undefined" else None,
            ],
        ),
    ),
    Parameter(
        name="additional_features",
        type=list,
        default=[],
        help="A comma-delimited list of additional features that will be enabled when running the tests. "
        "This should be used sparingly since specifying ad-hoc features manually is error-prone and "
        "brittle in the long run as changes are made to the test suite.",
        actions=lambda features: [AddFeature(f) for f in features],
    ),
    Parameter(
        name="enable_transitive_includes",
        choices=[True, False],
        type=bool,
        default=True,
        help="Whether to enable backwards-compatibility transitive includes when running the tests. This "
        "is provided to ensure that the trimmed-down version of libc++ does not bit-rot in between "
        "points at which we bulk-remove transitive includes.",
        actions=lambda enabled: [] if enabled else [
            AddFeature("transitive-includes-disabled"),
            AddCompileFlag("-D_LIBCPP_REMOVE_TRANSITIVE_INCLUDES"),
        ],
    ),
    Parameter(
        name="executor",
        type=str,
        default=f"{shlex.quote(sys.executable)} {shlex.quote(str(Path(__file__).resolve().parent.parent.parent / 'run.py'))}",
        help="Custom executor to use instead of the configured default.",
        actions=lambda executor: [AddSubstitution("%{executor}", executor)],
    ),
    Parameter(
        name='clang-tidy-executable',
        type=str,
        default=lambda cfg: getSuitableClangTidy(cfg),
        help="Selects the clang-tidy executable to use.",
        actions=lambda exe: [] if exe is None else [
            AddFeature('has-clang-tidy'),
            AddSubstitution('%{clang-tidy}', exe),
        ]
    ),
]
# fmt: on
