import json
import textwrap
from pathlib import Path
from typing import Any, Dict, Tuple

import pytest
from packaging.utils import canonicalize_name

from ..lib import PipTestEnvironment, TestData


def _install_dict(report: Dict[str, Any]) -> Dict[str, Any]:
    return {canonicalize_name(i["metadata"]["name"]): i for i in report["install"]}


def test_install_report_basic(
    script: PipTestEnvironment, shared_data: TestData, tmp_path: Path
) -> None:
    report_path = tmp_path / "report.json"
    script.pip(
        "install",
        "simplewheel",
        "--dry-run",
        "--no-index",
        "--find-links",
        str(shared_data.root / "packages/"),
        "--report",
        str(report_path),
    )
    report = json.loads(report_path.read_text())
    assert "install" in report
    assert len(report["install"]) == 1
    simplewheel_report = _install_dict(report)["simplewheel"]
    assert simplewheel_report["metadata"]["name"] == "simplewheel"
    assert simplewheel_report["requested"] is True
    assert simplewheel_report["is_direct"] is False
    url = simplewheel_report["download_info"]["url"]
    assert url.startswith("file://")
    assert url.endswith("/packages/simplewheel-2.0-1-py2.py3-none-any.whl")
    assert (
        simplewheel_report["download_info"]["archive_info"]["hash"]
        == "sha256=191d6520d0570b13580bf7642c97ddfbb46dd04da5dd2cf7bef9f32391dfe716"
    )


def test_install_report_dep(
    script: PipTestEnvironment, shared_data: TestData, tmp_path: Path
) -> None:
    """Test dependencies are present in the install report with requested=False."""
    report_path = tmp_path / "report.json"
    script.pip(
        "install",
        "require_simple",
        "--dry-run",
        "--no-index",
        "--find-links",
        str(shared_data.root / "packages/"),
        "--report",
        str(report_path),
    )
    report = json.loads(report_path.read_text())
    assert len(report["install"]) == 2
    assert _install_dict(report)["require-simple"]["requested"] is True
    assert _install_dict(report)["simple"]["requested"] is False


def test_yanked_version(
    script: PipTestEnvironment, data: TestData, tmp_path: Path
) -> None:
    """
    Test is_yanked is True when explicitly requesting a yanked package.
    Yanked files are always ignored, unless they are the only file that
    matches a version specifier that "pins" to an exact version (PEP 592).
    """
    report_path = tmp_path / "report.json"
    script.pip(
        "install",
        "simple==3.0",
        "--index-url",
        data.index_url("yanked"),
        "--dry-run",
        "--report",
        str(report_path),
        allow_stderr_warning=True,
    )
    report = json.loads(report_path.read_text())
    simple_report = _install_dict(report)["simple"]
    assert simple_report["requested"] is True
    assert simple_report["is_direct"] is False
    assert simple_report["is_yanked"] is True
    assert simple_report["metadata"]["version"] == "3.0"


def test_skipped_yanked_version(
    script: PipTestEnvironment, data: TestData, tmp_path: Path
) -> None:
    """
    Test is_yanked is False when not explicitly requesting a yanked package.
    Yanked files are always ignored, unless they are the only file that
    matches a version specifier that "pins" to an exact version (PEP 592).
    """
    report_path = tmp_path / "report.json"
    script.pip(
        "install",
        "simple",
        "--index-url",
        data.index_url("yanked"),
        "--dry-run",
        "--report",
        str(report_path),
    )
    report = json.loads(report_path.read_text())
    simple_report = _install_dict(report)["simple"]
    assert simple_report["requested"] is True
    assert simple_report["is_direct"] is False
    assert simple_report["is_yanked"] is False
    assert simple_report["metadata"]["version"] == "2.0"


@pytest.mark.parametrize(
    "specifiers",
    [
        # result should be the same regardless of the method and order in which
        # extras are specified
        ("Paste[openid]==1.7.5.1",),
        ("Paste==1.7.5.1", "Paste[openid]==1.7.5.1"),
        ("Paste[openid]==1.7.5.1", "Paste==1.7.5.1"),
    ],
)
@pytest.mark.network
def test_install_report_index(
    script: PipTestEnvironment, tmp_path: Path, specifiers: Tuple[str, ...]
) -> None:
    """Test report for sdist obtained from index."""
    report_path = tmp_path / "report.json"
    script.pip(
        "install",
        "--dry-run",
        *specifiers,
        "--report",
        str(report_path),
    )
    report = json.loads(report_path.read_text())
    assert len(report["install"]) == 2
    install_dict = _install_dict(report)
    assert install_dict["paste"]["requested"] is True
    assert install_dict["python-openid"]["requested"] is False
    paste_report = install_dict["paste"]
    assert paste_report["download_info"]["url"].startswith(
        "https://files.pythonhosted.org/"
    )
    assert paste_report["download_info"]["url"].endswith("/Paste-1.7.5.1.tar.gz")
    assert (
        paste_report["download_info"]["archive_info"]["hash"]
        == "sha256=11645842ba8ec986ae8cfbe4c6cacff5c35f0f4527abf4f5581ae8b4ad49c0b6"
    )
    assert paste_report["requested_extras"] == ["openid"]
    assert "requires_dist" in paste_report["metadata"]


@pytest.mark.network
def test_install_report_index_multiple_extras(
    script: PipTestEnvironment, tmp_path: Path
) -> None:
    """Test report for sdist obtained from index, with multiple extras requested."""
    report_path = tmp_path / "report.json"
    script.pip(
        "install",
        "--dry-run",
        "Paste[openid]",
        "Paste[subprocess]",
        "--report",
        str(report_path),
    )
    report = json.loads(report_path.read_text())
    install_dict = _install_dict(report)
    assert "paste" in install_dict
    assert install_dict["paste"]["requested_extras"] == ["openid", "subprocess"]


@pytest.mark.network
def test_install_report_direct_archive(
    script: PipTestEnvironment, tmp_path: Path, shared_data: TestData
) -> None:
    """Test report for direct URL archive."""
    report_path = tmp_path / "report.json"
    script.pip(
        "install",
        str(shared_data.root / "packages" / "simplewheel-1.0-py2.py3-none-any.whl"),
        "--dry-run",
        "--no-index",
        "--report",
        str(report_path),
    )
    report = json.loads(report_path.read_text())
    assert "install" in report
    assert len(report["install"]) == 1
    simplewheel_report = _install_dict(report)["simplewheel"]
    assert simplewheel_report["metadata"]["name"] == "simplewheel"
    assert simplewheel_report["requested"] is True
    assert simplewheel_report["is_direct"] is True
    url = simplewheel_report["download_info"]["url"]
    assert url.startswith("file://")
    assert url.endswith("/packages/simplewheel-1.0-py2.py3-none-any.whl")
    assert (
        simplewheel_report["download_info"]["archive_info"]["hash"]
        == "sha256=e63aa139caee941ec7f33f057a5b987708c2128238357cf905429846a2008718"
    )
    assert simplewheel_report["download_info"]["archive_info"]["hashes"] == {
        "sha256": "e63aa139caee941ec7f33f057a5b987708c2128238357cf905429846a2008718"
    }


@pytest.mark.network
def test_install_report_vcs_and_wheel_cache(
    script: PipTestEnvironment, tmp_path: Path
) -> None:
    """Test report for VCS reference, and interactions with the wheel cache."""
    cache_dir = tmp_path / "cache"
    report_path = tmp_path / "report.json"
    script.pip(
        "install",
        "git+https://github.com/pypa/pip-test-package"
        "@5547fa909e83df8bd743d3978d6667497983a4b7",
        "--cache-dir",
        str(cache_dir),
        "--report",
        str(report_path),
    )
    report = json.loads(report_path.read_text())
    assert len(report["install"]) == 1
    pip_test_package_report = report["install"][0]
    assert pip_test_package_report["is_direct"] is True
    assert pip_test_package_report["requested"] is True
    assert (
        pip_test_package_report["download_info"]["url"]
        == "https://github.com/pypa/pip-test-package"
    )
    assert pip_test_package_report["download_info"]["vcs_info"]["vcs"] == "git"
    assert (
        pip_test_package_report["download_info"]["vcs_info"]["commit_id"]
        == "5547fa909e83df8bd743d3978d6667497983a4b7"
    )
    # Now do it again to make sure the cache is used and that the report still contains
    # the original VCS url.
    report_path.unlink()
    result = script.pip(
        "install",
        "pip-test-package @ git+https://github.com/pypa/pip-test-package"
        "@5547fa909e83df8bd743d3978d6667497983a4b7",
        "--ignore-installed",
        "--cache-dir",
        str(cache_dir),
        "--report",
        str(report_path),
    )
    assert "Using cached pip_test_package" in result.stdout
    report = json.loads(report_path.read_text())
    assert len(report["install"]) == 1
    pip_test_package_report = report["install"][0]
    assert pip_test_package_report["is_direct"] is True
    assert pip_test_package_report["requested"] is True
    assert (
        pip_test_package_report["download_info"]["url"]
        == "https://github.com/pypa/pip-test-package"
    )
    assert pip_test_package_report["download_info"]["vcs_info"]["vcs"] == "git"
    assert (
        pip_test_package_report["download_info"]["vcs_info"]["commit_id"]
        == "5547fa909e83df8bd743d3978d6667497983a4b7"
    )


@pytest.mark.network
def test_install_report_vcs_editable(
    script: PipTestEnvironment, tmp_path: Path
) -> None:
    """Test report remote editable."""
    report_path = tmp_path / "report.json"
    script.pip(
        "install",
        "--editable",
        "git+https://github.com/pypa/pip-test-package"
        "@5547fa909e83df8bd743d3978d6667497983a4b7"
        "#egg=pip-test-package",
        "--report",
        str(report_path),
    )
    report = json.loads(report_path.read_text())
    assert len(report["install"]) == 1
    pip_test_package_report = report["install"][0]
    assert pip_test_package_report["is_direct"] is True
    assert pip_test_package_report["download_info"]["url"].startswith("file://")
    assert pip_test_package_report["download_info"]["url"].endswith(
        "/src/pip-test-package"
    )
    assert pip_test_package_report["download_info"]["dir_info"]["editable"] is True


@pytest.mark.network
def test_install_report_local_path_with_extras(
    script: PipTestEnvironment, tmp_path: Path, shared_data: TestData
) -> None:
    """Test report remote editable."""
    project_path = tmp_path / "pkga"
    project_path.mkdir()
    project_path.joinpath("pyproject.toml").write_text(
        textwrap.dedent(
            """\
            [project]
            name = "pkga"
            version = "1.0"

            [project.optional-dependencies]
            test = ["simple"]
            """
        )
    )
    report_path = tmp_path / "report.json"
    script.pip(
        "install",
        "--dry-run",
        "--no-build-isolation",
        "--no-index",
        "--find-links",
        str(shared_data.root / "packages/"),
        "--report",
        str(report_path),
        str(project_path) + "[test]",
    )
    report = json.loads(report_path.read_text())
    assert len(report["install"]) == 2
    pkga_report = report["install"][0]
    assert pkga_report["metadata"]["name"] == "pkga"
    assert pkga_report["is_direct"] is True
    assert pkga_report["requested"] is True
    assert pkga_report["requested_extras"] == ["test"]
    simple_report = report["install"][1]
    assert simple_report["metadata"]["name"] == "simple"
    assert simple_report["is_direct"] is False
    assert simple_report["requested"] is False
    assert "requested_extras" not in simple_report


@pytest.mark.network
def test_install_report_editable_local_path_with_extras(
    script: PipTestEnvironment, tmp_path: Path, shared_data: TestData
) -> None:
    """Test report remote editable."""
    project_path = tmp_path / "pkga"
    project_path.mkdir()
    project_path.joinpath("pyproject.toml").write_text(
        textwrap.dedent(
            """\
            [project]
            name = "pkga"
            version = "1.0"

            [project.optional-dependencies]
            test = ["simple"]
            """
        )
    )
    report_path = tmp_path / "report.json"
    script.pip(
        "install",
        "--dry-run",
        "--no-build-isolation",
        "--no-index",
        "--find-links",
        str(shared_data.root / "packages/"),
        "--report",
        str(report_path),
        "--editable",
        str(project_path) + "[test]",
    )
    report = json.loads(report_path.read_text())
    assert len(report["install"]) == 2
    pkga_report = report["install"][0]
    assert pkga_report["metadata"]["name"] == "pkga"
    assert pkga_report["is_direct"] is True
    assert pkga_report["requested"] is True
    assert pkga_report["requested_extras"] == ["test"]
    simple_report = report["install"][1]
    assert simple_report["metadata"]["name"] == "simple"
    assert simple_report["is_direct"] is False
    assert simple_report["requested"] is False
    assert "requested_extras" not in simple_report


def test_install_report_to_stdout(
    script: PipTestEnvironment, shared_data: TestData
) -> None:
    result = script.pip(
        "install",
        "simplewheel",
        "--quiet",
        "--dry-run",
        "--no-index",
        "--find-links",
        str(shared_data.root / "packages/"),
        "--report",
        "-",
    )
    report = json.loads(result.stdout)
    assert "install" in report
    assert len(report["install"]) == 1
