"""test_setup
----------------------------------

Tests for `skbuild.setup` function.
"""

from __future__ import annotations

import os
import pathlib
import pprint
import sys
import tempfile
import textwrap
from collections.abc import Sequence
from unittest.mock import patch

import pytest
from setuptools import Distribution as setuptool_Distribution
from distutils.core import Distribution as distutils_Distribution

from skbuild import setup as skbuild_setup
from skbuild.constants import CMAKE_INSTALL_DIR, SKBUILD_DIR
from skbuild.exceptions import SKBuildError
from skbuild.platform_specifics import get_platform
from skbuild.setuptools_wrap import strip_package
from skbuild.utils import push_dir, to_platform_path

from . import (
    _tmpdir,
    execute_setup_py,
    initialize_git_repo_and_commit,
    is_site_reachable,
    push_argv,
)


@pytest.mark.parametrize("distribution_type", ["unknown", "py_modules", "packages", "skbuild"])
def test_distribution_is_pure(distribution_type, tmp_path):
    skbuild_setup_kwargs = {}

    if distribution_type == "unknown":
        is_pure = False

    elif distribution_type == "py_modules":
        is_pure = True
        hello_py = tmp_path / "hello.py"
        hello_py.write_text("")
        skbuild_setup_kwargs["py_modules"] = ["hello"]

    elif distribution_type == "packages":
        is_pure = True
        package_dir = tmp_path / "hello"
        package_dir.mkdir()
        init_py = package_dir / "__init__.py"
        init_py.write_text("")
        skbuild_setup_kwargs["packages"] = ["hello"]

    elif distribution_type == "skbuild":
        is_pure = False
        cmakelists_txt = tmp_path / "CMakeLists.txt"
        cmakelists_txt.write_text(
            """
            cmake_minimum_required(VERSION 3.5...3.26)
            project(test NONE)
            install(CODE "execute_process(
              COMMAND \\${CMAKE_COMMAND} -E sleep 0)")
            """
        )
    else:
        msg = f"Unknown distribution_type: {distribution_type}"
        raise Exception(msg)

    platform = get_platform()
    original_write_test_cmakelist = platform.write_test_cmakelist

    def write_test_cmakelist_no_languages(_self, _languages):
        original_write_test_cmakelist([])

    with patch.object(type(platform), "write_test_cmakelist", new=write_test_cmakelist_no_languages):
        with push_dir(str(tmp_path)), push_argv(["setup.py", "build"]):
            distribution = skbuild_setup(
                name="test",
                version="0.0.1",
                description="test object returned by setup function",
                author="The scikit-build team",
                license="MIT",
                **skbuild_setup_kwargs,  # type: ignore[arg-type]
            )
            assert issubclass(distribution.__class__, (distutils_Distribution, setuptool_Distribution))
            assert is_pure == distribution.is_pure()


@pytest.mark.parametrize("cmake_args", [[], ["--", "-DVAR:STRING=43", "-DVAR_WITH_SPACE:STRING=Ciao Mondo"]])
def test_cmake_args_keyword(cmake_args, capfd):
    tmp_dir = _tmpdir("cmake_args_keyword")

    (tmp_dir / "setup.py").write_text(
        textwrap.dedent(
            """
        from skbuild import setup
        setup(
            name="test_cmake_args_keyword",
            version="1.2.3",
            description="a minimal example package",
            author='The scikit-build team',
            license="MIT",
            cmake_args=[
                "-DVAR:STRING=42",
                "-DVAR_WITH_SPACE:STRING=Hello World"
            ]

        )
        """
        )
    )
    (tmp_dir / "CMakeLists.txt").write_text(
        textwrap.dedent(
            """
        cmake_minimum_required(VERSION 3.5...3.26)
        project(test NONE)
        message(STATUS "VAR[${VAR}]")
        message(STATUS "VAR_WITH_SPACE[${VAR_WITH_SPACE}]")
        install(CODE "execute_process(
          COMMAND \\${CMAKE_COMMAND} -E sleep 0)")
        """
        )
    )

    with execute_setup_py(tmp_dir, ["build", *cmake_args], disable_languages_test=True):
        pass

    out, _ = capfd.readouterr()

    if not cmake_args:
        assert "VAR[42]" in out
        assert "VAR_WITH_SPACE[Hello World]" in out
    else:
        assert "VAR[43]" in out
        assert "VAR_WITH_SPACE[Ciao Mondo]" in out


@pytest.mark.parametrize(
    ("cmake_install_dir", "expected_failed", "error_code_type"),
    [
        (None, True, str),
        ("", True, str),
        (str(pathlib.Path(tempfile.gettempdir()) / "scikit-build"), True, SKBuildError),
        ("banana", False, str),
    ],
)
def test_cmake_install_dir_keyword(cmake_install_dir, expected_failed, error_code_type, capsys, caplog):
    # -------------------------------------------------------------------------
    # "SOURCE" tree layout:
    #
    # ROOT/
    #
    #     CMakeLists.txt
    #     setup.py
    #
    #     apple/
    #         __init__.py
    #
    # -------------------------------------------------------------------------
    # "BINARY" distribution layout
    #
    # ROOT/
    #
    #     apple/
    #         __init__.py
    #

    tmp_dir = _tmpdir("cmake_install_dir_keyword")

    setup_kwarg = ""
    if cmake_install_dir is not None:
        setup_kwarg = f"cmake_install_dir={str(cmake_install_dir)!r}"

    (tmp_dir / "setup.py").write_text(
        textwrap.dedent(
            f"""
        from skbuild import setup
        setup(
            name="test_cmake_install_dir",
            version="1.2.3",
            description="a package testing use of cmake_install_dir",
            author='The scikit-build team',
            license="MIT",
            packages=['apple', 'banana'],
            {setup_kwarg}
        )
        """
        )
    )

    # Install location purposely set to "." so that we can test
    # usage of "cmake_install_dir" skbuild.setup keyword.
    (tmp_dir / "CMakeLists.txt").write_text(
        textwrap.dedent(
            """
        cmake_minimum_required(VERSION 3.5...3.26)
        project(banana NONE)
        file(WRITE "${CMAKE_BINARY_DIR}/__init__.py" "")
        install(FILES "${CMAKE_BINARY_DIR}/__init__.py" DESTINATION ".")
        """
        )
    )

    apple_init = tmp_dir / "apple" / "__init__.py"
    apple_init.parent.mkdir(parents=True, exist_ok=True)
    apple_init.touch(exist_ok=True)

    failed = False
    message = ""
    try:
        with execute_setup_py(tmp_dir, ["build"], disable_languages_test=True):
            pass
    except SystemExit as e:
        # Error is not of type SKBuildError, it is expected to be
        # raised by distutils.core.setup
        failed = isinstance(e.code, error_code_type)
        message = str(e)

    out, _ = capsys.readouterr()
    out += caplog.text

    assert failed == expected_failed
    if failed:
        if error_code_type is str:
            assert message == "error: package directory '{}' does not exist".format(
                os.path.join(CMAKE_INSTALL_DIR(), "banana")
            )
        else:
            assert message.strip().startswith("setup parameter 'cmake_install_dir' is set to an absolute path.")
    else:
        init_py = to_platform_path(f"{CMAKE_INSTALL_DIR()}/banana/__init__.py")
        assert f"copying {init_py}" in out


@pytest.mark.parametrize("cmake_with_sdist", [True, False])
def test_cmake_with_sdist_keyword(cmake_with_sdist, capfd):
    tmp_dir = _tmpdir("cmake_with_sdist")

    (tmp_dir / "setup.py").write_text(
        textwrap.dedent(
            f"""
        from skbuild import setup
        setup(
            name="cmake_with_sdist_keyword",
            version="1.2.3",
            description="a minimal example package",
            author='The scikit-build team',
            license="MIT",
            cmake_with_sdist={cmake_with_sdist}
        )
        """
        )
    )
    (tmp_dir / "CMakeLists.txt").write_text(
        textwrap.dedent(
            """
        cmake_minimum_required(VERSION 3.5...3.26)
        project(test NONE)
        install(CODE "execute_process(
          COMMAND \\${CMAKE_COMMAND} -E sleep 0)")
        """
        )
    )

    initialize_git_repo_and_commit(tmp_dir)

    with execute_setup_py(tmp_dir, ["sdist"], disable_languages_test=True):
        pass

    out, _ = capfd.readouterr()

    if cmake_with_sdist:
        assert "Generating done" in out
    else:
        assert "Generating done" not in out


def test_cmake_minimum_required_version_keyword():
    tmp_dir = _tmpdir("cmake_minimum_required_version")

    (tmp_dir / "setup.py").write_text(
        textwrap.dedent(
            """
        from skbuild import setup
        setup(
            name="cmake_with_sdist_keyword",
            version="1.2.3",
            description="a minimal example package",
            author='The scikit-build team',
            license="MIT",
            cmake_minimum_required_version='99.98.97'
        )
        """
        )
    )
    (tmp_dir / "CMakeLists.txt").write_text(
        textwrap.dedent(
            """
        cmake_minimum_required(VERSION 3.5...3.26)
        project(test NONE)
        install(CODE "execute_process(
          COMMAND \\${CMAKE_COMMAND} -E sleep 0)")
        """
        )
    )

    try:
        with execute_setup_py(tmp_dir, ["build"], disable_languages_test=True):
            pass
    except SystemExit as e:
        # Error is not of type SKBuildError, it is expected to be
        # raised by distutils.core.setup
        failed = isinstance(e.code, SKBuildError)
        message = str(e)
        assert failed
        assert "CMake version 99.98.97 or higher is required." in message


@pytest.mark.deprecated
@pytest.mark.filterwarnings("ignore:setuptools.installer is deprecated:Warning")
@pytest.mark.skipif(
    os.environ.get("CONDA_BUILD", "0") == "1",
    reason="running tests expecting network connection in Conda is not possible. "
    "See https://github.com/conda/conda/issues/508",
)
@pytest.mark.skipif(not is_site_reachable("https://pypi.org/simple/cmake/"), reason="pypi.org website not reachable")
@pytest.mark.xfail(
    sys.platform.startswith("cygwin"), strict=False, reason="Cygwin needs a release of scikit-build first"
)
def test_setup_requires_keyword_include_cmake(mocker, capsys):
    mock_setup = mocker.patch("skbuild.setuptools_wrap.setuptools.setup")

    tmp_dir = _tmpdir("setup_requires_keyword_include_cmake")

    setup_requires = ["cmake>=3.10"]

    (tmp_dir / "setup.py").write_text(
        textwrap.dedent(
            """
        from skbuild import setup
        setup(
            name="cmake_with_sdist_keyword",
            version="1.2.3",
            description="a minimal example package",
            author='The scikit-build team',
            license="MIT",
            setup_requires=[{setup_requires}]
        )
        """.format(setup_requires=",".join([f"'{package}'" for package in setup_requires]))
        )
    )
    (tmp_dir / "CMakeLists.txt").write_text(
        textwrap.dedent(
            """
        cmake_minimum_required(VERSION 3.5...3.26)
        project(test NONE)
        install(CODE "execute_process(
          COMMAND \\${CMAKE_COMMAND} -E sleep 0)")
        """
        )
    )

    with execute_setup_py(tmp_dir, ["build"], disable_languages_test=True):
        assert mock_setup.call_count == 1
        setup_kw = mock_setup.call_args[1]
        assert setup_kw["setup_requires"] == setup_requires

        import cmake

        out, _ = capsys.readouterr()
        if "Searching for cmake>=3.10" in out:
            assert cmake.__file__.lower().startswith(str(tmp_dir).lower())


@pytest.mark.parametrize("distribution_type", ["pure", "skbuild"])
def test_script_keyword(distribution_type, capsys, caplog):
    # -------------------------------------------------------------------------
    #
    # "SOURCE" tree layout for "pure" distribution:
    #
    # ROOT/
    #     setup.py
    #     foo.py
    #     bar.py
    #
    # "SOURCE" tree layout for "pure" distribution:
    #
    # ROOT/
    #     setup.py
    #     CMakeLists.txt
    #
    # -------------------------------------------------------------------------
    # "BINARY" distribution layout is identical for both
    #
    # ROOT/
    #     foo.py
    #     bar.py
    #

    tmp_dir = _tmpdir("script_keyword")

    (tmp_dir / "setup.py").write_text(
        textwrap.dedent(
            """
        from skbuild import setup
        setup(
            name="test_script_keyword",
            version="1.2.3",
            description="a package testing use of script keyword",
            author='The scikit-build team',
            license="MIT",
            scripts=['foo.py', 'bar.py'],
            packages=[],
        )
        """
        )
    )

    if distribution_type == "skbuild":
        (tmp_dir / "CMakeLists.txt").write_text(
            textwrap.dedent(
                """
            cmake_minimum_required(VERSION 3.5...3.26)
            project(foo NONE)
            file(WRITE "${CMAKE_BINARY_DIR}/foo.py" "# foo.py")
            file(WRITE "${CMAKE_BINARY_DIR}/bar.py" "# bar.py")
            install(
                FILES
                    "${CMAKE_BINARY_DIR}/foo.py"
                    "${CMAKE_BINARY_DIR}/bar.py"
                DESTINATION "."
                )
            """
            )
        )

        messages = [
            f"copying {CMAKE_INSTALL_DIR()}/{module}.py -> {SKBUILD_DIR()}/setuptools/scripts-"
            for module in ["foo", "bar"]
        ]

    elif distribution_type == "pure":
        (tmp_dir / "foo.py").write_text("# foo.py")
        (tmp_dir / "bar.py").write_text("# bar.py")

        messages = [f"copying {module}.py -> {SKBUILD_DIR()}/setuptools/scripts-" for module in ["foo", "bar"]]

    with execute_setup_py(tmp_dir, ["build"], disable_languages_test=True):
        pass

    out, _ = capsys.readouterr()
    out += caplog.text
    for message in messages:
        assert to_platform_path(message) in out


@pytest.mark.parametrize("distribution_type", ["pure", "skbuild"])
def test_py_modules_keyword(distribution_type, capsys, caplog):
    # -------------------------------------------------------------------------
    #
    # "SOURCE" tree layout for "pure" distribution:
    #
    # ROOT/
    #     setup.py
    #     foo.py
    #     bar.py
    #
    # "SOURCE" tree layout for "skbuild" distribution:
    #
    # ROOT/
    #     setup.py
    #     CMakeLists.txt
    #
    # -------------------------------------------------------------------------
    # "BINARY" distribution layout is identical for both
    #
    # ROOT/
    #     foo.py
    #     bar.py
    #

    tmp_dir = _tmpdir("py_modules_keyword")

    (tmp_dir / "setup.py").write_text(
        textwrap.dedent(
            """
        from skbuild import setup
        setup(
            name="test_py_modules_keyword",
            version="1.2.3",
            description="a package testing use of py_modules keyword",
            author='The scikit-build team',
            license="MIT",
            py_modules=['foo', 'bar']
        )
        """
        )
    )

    if distribution_type == "skbuild":
        (tmp_dir / "CMakeLists.txt").write_text(
            textwrap.dedent(
                """
            cmake_minimum_required(VERSION 3.5...3.26)
            project(foobar NONE)
            file(WRITE "${CMAKE_BINARY_DIR}/foo.py" "# foo.py")
            file(WRITE "${CMAKE_BINARY_DIR}/bar.py" "# bar.py")
            install(
                FILES
                    "${CMAKE_BINARY_DIR}/foo.py"
                    "${CMAKE_BINARY_DIR}/bar.py"
                DESTINATION "."
                )
            """
            )
        )

        messages = [
            f"copying {CMAKE_INSTALL_DIR()}/{module}.py -> {SKBUILD_DIR()}/setuptools/lib" for module in ["foo", "bar"]
        ]

    elif distribution_type == "pure":
        (tmp_dir / "foo.py").write_text("# foo.py")
        (tmp_dir / "bar.py").write_text("# bar.py")

        messages = [f"copying {module}.py -> {SKBUILD_DIR()}/setuptools/lib" for module in ["foo", "bar"]]

    with execute_setup_py(tmp_dir, ["build"], disable_languages_test=True):
        pass

    out, _ = capsys.readouterr()
    out += caplog.text
    for message in messages:
        assert to_platform_path(message) in out


@pytest.mark.parametrize(
    ("package_parts", "module_file", "expected"),
    [
        ([], "", ""),
        ([""], "file.py", "file.py"),
        ([], "foo/file.py", "foo/file.py"),
        (["foo"], "", ""),
        (["foo"], "foo/file.py", "file.py"),
        (["foo"], "foo\\file.py", "file.py"),
        (["foo", "bar"], "foo/file.py", "foo/file.py"),
        (["foo", "bar"], "foo/bar/file.py", "file.py"),
        (["foo", "bar"], "foo/bar/baz/file.py", "baz/file.py"),
        (["foo"], "/foo/file.py", "/foo/file.py"),
    ],
)
def test_strip_package(package_parts, module_file, expected):
    assert strip_package(package_parts, module_file) == expected


@pytest.mark.parametrize("has_cmake_package", [0, 1])
@pytest.mark.parametrize("has_cmake_module", [0, 1])
@pytest.mark.parametrize("has_hybrid_package", [0, 1])
@pytest.mark.parametrize("has_pure_package", [0, 1])
@pytest.mark.parametrize("has_pure_module", [0, 1])
@pytest.mark.parametrize("with_package_base", [0, 1])
def test_setup_inputs(
    has_cmake_package,
    has_cmake_module,
    has_hybrid_package,
    has_pure_package,
    has_pure_module,
    with_package_base,
    mocker,
):
    """This test that a project can have a package with some modules
    installed using setup.py and some other modules installed using CMake.
    """

    tmp_dir = _tmpdir("test_setup_inputs")

    package_base = "to/the/base" if with_package_base else ""
    package_base_dir = package_base + "/" if package_base else ""
    cmake_source_dir = package_base

    if cmake_source_dir and (has_cmake_package or has_cmake_module):
        pytest.skip(
            "unsupported configuration: "
            "python package fully generated by CMake does *NOT* work. "
            "At least __init__.py should be in the project source tree"
        )

    # -------------------------------------------------------------------------
    # Here is the "SOURCE" tree layout:
    #
    # ROOT/
    #
    #     setup.py
    #
    #     [<base>/]
    #
    #         pureModule.py
    #
    #         pure/
    #             __init__.py
    #             pure.py
    #
    #             data/
    #                 pure.dat
    #
    #     [<cmake_src_dir>/]
    #
    #         hybrid/
    #             CMakeLists.txt
    #             __init__.py
    #             hybrid_pure.dat
    #             hybrid_pure.py
    #
    #             data/
    #                 hybrid_data_pure.dat
    #
    #             hybrid_2/
    #                 __init__.py
    #                 hybrid_2_pure.py
    #
    #             hybrid_2_pure/
    #                 __init__.py
    #                 hybrid_2_pure_1.py
    #                 hybrid_2_pure_2.py
    #
    #
    # -------------------------------------------------------------------------
    # and here is the "BINARY" distribution layout:
    #
    # The comment "CMake" or "Setuptools" indicates which tool is responsible
    # for placing the file in the tree used to create the binary distribution.
    #
    # ROOT/
    #
    #     cmakeModule.py                 # CMake
    #
    #     cmake/
    #         __init__.py                # CMake
    #         cmake.py                   # CMake
    #
    #     hybrid/
    #         hybrid_cmake.dat           # CMake
    #         hybrid_cmake.py            # CMake
    #         hybrid_pure.dat            # Setuptools
    #         hybrid_pure.py             # Setuptools
    #
    #         data/
    #             hybrid_data_pure.dat   # CMake or Setuptools
    #             hybrid_data_cmake.dat  # CMake                  *NO TEST*
    #
    #         hybrid_2/
    #             __init__.py            # CMake or Setuptools
    #             hybrid_2_pure.py       # CMake or Setuptools
    #             hybrid_2_cmake.py      # CMake
    #
    #         hybrid_2_pure/
    #             __init__.py            # CMake or Setuptools
    #             hybrid_2_pure_1.py     # CMake or Setuptools
    #             hybrid_2_pure_2.py     # CMake or Setuptools
    #
    #     pureModule.py                  # Setuptools
    #
    #     pure/
    #         __init__.py                # Setuptools
    #         pure.py                    # Setuptools
    #
    #         data/
    #             pure.dat               # Setuptools

    (tmp_dir / "setup.py").write_text(
        textwrap.dedent(
            """
        from skbuild import setup
        #from setuptools import setup
        setup(
            name="test_hybrid_project",
            version="1.2.3",
            description=("an hybrid package mixing files installed by both "
                        "CMake and setuptools"),
            author='The scikit-build team',
            license="MIT",
            cmake_source_dir='{cmake_source_dir}',
            cmake_install_dir='{cmake_install_dir}',
            # Arbitrary order of packages
            packages=[
        {p_off}    'pure',
        {h_off}    'hybrid.hybrid_2',
        {h_off}    'hybrid',
        {c_off}    'cmake',
        {p_off}    'hybrid.hybrid_2_pure',
            ],
            py_modules=[
        {pm_off}   '{package_base}pureModule',
        {cm_off}   '{package_base}cmakeModule',
            ],
            package_data={{
        {p_off}        'pure': ['data/pure.dat'],
        {h_off}        'hybrid': ['hybrid_pure.dat', 'data/hybrid_data_pure.dat'],
            }},
            # Arbitrary order of package_dir
            package_dir = {{
        {p_off}    'hybrid.hybrid_2_pure': '{package_base}hybrid/hybrid_2_pure',
        {p_off}    'pure': '{package_base}pure',
        {h_off}    'hybrid': '{package_base}hybrid',
        {h_off}    'hybrid.hybrid_2': '{package_base}hybrid/hybrid_2',
        {c_off}    'cmake': '{package_base}cmake',
            }}
        )
        """.format(
                cmake_source_dir=cmake_source_dir,
                cmake_install_dir=package_base,
                package_base=package_base_dir,
                c_off="" if has_cmake_package else "#",
                cm_off="" if has_cmake_module else "#",
                h_off="" if has_hybrid_package else "#",
                p_off="" if has_pure_package else "#",
                pm_off="" if has_pure_module else "#",
            )
        )
    )

    src_dir = tmp_dir / package_base
    src_dir.mkdir(parents=True, exist_ok=True)

    (src_dir / "CMakeLists.txt").write_text(
        textwrap.dedent(
            """
        cmake_minimum_required(VERSION 3.5...3.26)
        project(hybrid NONE)
        set(build_dir ${{CMAKE_BINARY_DIR}})

        {c_off} file(WRITE ${{build_dir}}/__init__.py "")
        {c_off} file(WRITE ${{build_dir}}/cmake.py "")
        {c_off} install(
        {c_off}     FILES
        {c_off}         ${{build_dir}}/__init__.py
        {c_off}         ${{build_dir}}/cmake.py
        {c_off}     DESTINATION cmake
        {c_off}     )

        {cm_off} file(WRITE ${{build_dir}}/cmakeModule.py "")
        {cm_off} install(
        {cm_off}     FILES ${{build_dir}}/cmakeModule.py
        {cm_off}     DESTINATION .)

        {h_off} file(WRITE ${{build_dir}}/hybrid_cmake.dat "")
        {h_off} install(
        {h_off}     FILES ${{build_dir}}/hybrid_cmake.dat
        {h_off}     DESTINATION hybrid)

        {h_off} file(WRITE ${{build_dir}}/hybrid_cmake.py "")
        {h_off} install(
        {h_off}     FILES ${{build_dir}}/hybrid_cmake.py
        {h_off}     DESTINATION hybrid)

        {h_off} file(WRITE ${{build_dir}}/hybrid_data_cmake.dat "")
        {h_off} install(
        {h_off}     FILES ${{build_dir}}/hybrid_data_cmake.dat
        {h_off}     DESTINATION hybrid/data)

        {h_off} file(WRITE ${{build_dir}}/hybrid_2_cmake.py "")
        {h_off} install(
        {h_off}     FILES ${{build_dir}}/hybrid_2_cmake.py
        {h_off}     DESTINATION hybrid/hybrid_2)

        install(CODE "message(STATUS \\\"Installation complete\\\")")
        """.format(
                c_off="" if has_cmake_package else "#",
                cm_off="" if has_cmake_module else "#",
                h_off="" if has_hybrid_package else "#",
            )
        )
    )

    # List path types: 'c', 'cm', 'h', 'p' or 'pm'
    path_types: Sequence[str]
    try:
        path_types = next(
            iter(
                zip(
                    *filter(
                        lambda i: i[1],
                        [
                            ("c", has_cmake_package),
                            ("cm", has_cmake_module),
                            ("h", has_hybrid_package),
                            ("p", has_pure_package),
                            ("pm", has_pure_module),
                        ],
                    )
                )
            )
        )
    except StopIteration:
        path_types = []

    def select_paths(annotated_paths):
        """Return a filtered list paths considering ``path_types``.

        `annotated_paths`` is list of tuple ``(type, path)`` where type
        is either `c`, 'cm', `h`, `p` or 'pm'.

        """
        return filter(lambda i: i[0] in path_types, annotated_paths)

    # Commented paths are the one expected to be installed by CMake. For
    # this reason, corresponding files should NOT be created in the source
    # tree.
    for _type, path in select_paths(
        [
            # ('c', 'cmake/__init__.py'),
            # ('c', 'cmake/cmake.py'),
            # ('cm', 'cmakeModule.py'),
            ("h", "hybrid/__init__.py"),
            # ('h', 'hybrid/hybrid_cmake.dat'),
            # ('h', 'hybrid/hybrid_cmake.py'),
            ("h", "hybrid/hybrid_pure.dat"),
            ("h", "hybrid/hybrid_pure.py"),
            # ('h', 'hybrid/data/hybrid_data_cmake.dat'),
            ("h", "hybrid/data/hybrid_data_pure.dat"),
            ("h", "hybrid/hybrid_2/__init__.py"),
            # ('h', 'hybrid/hybrid_2/hybrid_2_cmake.py'),
            ("h", "hybrid/hybrid_2/hybrid_2_pure.py"),
            ("p", "hybrid/hybrid_2_pure/__init__.py"),
            ("p", "hybrid/hybrid_2_pure/hybrid_2_pure_1.py"),
            ("p", "hybrid/hybrid_2_pure/hybrid_2_pure_2.py"),
            ("pm", "pureModule.py"),
            ("p", "pure/__init__.py"),
            ("p", "pure/pure.py"),
            ("p", "pure/data/pure.dat"),
        ]
    ):
        assert _type in {"p", "pm", "h"}
        root = package_base if _type in {"p", "pm"} else cmake_source_dir
        expected_path = tmp_dir / root / path
        expected_path.parent.mkdir(parents=True, exist_ok=True)
        expected_path.touch(exist_ok=True)

    # Do not call the real setup function. Instead, replace it with
    # a MagicMock allowing to check with which arguments it was invoked.
    mock_setup = mocker.patch("skbuild.setuptools_wrap.setuptools.setup")

    # Convenience print function
    def _pprint(desc, value=None):
        print(
            "-----------------\n"
            f"{desc}:\n"
            "\n"
            f"{pprint.pformat(setup_kw.get(desc, {}) if value is None else value, indent=2)}\n"
        )

    with execute_setup_py(tmp_dir, ["build"], disable_languages_test=True):
        assert mock_setup.call_count == 1
        setup_kw = mock_setup.call_args[1]

        # packages
        expected_packages = []
        if has_cmake_package:
            expected_packages += ["cmake"]
        if has_hybrid_package:
            expected_packages += ["hybrid", "hybrid.hybrid_2"]
        if has_pure_package:
            expected_packages += ["hybrid.hybrid_2_pure", "pure"]

        _pprint("expected_packages", expected_packages)
        _pprint("packages")

        # package dir
        expected_package_dir = {
            package: (os.path.join(CMAKE_INSTALL_DIR(), package_base, package.replace(".", "/")))
            for package in expected_packages
        }
        _pprint("expected_package_dir", expected_package_dir)
        _pprint("package_dir")

        # package data
        expected_package_data = {}

        if has_cmake_package:
            expected_package_data["cmake"] = ["__init__.py", "cmake.py"]

        if has_hybrid_package:
            expected_package_data["hybrid"] = [
                "__init__.py",
                "hybrid_cmake.dat",
                "hybrid_cmake.py",
                "hybrid_pure.dat",
                "hybrid_pure.py",
                "data/hybrid_data_cmake.dat",
                "data/hybrid_data_pure.dat",
            ]
            expected_package_data["hybrid.hybrid_2"] = ["__init__.py", "hybrid_2_cmake.py", "hybrid_2_pure.py"]

        if has_pure_package:
            expected_package_data["hybrid.hybrid_2_pure"] = ["__init__.py", "hybrid_2_pure_1.py", "hybrid_2_pure_2.py"]
            expected_package_data["pure"] = [
                "__init__.py",
                "pure.py",
                "data/pure.dat",
            ]

        if has_cmake_module or has_pure_module:
            expected_modules = []
            if has_cmake_module:
                expected_modules.append(package_base_dir + "cmakeModule.py")
            if has_pure_module:
                expected_modules.append(package_base_dir + "pureModule.py")
            expected_package_data[""] = expected_modules

        _pprint("expected_package_data", expected_package_data)
        package_data = {p: sorted(files) for p, files in setup_kw["package_data"].items()}

        _pprint("package_data", package_data)

        # py_modules (corresponds to files associated with empty package)
        expected_py_modules = []
        if "" in expected_package_data:
            expected_py_modules = [
                str(pathlib.PurePosixPath(module_file).with_suffix("")) for module_file in expected_package_data[""]
            ]
        _pprint("expected_py_modules", expected_py_modules)
        _pprint("py_modules")

        # scripts
        _pprint("expected_scripts", [])
        _pprint("scripts")

        # data_files
        _pprint("expected_data_files", [])
        _pprint("data_files")

        assert sorted(setup_kw["packages"]) == sorted(expected_packages)
        assert sorted(setup_kw["package_dir"]) == sorted(expected_package_dir)
        assert package_data == {p: sorted(files) for p, files in expected_package_data.items()}
        assert sorted(setup_kw["py_modules"]) == sorted(expected_py_modules)
        assert sorted(setup_kw["scripts"]) == sorted([])
        assert sorted(setup_kw["data_files"]) == sorted([])


@pytest.mark.parametrize("with_cmake_source_dir", [0, 1])
def test_cmake_install_into_pure_package(with_cmake_source_dir, capsys, caplog):
    # -------------------------------------------------------------------------
    # "SOURCE" tree layout:
    #
    # (1) with_cmake_source_dir == 0
    #
    # ROOT/
    #
    #     CMakeLists.txt
    #     setup.py
    #
    #     fruits/
    #         __init__.py
    #
    #
    # (2) with_cmake_source_dir == 1
    #
    # ROOT/
    #
    #     setup.py
    #
    #     fruits/
    #         __init__.py
    #
    #     src/
    #
    #         CMakeLists.txt
    #
    # -------------------------------------------------------------------------
    # "BINARY" distribution layout:
    #
    # ROOT/
    #
    #     fruits/
    #
    #         __init__.py
    #         apple.py
    #         banana.py
    #
    #             data/
    #
    #                 apple.dat
    #                 banana.dat
    #

    tmp_dir = _tmpdir("cmake_install_into_pure_package")

    cmake_source_dir = "src" if with_cmake_source_dir else ""

    (tmp_dir / "setup.py").write_text(
        textwrap.dedent(
            f"""
        from skbuild import setup
        setup(
            name="test_py_modules_keyword",
            version="1.2.3",
            description="a package testing use of py_modules keyword",
            author='The scikit-build team',
            license="MIT",
            packages=['fruits'],
            cmake_install_dir='fruits',
            cmake_source_dir='{cmake_source_dir}',
        )
        """
        )
    )

    cmake_src_dir = tmp_dir / cmake_source_dir
    cmake_src_dir.mkdir(parents=True, exist_ok=True)
    (cmake_src_dir / "CMakeLists.txt").write_text(
        textwrap.dedent(
            """
        cmake_minimum_required(VERSION 3.5...3.26)
        project(test NONE)
        file(WRITE "${CMAKE_BINARY_DIR}/apple.py" "# apple.py")
        file(WRITE "${CMAKE_BINARY_DIR}/banana.py" "# banana.py")
        install(
            FILES
                "${CMAKE_BINARY_DIR}/apple.py"
                "${CMAKE_BINARY_DIR}/banana.py"
            DESTINATION "."
            )
        file(WRITE "${CMAKE_BINARY_DIR}/apple.dat" "# apple.dat")
        file(WRITE "${CMAKE_BINARY_DIR}/banana.dat" "# banana.dat")
        install(
            FILES
                "${CMAKE_BINARY_DIR}/apple.dat"
                "${CMAKE_BINARY_DIR}/banana.dat"
            DESTINATION "data"
            )
        """
        )
    )

    fruits_init = tmp_dir / "fruits" / "__init__.py"
    fruits_init.parent.mkdir(parents=True, exist_ok=True)
    fruits_init.touch(exist_ok=True)

    with execute_setup_py(tmp_dir, ["build"], disable_languages_test=True):
        pass

    messages = [
        f"copying {CMAKE_INSTALL_DIR()}/{module} -> {SKBUILD_DIR()}/setuptools/lib"
        for module in [
            "fruits/__init__.py",
            "fruits/apple.py",
            "fruits/banana.py",
            "fruits/data/apple.dat",
            "fruits/data/banana.dat",
        ]
    ]

    out, _ = capsys.readouterr()
    out += caplog.text
    for message in messages:
        assert to_platform_path(message) in out


@pytest.mark.parametrize("zip_safe", [None, False, True])
def test_zip_safe_default(zip_safe, mocker):
    mock_setup = mocker.patch("skbuild.setuptools_wrap.setuptools.setup")

    tmp_dir = _tmpdir("zip_safe_default")

    setup_kwarg = ""
    if zip_safe is not None:
        setup_kwarg = f"zip_safe={zip_safe}"

    (tmp_dir / "setup.py").write_text(
        textwrap.dedent(
            f"""
        from skbuild import setup
        setup(
            name="zip_safe_default",
            version="1.2.3",
            description="a minimal example package",
            author='The scikit-build team',
            license="MIT",
            {setup_kwarg}
        )
        """
        )
    )
    (tmp_dir / "CMakeLists.txt").write_text(
        textwrap.dedent(
            """
        cmake_minimum_required(VERSION 3.5...3.26)
        project(test NONE)
        install(CODE "execute_process(
          COMMAND \\${CMAKE_COMMAND} -E sleep 0)")
        """
        )
    )

    with execute_setup_py(tmp_dir, ["build"], disable_languages_test=True):
        pass

    assert mock_setup.call_count == 1
    setup_kw = mock_setup.call_args[1]

    assert "zip_safe" in setup_kw
    if zip_safe is None:
        assert not setup_kw["zip_safe"]
    elif zip_safe:
        assert setup_kw["zip_safe"]
    else:  # zip_safe is False
        assert not setup_kw["zip_safe"]
