from __future__ import annotations

import json
import tarfile
from pathlib import Path
from zipfile import ZipFile

import pytest

from pip._internal.utils.urls import path_to_url

from tests.lib import PipTestEnvironment, create_basic_sdist_for_package

PYPROJECT_TOML = """\
[build-system]
requires = []
build-backend = "dummy_backend:main"
backend-path = ["backend"]
"""

BACKEND_SRC = '''
import csv
import json
import os.path
from zipfile import ZipFile
import hashlib
import base64
import io

WHEEL = """\
Wheel-Version: 1.0
Generator: dummy_backend 1.0
Root-Is-Purelib: true
Tag: py3-none-any
"""

METADATA = """\
Metadata-Version: 2.1
Name: {project}
Version: {version}
Summary: A dummy package
Author: None
Author-email: none@example.org
License: MIT
{requires_dist}
"""

def make_wheel(z, project, version, requires_dist, files):
    record = []
    def add_file(name, data):
        data = data.encode("utf-8")
        z.writestr(name, data)
        digest = hashlib.sha256(data).digest()
        hash = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ASCII")
        record.append((name, f"sha256={hash}", len(data)))
    distinfo = f"{project}-{version}.dist-info"
    add_file(f"{distinfo}/WHEEL", WHEEL)
    add_file(f"{distinfo}/METADATA", METADATA.format(
        project=project, version=version, requires_dist=requires_dist
    ))
    for name, data in files:
        add_file(name, data)
    record_name = f"{distinfo}/RECORD"
    record.append((record_name, "", ""))
    b = io.BytesIO()
    rec = io.TextIOWrapper(b, newline="", encoding="utf-8")
    w = csv.writer(rec)
    w.writerows(record)
    z.writestr(record_name, b.getvalue())
    rec.close()


class Backend:
    def build_wheel(
        self,
        wheel_directory,
        config_settings=None,
        metadata_directory=None
    ):
        if config_settings is None:
            config_settings = {}
        w = os.path.join(wheel_directory, "{{name}}-1.0-py3-none-any.whl")
        with open(w, "wb") as f:
            with ZipFile(f, "w") as z:
                make_wheel(
                    z, "{{name}}", "1.0", "{{requires_dist}}",
                    [("{{name}}-config.json", json.dumps(config_settings))]
                )
        return "{{name}}-1.0-py3-none-any.whl"

    build_editable = build_wheel

main = Backend()
'''


def make_project(
    path: Path, name: str = "foo", dependencies: list[str] | None = None
) -> tuple[str, str, Path]:
    version = "1.0"
    project_dir = path / name
    backend = project_dir / "backend"
    backend.mkdir(parents=True)
    (project_dir / "pyproject.toml").write_text(PYPROJECT_TOML)
    requires_dist = [f"Requires-Dist: {dep}" for dep in dependencies or []]
    (backend / "dummy_backend.py").write_text(
        BACKEND_SRC.replace("{{name}}", name).replace(
            "{{requires_dist}}", "\n".join(requires_dist)
        )
    )
    return name, version, project_dir


def test_backend_sees_config(script: PipTestEnvironment) -> None:
    name, version, project_dir = make_project(script.scratch_path)
    script.pip(
        "wheel",
        "--config-settings",
        "FOO=Hello",
        project_dir,
    )
    wheel_file_name = f"{name}-{version}-py3-none-any.whl"
    wheel_file_path = script.cwd / wheel_file_name
    with open(wheel_file_path, "rb") as f:
        with ZipFile(f) as z:
            output = z.read(f"{name}-config.json")
            assert json.loads(output) == {"FOO": "Hello"}


def test_backend_sees_config_reqs(script: PipTestEnvironment) -> None:
    name, version, project_dir = make_project(script.scratch_path)
    script.scratch_path.joinpath("reqs.txt").write_text(
        f"{project_dir} --config-settings FOO=Hello"
    )
    script.pip("wheel", "-r", "reqs.txt")
    wheel_file_name = f"{name}-{version}-py3-none-any.whl"
    wheel_file_path = script.cwd / wheel_file_name
    with open(wheel_file_path, "rb") as f:
        with ZipFile(f) as z:
            output = z.read(f"{name}-config.json")
            assert json.loads(output) == {"FOO": "Hello"}


def test_backend_sees_config_via_constraint(script: PipTestEnvironment) -> None:
    name, version, project_dir = make_project(script.scratch_path)
    constraints_file = script.scratch_path / "constraints.txt"
    constraints_file.write_text(f"{name} @ {path_to_url(str(project_dir))}")
    script.pip(
        "wheel",
        "--config-settings",
        "FOO=Hello",
        "-c",
        "constraints.txt",
        name,
    )
    wheel_file_name = f"{name}-{version}-py3-none-any.whl"
    wheel_file_path = script.cwd / wheel_file_name
    with open(wheel_file_path, "rb") as f:
        with ZipFile(f) as z:
            output = z.read(f"{name}-config.json")
            assert json.loads(output) == {"FOO": "Hello"}


@pytest.mark.network
def test_backend_sees_config_via_sdist(script: PipTestEnvironment) -> None:
    name, version, project_dir = make_project(script.scratch_path)
    dists_dir = script.scratch_path / "dists"
    dists_dir.mkdir()
    with tarfile.open(dists_dir / f"{name}-{version}.tar.gz", "w:gz") as dist_tar:
        dist_tar.add(project_dir, arcname=name)
    script.pip(
        "wheel",
        "--config-settings",
        "FOO=Hello",
        "-f",
        dists_dir,
        name,
    )
    wheel_file_name = f"{name}-{version}-py3-none-any.whl"
    wheel_file_path = script.cwd / wheel_file_name
    with open(wheel_file_path, "rb") as f:
        with ZipFile(f) as z:
            output = z.read(f"{name}-config.json")
            assert json.loads(output) == {"FOO": "Hello"}


def test_req_file_does_not_see_config(script: PipTestEnvironment) -> None:
    """Test that CLI config settings do not propagate to requirement files."""
    name, _, project_dir = make_project(script.scratch_path)
    reqs_file = script.scratch_path / "reqs.txt"
    reqs_file.write_text(f"{project_dir}")
    script.pip(
        "install",
        "--config-settings",
        "FOO=Hello",
        "-r",
        reqs_file,
    )
    config = script.site_packages_path / f"{name}-config.json"
    with open(config, "rb") as f:
        assert json.load(f) == {}


def test_dep_does_not_see_config(script: PipTestEnvironment) -> None:
    """Test that CLI config settings do not propagate to dependencies."""
    _, _, bar_project_dir = make_project(script.scratch_path, name="bar")
    _, _, foo_project_dir = make_project(
        script.scratch_path,
        name="foo",
        dependencies=[f"bar @ {path_to_url(str(bar_project_dir))}"],
    )
    script.pip(
        "install",
        "--config-settings",
        "FOO=Hello",
        foo_project_dir,
    )
    foo_config = script.site_packages_path / "foo-config.json"
    with open(foo_config, "rb") as f:
        assert json.load(f) == {"FOO": "Hello"}
    bar_config = script.site_packages_path / "bar-config.json"
    with open(bar_config, "rb") as f:
        assert json.load(f) == {}


def test_dep_in_req_file_does_not_see_config(script: PipTestEnvironment) -> None:
    """Test that CLI config settings do not propagate to dependencies found in
    requirement files."""
    _, _, bar_project_dir = make_project(script.scratch_path, name="bar")
    _, _, foo_project_dir = make_project(
        script.scratch_path,
        name="foo",
        dependencies=["bar"],
    )
    reqs_file = script.scratch_path / "reqs.txt"
    reqs_file.write_text(f"bar @ {path_to_url(str(bar_project_dir))}")
    script.pip(
        "install",
        "--config-settings",
        "FOO=Hello",
        "-r",
        reqs_file,
        foo_project_dir,
    )
    foo_config = script.site_packages_path / "foo-config.json"
    with open(foo_config, "rb") as f:
        assert json.load(f) == {"FOO": "Hello"}
    bar_config = script.site_packages_path / "bar-config.json"
    with open(bar_config, "rb") as f:
        assert json.load(f) == {}


def test_install_sees_config(script: PipTestEnvironment) -> None:
    name, _, project_dir = make_project(script.scratch_path)
    script.pip(
        "install",
        "--config-settings",
        "FOO=Hello",
        project_dir,
    )
    config = script.site_packages_path / f"{name}-config.json"
    with open(config, "rb") as f:
        assert json.load(f) == {"FOO": "Hello"}


def test_install_sees_config_reqs(script: PipTestEnvironment) -> None:
    name, _, project_dir = make_project(script.scratch_path)
    script.scratch_path.joinpath("reqs.txt").write_text(
        f"{project_dir} --config-settings FOO=Hello"
    )
    script.pip("install", "-r", "reqs.txt")
    config = script.site_packages_path / f"{name}-config.json"
    with open(config, "rb") as f:
        assert json.load(f) == {"FOO": "Hello"}


def test_install_editable_sees_config(script: PipTestEnvironment) -> None:
    name, _, project_dir = make_project(script.scratch_path)
    script.pip(
        "install",
        "--config-settings",
        "FOO=Hello",
        "--editable",
        project_dir,
    )
    config = script.site_packages_path / f"{name}-config.json"
    with open(config, "rb") as f:
        assert json.load(f) == {"FOO": "Hello"}


def test_install_config_reqs(script: PipTestEnvironment) -> None:
    name, _, project_dir = make_project(script.scratch_path)
    a_sdist = create_basic_sdist_for_package(
        script,
        "foo",
        "1.0",
        {"pyproject.toml": PYPROJECT_TOML, "backend/dummy_backend.py": BACKEND_SRC},
    )
    script.scratch_path.joinpath("reqs.txt").write_text(
        f'{project_dir} --config-settings "--build-option=--cffi" '
        '--config-settings "--build-option=--avx2" '
        "--config-settings FOO=BAR"
    )
    script.pip("install", "--no-index", "-f", str(a_sdist.parent), "-r", "reqs.txt")
    script.assert_installed(foo="1.0")
    config = script.site_packages_path / f"{name}-config.json"
    with open(config, "rb") as f:
        assert json.load(f) == {"--build-option": ["--cffi", "--avx2"], "FOO": "BAR"}
