# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt

"""Determine facts about the environment."""

from __future__ import annotations

import os
import platform
import sys
import sysconfig
from collections.abc import Iterable
from typing import Any, Final

# debug_info() at the bottom wants to show all the globals, but not imports.
# Grab the global names here to know which names to not show. Nothing defined
# above this line will be in the output.
_UNINTERESTING_GLOBALS = list(globals())
# These names also shouldn't be shown.
_UNINTERESTING_GLOBALS += ["PYBEHAVIOR", "debug_info"]

# Operating systems.
WINDOWS = sys.platform == "win32"
LINUX = sys.platform.startswith("linux")
MACOS = sys.platform == "darwin"

# Python implementations.
CPYTHON = (platform.python_implementation() == "CPython")  # fmt: skip
PYPY = (platform.python_implementation() == "PyPy")  # fmt: skip

# Python versions. We amend version_info with one more value, a zero if an
# official version, or 1 if built from source beyond an official version.
# Only use sys.version_info directly where tools like mypy need it to understand
# version-specfic code, otherwise use PYVERSION.
PYVERSION = sys.version_info + (int(platform.python_version()[-1] == "+"),)

if PYPY:
    # Minimum now is 7.3.16
    PYPYVERSION = tuple(sys.pypy_version_info)  # type: ignore[attr-defined]
else:
    PYPYVERSION = (0,)

# Do we have a GIL?
GIL = getattr(sys, "_is_gil_enabled", lambda: True)()

# Is this a free-threaded build of CPython? Checks the build, not runtime GIL
# state, since the GIL can be re-enabled at runtime on a free-threaded build.
FREE_THREADED = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))

# Do we ship compiled coveragepy wheels for this version?
SHIPPING_WHEELS = (
    CPYTHON and PYVERSION[:2] <= (3, 15) and not (FREE_THREADED and PYVERSION[:2] == (3, 13))
)

# Should we default to sys.monitoring?
SYSMON_DEFAULT = CPYTHON and PYVERSION >= (3, 14)


# Python behavior.
class PYBEHAVIOR:
    """Flags indicating this Python's behavior."""

    # When leaving a with-block, do we visit the with-line exactly,
    # or the context managers in inner-out order?
    #
    # mwith.py:
    #    with (
    #        open("/tmp/one", "w") as f2,
    #        open("/tmp/two", "w") as f3,
    #        open("/tmp/three", "w") as f4,
    #    ):
    #        print("hello 6")
    #
    # % python3.11 -m trace -t mwith.py | grep mwith
    #  --- modulename: mwith, funcname: <module>
    # mwith.py(2):     open("/tmp/one", "w") as f2,
    # mwith.py(1): with (
    # mwith.py(2):     open("/tmp/one", "w") as f2,
    # mwith.py(3):     open("/tmp/two", "w") as f3,
    # mwith.py(1): with (
    # mwith.py(3):     open("/tmp/two", "w") as f3,
    # mwith.py(4):     open("/tmp/three", "w") as f4,
    # mwith.py(1): with (
    # mwith.py(4):     open("/tmp/three", "w") as f4,
    # mwith.py(6):     print("hello 6")
    # mwith.py(1): with (
    #
    # % python3.12 -m trace -t mwith.py | grep mwith
    #  --- modulename: mwith, funcname: <module>
    # mwith.py(2):      open("/tmp/one", "w") as f2,
    # mwith.py(3):      open("/tmp/two", "w") as f3,
    # mwith.py(4):      open("/tmp/three", "w") as f4,
    # mwith.py(6):      print("hello 6")
    # mwith.py(4):      open("/tmp/three", "w") as f4,
    # mwith.py(3):      open("/tmp/two", "w") as f3,
    # mwith.py(2):      open("/tmp/one", "w") as f2,

    exit_with_through_ctxmgr = (PYVERSION >= (3, 12, 6))  # fmt: skip

    # f-strings are parsed as code, pep 701
    fstring_syntax = (PYVERSION >= (3, 12))  # fmt: skip

    # PEP669 Low Impact Monitoring: https://peps.python.org/pep-0669/
    pep669: Final[bool] = bool(getattr(sys, "monitoring", None))

    # Where does frame.f_lasti point when yielding from a generator?
    # It used to point at the YIELD, in 3.13 it points at the RESUME,
    # then it went back to the YIELD.
    # https://github.com/python/cpython/issues/113728
    lasti_is_yield = (PYVERSION[:2] != (3, 13))  # fmt: skip

    # PEP649 and PEP749: Deferred annotations
    deferred_annotations = (PYVERSION >= (3, 14))  # fmt: skip

    # Does sys.monitoring support BRANCH_RIGHT and BRANCH_LEFT?  The names
    # were added in early 3.14 alphas, but didn't work entirely correctly until
    # after 3.14.0a5.
    branch_right_left = pep669 and (PYVERSION > (3, 14, 0, "alpha", 5, 0))


# Coverage.py specifics, about testing scenarios. See tests/testenv.py also.

# Are we coverage-measuring ourselves?
METACOV = os.getenv("COVERAGE_COVERAGE") is not None

# Are we running our test suite?
# Even when running tests, you can use COVERAGE_TESTING=0 to disable the
# test-specific behavior like AST checking.
TESTING = os.getenv("COVERAGE_TESTING") == "True"


def debug_info() -> Iterable[tuple[str, Any]]:
    """Return a list of (name, value) pairs for printing debug information."""
    info = [
        (name, value)
        for name, value in globals().items()
        if not name.startswith("_") and name not in _UNINTERESTING_GLOBALS
    ]
    info += [
        (name, value) for name, value in PYBEHAVIOR.__dict__.items() if not name.startswith("_")
    ]
    return sorted(info)
