import importlib
import os
import subprocess
import sys
import uuid
from textwrap import dedent

import pytest

from .._setup_helpers import get_compiler, get_extensions
from . import cleanup_import, run_setup

if sys.version_info >= (3, 11):
    from contextlib import chdir
else:
    from .py311_backports import chdir

extension_helpers_PATH = os.path.abspath(
    os.path.join(os.path.dirname(__file__), "..", "..")
)  # noqa


def teardown_module(module):
    # Remove file generated by test_generate_openmp_enabled_py but
    # somehow needed in test_cython_autoextensions
    tmpfile = "openmp_enabled.py"
    if os.path.exists(tmpfile):
        os.remove(tmpfile)


POSSIBLE_COMPILERS = ["unix", "msvc", "bcpp", "cygwin", "mingw32"]


def test_get_compiler():
    assert get_compiler() in POSSIBLE_COMPILERS


def _extension_test_package(
    tmp_path,
    request=None,
    extension_type="c",
    include_numpy=False,
    include_setup_py=True,
):
    """Creates a simple test package with an extension module."""

    test_pkg = tmp_path / "test_pkg"
    os.makedirs(test_pkg / "helpers_test_package")
    (test_pkg / "helpers_test_package" / "__init__.py").touch()

    # TODO: It might be later worth making this particular test package into a
    # reusable fixture for other build_ext tests

    if extension_type in ("c", "both"):
        # A minimal C extension for testing
        (test_pkg / "helpers_test_package" / "unit01.c").write_text(
            dedent(
                """\
            #include <Python.h>

            static struct PyModuleDef moduledef = {
                PyModuleDef_HEAD_INIT,
                "unit01",
                NULL,
                -1,
                NULL
            };
            PyMODINIT_FUNC
            PyInit_unit01(void) {
                return PyModule_Create(&moduledef);
            }
        """
            )
        )

    if extension_type in ("pyx", "both"):
        # A minimal Cython extension for testing
        (test_pkg / "helpers_test_package" / "unit02.pyx").write_text(
            dedent(
                """\
            print("Hello cruel angel.")
        """
            )
        )

    if extension_type == "c":
        extensions = ["unit01.c"]
    elif extension_type == "pyx":
        extensions = ["unit02.pyx"]
    elif extension_type == "both":
        extensions = ["unit01.c", "unit02.pyx"]

    include_dirs = ["numpy"] if include_numpy else []

    extensions_list = [
        f"Extension('helpers_test_package.{os.path.splitext(extension)[0]}', "
        f"[join('helpers_test_package', '{extension}')], "
        f"{include_dirs=})"
        for extension in extensions
    ]

    (test_pkg / "helpers_test_package" / "setup_package.py").write_text(
        dedent(
            """\
        from setuptools import Extension
        from os.path import join
        def get_extensions():
            return [{}]
    """.format(
                ", ".join(extensions_list)
            )
        )
    )

    if include_setup_py:
        (test_pkg / "setup.py").write_text(
            dedent(
                f"""\
            import sys
            from os.path import join
            from setuptools import setup, find_packages
            sys.path.insert(0, r'{extension_helpers_PATH}')
            from extension_helpers import get_extensions

            setup(
                name='helpers_test_package',
                version='0.1',
                packages=find_packages(),
                ext_modules=get_extensions()
            )
        """
            )
        )

    if "" in sys.path:
        sys.path.remove("")

    sys.path.insert(0, "")

    def finalize():
        cleanup_import("helpers_test_package")

    if request:
        request.addfinalizer(finalize)

    return test_pkg


@pytest.fixture
def extension_test_package(tmp_path, request):
    return _extension_test_package(tmp_path, request, extension_type="both")


@pytest.fixture
def c_extension_test_package(tmp_path, request):
    # Check whether numpy is installed in the test environment
    has_numpy = bool(importlib.util.find_spec("numpy"))
    return _extension_test_package(tmp_path, request, extension_type="c", include_numpy=has_numpy)


@pytest.fixture
def pyx_extension_test_package(tmp_path, request):
    return _extension_test_package(tmp_path, request, extension_type="pyx")


def test_cython_autoextensions(tmp_path):
    """
    Regression test for https://github.com/astropy/astropy-helpers/pull/19

    Ensures that Cython extensions in sub-packages are discovered and built
    only once.
    """

    # Make a simple test package

    test_pkg = tmp_path / "test_pkg"
    os.makedirs(test_pkg / "yoda" / "luke")
    (test_pkg / "yoda" / "__init__.py").touch()
    (test_pkg / "yoda" / "luke" / "__init__.py").touch()
    (test_pkg / "yoda" / "luke" / "dagobah.pyx").write_text("""def testfunc(): pass""")

    # Required, currently, for get_extensions to work
    ext_modules = get_extensions(str(test_pkg))

    assert len(ext_modules) == 2
    assert ext_modules[0].name == "yoda.luke.dagobah"


def test_compiler_module(capsys, c_extension_test_package):
    """
    Test ensuring that the compiler module is built and installed for packages
    that have extension modules.
    """

    test_pkg = c_extension_test_package
    install_temp = test_pkg / "install_temp"
    os.mkdir(install_temp)

    with chdir(test_pkg):
        # This is one of the simplest ways to install just a package into a
        # test directory
        run_setup(
            "setup.py",
            [
                "install",
                "--single-version-externally-managed",
                f"--install-lib={install_temp}",
                "--record={}".format(install_temp / "record.txt"),
            ],
        )

    with chdir(install_temp):
        import helpers_test_package

        # Make sure we imported the helpers_test_package package from the correct place
        dirname = os.path.abspath(os.path.dirname(helpers_test_package.__file__))
        assert dirname == str(install_temp / "helpers_test_package")

        import helpers_test_package.compiler_version

        assert helpers_test_package.compiler_version != "unknown"


@pytest.mark.parametrize("use_extension_helpers", [None, False, True])
@pytest.mark.parametrize("pyproject_use_helpers", [None, False, True])
def test_no_setup_py(tmp_path, use_extension_helpers, pyproject_use_helpers):
    """
    Test that makes sure that extension-helpers can be enabled without a
    setup.py file.
    """

    package_name = "helpers_test_package_" + str(uuid.uuid4()).replace("-", "_")

    test_pkg = tmp_path / "test_pkg"
    os.makedirs(test_pkg / package_name)
    (test_pkg / package_name / "__init__.py").touch()

    simple_c = test_pkg / package_name / "simple.c"

    simple_c.write_text(
        dedent(
            """\
        #include <Python.h>

        static struct PyModuleDef moduledef = {
            PyModuleDef_HEAD_INIT,
            "simple",
            NULL,
            -1,
            NULL
        };
        PyMODINIT_FUNC
        PyInit_simple(void) {
            return PyModule_Create(&moduledef);
        }
    """
        )
    )

    (test_pkg / package_name / "setup_package.py").write_text(
        dedent(
            f"""\
        from setuptools import Extension
        from os.path import join
        def get_extensions():
            return [Extension('{package_name}.simple', [join('{package_name}', 'simple.c')])]
        """
        )
    )

    if use_extension_helpers is None:
        (test_pkg / "setup.cfg").write_text(
            dedent(
                f"""\
            [metadata]
            name = {package_name}
            version = 0.1

            [options]
            packages = find:
        """
            )
        )
    else:
        (test_pkg / "setup.cfg").write_text(
            dedent(
                f"""\
            [metadata]
            name = {package_name}
            version = 0.1

            [options]
            packages = find:

            [extension-helpers]
            use_extension_helpers = {str(use_extension_helpers).lower()}
        """
            )
        )

    if pyproject_use_helpers is None:
        (test_pkg / "pyproject.toml").write_text(
            dedent(
                """\
            [build-system]
            requires = ["setuptools>=43.0.0",
                        "wheel"]
            build-backend = 'setuptools.build_meta'
        """
            )
        )
    else:
        (test_pkg / "pyproject.toml").write_text(
            dedent(
                f"""\
            [build-system]
            requires = ["setuptools>=43.0.0",
                        "wheel"]
            build-backend = 'setuptools.build_meta'

            [tool.extension-helpers]
            use_extension_helpers = {str(pyproject_use_helpers).lower()}
        """
            )
        )

    install_temp = test_pkg / "install_temp"
    os.mkdir(install_temp)

    with chdir(test_pkg):
        # NOTE: we disable build isolation as we need to pick up the current
        # developer version of extension-helpers
        subprocess.call(
            [
                sys.executable,
                "-m",
                "pip",
                "install",
                ".",
                "--no-build-isolation",
                f"--target={install_temp}",
            ]
        )

    if "" in sys.path:
        sys.path.remove("")

    sys.path.insert(0, "")

    with chdir(install_temp):
        importlib.import_module(package_name)

        if use_extension_helpers or (use_extension_helpers is None and pyproject_use_helpers):
            compiler_version_mod = importlib.import_module(package_name + ".compiler_version")
            assert compiler_version_mod.compiler != "unknown"
        else:
            try:
                importlib.import_module(package_name + ".compiler_version")
            except ImportError:
                pass
            else:
                raise AssertionError(package_name + ".compiler_version should not exist")


@pytest.mark.parametrize("pyproject_use_helpers", [None, False, True])
def test_only_pyproject(tmp_path, pyproject_use_helpers):
    """
    Test that makes sure that extension-helpers can be enabled without a
    setup.py and without a setup.cfg file.
    """

    pytest.importorskip("setuptools", minversion="62.0")

    package_name = "helpers_test_package_" + str(uuid.uuid4()).replace("-", "_")

    test_pkg = tmp_path / "test_pkg"
    os.makedirs(test_pkg / package_name)
    (test_pkg / package_name / "__init__.py").touch()
    simple_pyx = test_pkg / package_name / "simple.pyx"
    simple_pyx.write_text(
        dedent(
            """\
        def test():
            pass
    """
        )
    )

    if pyproject_use_helpers is None:
        extension_helpers_option = ""
    else:
        extension_helpers_option = dedent(
            f"""
        [tool.extension-helpers]
        use_extension_helpers = {str(pyproject_use_helpers).lower()}
        """
        )

    buildtime_requirements = ["setuptools>=43.0.0", "wheel", "Cython"]
    (test_pkg / "pyproject.toml").write_text(
        dedent(
            f"""\
            [project]
            name = "{package_name}"
            version = "0.1"

            [tool.setuptools.packages]
            find = {{namespaces = false}}

            [build-system]
            requires = [{', '.join(f'"{_}"' for _ in buildtime_requirements)}]
            build-backend = 'setuptools.build_meta'

            """
        )
        + extension_helpers_option
    )

    install_temp = test_pkg / "install_temp"
    os.mkdir(install_temp)

    with chdir(test_pkg):
        # NOTE: we disable build isolation as we need to pick up the current
        # developer version of extension-helpers
        # In order to do so, we need to ensure that build-time dependencies are
        # installed first
        cmd1 = [
            sys.executable,
            "-m",
            "pip",
            "install",
            *buildtime_requirements,
            f"--target={install_temp}",
        ]
        subprocess.call(cmd1)

        cmd2 = [
            sys.executable,
            "-m",
            "pip",
            "install",
            ".",
            "--no-build-isolation",
            f"--target={install_temp}",
        ]
        subprocess.call(cmd2)

    if "" in sys.path:
        sys.path.remove("")

    sys.path.insert(0, "")

    with chdir(install_temp):
        importlib.import_module(package_name)

        if pyproject_use_helpers:
            compiler_version_mod = importlib.import_module(package_name + ".compiler_version")
            assert compiler_version_mod.compiler != "unknown"
        else:
            try:
                importlib.import_module(package_name + ".compiler_version")
            except ImportError:
                pass
            else:
                raise AssertionError(package_name + ".compiler_version should not exist")


# Tests to make sure that limited API support works correctly


@pytest.mark.skip(reason="Requires Cython >= 3.1")
@pytest.mark.parametrize("config", ("setup.cfg", "pyproject.toml"))
@pytest.mark.parametrize("envvar", (False, True))
@pytest.mark.parametrize("limited_api", (None, "cp310"))
@pytest.mark.parametrize("extension_type", ("c", "pyx", "both"))
def test_limited_api(tmp_path, config, envvar, limited_api, extension_type):

    if sys.version_info < (3, 11):
        pytest.skip(
            "This test requires setuptools>=65.4 which is only available for Python 3.11 and later"
        )

    package = _extension_test_package(
        tmp_path, extension_type=extension_type, include_numpy=True, include_setup_py=False
    )

    if config == "setup.cfg":

        setup_cfg = dedent(
            """\
            [metadata]
            name = helpers_test_package
            version = 0.1

            [options]
            packages = find:

            [extension-helpers]
            use_extension_helpers = true
        """
        )

        if limited_api and not envvar:
            setup_cfg += f"\n[bdist_wheel]\npy_limited_api={limited_api}"
        elif envvar:
            # Make sure if we are using the environment variable that it takes
            # precedence over this setting (this only works for setup.cfg)
            setup_cfg += "\n[bdist_wheel]\npy_limited_api=cp35"

        (package / "setup.cfg").write_text(setup_cfg)

        # Still require a minimal pyproject.toml file if no setup.py file

        (package / "pyproject.toml").write_text(
            dedent(
                """
            [build-system]
            requires = ["setuptools>=43.0.0",
                        "wheel"]
            build-backend = 'setuptools.build_meta'

            [tool.extension-helpers]
            use_extension_helpers = true
        """
            )
        )

    elif config == "pyproject.toml":

        pyproject_toml = dedent(
            """\
            [build-system]
            requires = ["setuptools>=43.0.0",
                        "wheel"]
            build-backend = 'setuptools.build_meta'

            [project]
            name = "helpers_test_package"
            version = "0.1"

            [tool.setuptools.packages]
            find = {namespaces = false}

            [tool.extension-helpers]
            use_extension_helpers = true
            """
        )

        if limited_api and not envvar:
            pyproject_toml += f'\n[tool.distutils.bdist_wheel]\npy-limited-api = "{limited_api}"'

        (package / "pyproject.toml").write_text(pyproject_toml)

    env = os.environ.copy()

    if envvar:
        if limited_api:
            env["EXTENSION_HELPERS_PY_LIMITED_API"] = limited_api
        else:
            env["EXTENSION_HELPERS_PY_LIMITED_API"] = ""

    with chdir(package):
        subprocess.run(
            [sys.executable, "-m", "build", "--wheel", "--no-isolation"], env=env, check=True
        )

    wheels = os.listdir(package / "dist")

    assert len(wheels) == 1
    assert ("abi3" in wheels[0]) == (limited_api is not None)


@pytest.mark.skip(reason="Requires Cython >= 3.1")
def test_limited_api_invalid_abi(tmp_path, capsys):

    package = _extension_test_package(
        tmp_path, extension_type="c", include_numpy=True, include_setup_py=False
    )

    (package / "setup.cfg").write_text(
        dedent(
            """\
        [metadata]
        name = helpers_test_package
        version = 0.1

        [options]
        packages = find:

        [extension-helpers]
        use_extension_helpers = true

        [bdist_wheel]
        py_limited_api=invalid
    """
        )
    )

    (package / "pyproject.toml").write_text(
        dedent(
            """
    [build-system]
    requires = ["setuptools>=43.0.0",
                "wheel"]
    build-backend = 'setuptools.build_meta'
    """
        )
    )

    with chdir(package):
        result = subprocess.run(
            [sys.executable, "-m", "build", "--wheel", "--no-isolation"], stderr=subprocess.PIPE
        )

    assert result.stderr.strip().endswith(
        b"ValueError: Unrecognized abi version for limited API: invalid"
    )
