from __future__ import annotations

import os
from pathlib import Path
from typing import Any

import pytest
import tomli_w

from pip._internal.build_env import (
    BuildEnvironment,
    SubprocessBuildEnvironmentInstaller,
)
from pip._internal.req import InstallRequirement

from tests.lib import (
    PipTestEnvironment,
    TestData,
    create_basic_wheel_for_package,
    make_test_finder,
)


def make_project(
    tmpdir: Path,
    requires: list[str] | None = None,
    backend: str | None = None,
    backend_path: list[str] | None = None,
) -> Path:
    requires = requires or []
    project_dir = tmpdir / "project"
    project_dir.mkdir()
    buildsys: dict[str, Any] = {"requires": requires}
    if backend:
        buildsys["build-backend"] = backend
    if backend_path:
        buildsys["backend-path"] = backend_path
    data = tomli_w.dumps({"build-system": buildsys})
    project_dir.joinpath("pyproject.toml").write_text(data)
    return project_dir


def test_backend(tmpdir: Path, data: TestData) -> None:
    """Check we can call a requirement's backend successfully"""
    project_dir = make_project(tmpdir, backend="dummy_backend")
    req = InstallRequirement(None, None)
    req.source_dir = os.fspath(project_dir)  # make req believe it has been unpacked
    req.load_pyproject_toml()
    finder = make_test_finder(find_links=[data.backends])
    env = BuildEnvironment(SubprocessBuildEnvironmentInstaller(finder))
    env.install_requirements(["dummy_backend"], "normal", kind="Installing")
    conflicting, missing = env.check_requirements(["dummy_backend"])
    assert not conflicting
    assert not missing
    assert hasattr(req.pep517_backend, "build_wheel")
    with env:
        assert req.pep517_backend is not None
        assert req.pep517_backend.build_wheel("dir") == "Backend called"


dummy_backend_code = """\
def build_wheel(
    wheel_directory,
    config_settings=None,
    metadata_directory=None
):
    return "Backend called"
"""


def test_backend_path(tmpdir: Path, data: TestData) -> None:
    """Check we can call a backend inside the project"""
    project_dir = make_project(tmpdir, backend="dummy_backend", backend_path=["."])
    (project_dir / "dummy_backend.py").write_text(dummy_backend_code)
    req = InstallRequirement(None, None)
    req.source_dir = os.fspath(project_dir)  # make req believe it has been unpacked
    req.load_pyproject_toml()

    env = BuildEnvironment(object())  # type: ignore
    assert hasattr(req.pep517_backend, "build_wheel")
    with env:
        assert req.pep517_backend is not None
        assert req.pep517_backend.build_wheel("dir") == "Backend called"


def test_backend_path_and_dep(tmpdir: Path, data: TestData) -> None:
    """Check we can call a requirement's backend successfully"""
    project_dir = make_project(
        tmpdir, backend="dummy_internal_backend", backend_path=["."]
    )
    (project_dir / "dummy_internal_backend.py").write_text(
        "from dummy_backend import build_wheel"
    )
    req = InstallRequirement(None, None)
    req.source_dir = os.fspath(project_dir)  # make req believe it has been unpacked
    req.load_pyproject_toml()
    finder = make_test_finder(find_links=[data.backends])
    env = BuildEnvironment(SubprocessBuildEnvironmentInstaller(finder))
    env.install_requirements(["dummy_backend"], "normal", kind="Installing")

    assert hasattr(req.pep517_backend, "build_wheel")
    with env:
        assert req.pep517_backend is not None
        assert req.pep517_backend.build_wheel("dir") == "Backend called"


@pytest.mark.parametrize("flag", ["", "--use-feature=inprocess-build-deps"])
def test_pep517_install(
    script: PipTestEnvironment, tmpdir: Path, data: TestData, flag: str
) -> None:
    """Check we can build with a custom backend"""
    project_dir = make_project(
        tmpdir, requires=["test_backend"], backend="test_backend"
    )
    result = script.pip("install", "--no-index", "-f", data.backends, project_dir, flag)
    result.assert_installed("project", editable=False)


@pytest.mark.parametrize("flag", ["", "--use-feature=inprocess-build-deps"])
def test_pep517_install_with_reqs(
    script: PipTestEnvironment, tmpdir: Path, data: TestData, flag: str
) -> None:
    """Backend generated requirements are installed in the build env"""
    project_dir = make_project(
        tmpdir, requires=["test_backend"], backend="test_backend"
    )
    project_dir.joinpath("backend_reqs.txt").write_text("simplewheel")
    result = script.pip(
        "install",
        "--no-index",
        "-f",
        data.backends,
        "-f",
        data.packages,
        project_dir,
        flag,
    )
    result.assert_installed("project", editable=False)


def test_conflicting_pep517_backend_requirements(
    script: PipTestEnvironment, tmpdir: Path, data: TestData
) -> None:
    project_dir = make_project(
        tmpdir, requires=["test_backend", "simplewheel==1.0"], backend="test_backend"
    )
    project_dir.joinpath("backend_reqs.txt").write_text("simplewheel==2.0")
    result = script.pip(
        "install",
        "--no-index",
        "-f",
        data.backends,
        "-f",
        data.packages,
        project_dir,
        expect_error=True,
    )
    msg = (
        f"Some build dependencies for {project_dir.as_uri()} conflict with the backend "
        "dependencies: simplewheel==1.0 is incompatible with "
        "simplewheel==2.0."
    )
    assert result.returncode != 0
    assert msg in result.stderr, str(result)


def test_no_check_build_deps(
    script: PipTestEnvironment, tmpdir: Path, data: TestData
) -> None:
    project_dir = make_project(
        tmpdir, requires=["simplewheel==2.0"], backend="test_backend"
    )
    script.pip(
        "install",
        "simplewheel==1.0",
        "test_backend",
        "--no-index",
        "-f",
        data.packages,
        "-f",
        data.backends,
    )
    result = script.pip("install", "--no-build-isolation", project_dir)
    result.assert_installed("project", editable=False)


def test_validate_missing_pep517_backend_requirements(
    script: PipTestEnvironment, tmpdir: Path, data: TestData
) -> None:
    project_dir = make_project(
        tmpdir, requires=["test_backend", "simplewheel==1.0"], backend="test_backend"
    )
    result = script.pip_install_local(
        "--no-build-isolation",
        "--check-build-dependencies",
        project_dir,
        find_links=[data.backends, data.packages],
        expect_error=True,
    )
    msg = (
        f"Some build dependencies for {project_dir.as_uri()} are missing: "
        "'simplewheel==1.0', 'test_backend'."
    )
    assert result.returncode != 0
    assert msg in result.stderr, str(result)


def test_validate_conflicting_pep517_backend_requirements(
    script: PipTestEnvironment, tmpdir: Path, data: TestData
) -> None:
    project_dir = make_project(
        tmpdir, requires=["simplewheel==1.0"], backend="test_backend"
    )
    script.pip("install", "simplewheel==2.0", "--no-index", "-f", data.packages)
    result = script.pip_install_local(
        "--no-build-isolation",
        "--check-build-dependencies",
        project_dir,
        find_links=[data.backends, data.packages],
        expect_error=True,
    )
    msg = (
        f"Some build dependencies for {project_dir.as_uri()} conflict with the backend "
        "dependencies: simplewheel==2.0 is incompatible with "
        "simplewheel==1.0."
    )
    assert result.returncode != 0
    assert msg in result.stderr, str(result)


def test_pep517_backend_requirements_satisfied_by_prerelease(
    script: PipTestEnvironment,
    data: TestData,
) -> None:
    create_basic_wheel_for_package(script, "myreq", "1.0a1")
    script.pip("install", "myreq==1.0a1", "--no-index", "-f", script.scratch_path)
    script.pip("install", "test_backend", "--no-index", "-f", data.backends)

    project_dir = make_project(
        script.scratch_path,
        requires=["test_backend", "myreq"],
        backend="test_backend",
    )
    project_dir.joinpath("backend_reqs.txt").write_text("myreq")

    result = script.pip("install", "--no-index", "--no-build-isolation", project_dir)
    assert "Installing backend dependencies:" not in result.stdout


@pytest.mark.parametrize("flag", ["", "--use-feature=inprocess-build-deps"])
def test_pep517_backend_requirements_already_satisfied(
    script: PipTestEnvironment, tmpdir: Path, data: TestData, flag: str
) -> None:
    project_dir = make_project(
        tmpdir, requires=["test_backend", "simplewheel==1.0"], backend="test_backend"
    )
    project_dir.joinpath("backend_reqs.txt").write_text("simplewheel")
    result = script.pip_install_local(
        project_dir,
        flag,
        build_isolation=True,
        find_links=[data.backends, data.packages],
    )
    assert "Installing backend dependencies:" not in result.stdout


@pytest.mark.parametrize("flag", ["", "--use-feature=inprocess-build-deps"])
def test_pep517_install_with_no_cache_dir(
    script: PipTestEnvironment, tmpdir: Path, data: TestData, flag: str
) -> None:
    """Check builds with a custom backends work, even with no cache."""
    project_dir = make_project(
        tmpdir, requires=["test_backend"], backend="test_backend"
    )
    result = script.pip_install_local(
        "--no-cache-dir",
        project_dir,
        flag,
        build_isolation=True,
        find_links=data.backends,
    )
    result.assert_installed("project", editable=False)


def make_pyproject_with_setup(
    tmpdir: Path, build_system: bool = True, set_backend: bool = True
) -> tuple[Path, str]:
    project_dir = tmpdir / "project"
    project_dir.mkdir()
    setup_script = "from setuptools import setup\n"
    expect_script_dir_on_path = True
    if build_system:
        buildsys: dict[str, Any] = {
            "requires": ["setuptools", "wheel"],
        }
        if set_backend:
            buildsys["build-backend"] = "setuptools.build_meta"
            expect_script_dir_on_path = False
        project_data = tomli_w.dumps({"build-system": buildsys})
    else:
        project_data = ""

    if expect_script_dir_on_path:
        setup_script += "from pep517_test import __version__\n"
    else:
        setup_script += (
            "try:\n"
            "    import pep517_test\n"
            "except ImportError:\n"
            "    pass\n"
            "else:\n"
            '    raise RuntimeError("Source dir incorrectly on sys.path")\n'
        )

    setup_script += 'setup(name="pep517_test", version="0.1", packages=["pep517_test"])'

    project_dir.joinpath("pyproject.toml").write_text(project_data)
    project_dir.joinpath("setup.py").write_text(setup_script)
    package_dir = project_dir / "pep517_test"
    package_dir.mkdir()
    package_dir.joinpath("__init__.py").write_text('__version__ = "0.1"')
    return project_dir, "pep517_test"


def test_no_build_system_section(
    script: PipTestEnvironment, tmpdir: Path, data: TestData, common_wheels: Path
) -> None:
    """Check builds with setup.py, pyproject.toml, but no build-system section."""
    project_dir, name = make_pyproject_with_setup(tmpdir, build_system=False)
    result = script.pip_install_local(
        "--no-cache-dir", project_dir, find_links=common_wheels
    )
    result.assert_installed(name, editable=False)


def test_no_build_backend_entry(
    script: PipTestEnvironment, tmpdir: Path, data: TestData, common_wheels: Path
) -> None:
    """Check builds with setup.py, pyproject.toml, but no build-backend entry."""
    project_dir, name = make_pyproject_with_setup(tmpdir, set_backend=False)
    result = script.pip_install_local(
        "--no-cache-dir", project_dir, find_links=common_wheels
    )
    result.assert_installed(name, editable=False)


def test_explicit_setuptools_backend(
    script: PipTestEnvironment, tmpdir: Path, data: TestData, common_wheels: Path
) -> None:
    """Check builds with setup.py, pyproject.toml, and a build-backend entry."""
    project_dir, name = make_pyproject_with_setup(tmpdir)
    result = script.pip_install_local(
        "--no-cache-dir", project_dir, find_links=common_wheels
    )
    result.assert_installed(name, editable=False)


@pytest.mark.parametrize("flag", ["", "--use-feature=inprocess-build-deps"])
def test_nested_builds(script: PipTestEnvironment, flag: str, data: TestData) -> None:
    """Smoke test ensuring that nested PEP 517 builds work."""
    # trove-classifiers -> setuptools
    #                   -> calvar -> setuptools
    result = script.pip_install_local(
        "trove-classifiers",
        "--no-cache",
        "--no-binary",
        "trove-classifiers,calvar",
        flag,
        find_links=[data.pypi_packages, data.common_wheels],
    )
    result.assert_installed("trove_classifiers", editable=False)
