import json
import platform
import re
import subprocess
import textwrap
from pathlib import Path

import pytest

from cibuildwheel.util import resources

from . import test_projects, utils

VERSION_REGEX = r"([\w-]+)==([^\s]+)"

CHECK_VERSIONS_SCRIPT = """\
'''
Checks that the versions in the env var EXPECTED_VERSIONS match those
installed in the active venv.
'''
import os, subprocess, sys, json

versions_raw = json.loads(
    subprocess.check_output([
        sys.executable, '-m', 'pip', 'list', '--format=json',
    ], text=True)
)
versions = {item['name']: item['version'] for item in versions_raw}
expected_versions = json.loads(os.environ['EXPECTED_VERSIONS'])

for name, expected_version in expected_versions.items():
    if name not in versions:
        continue
    if versions[name] != expected_version:
        raise SystemExit(f'error: {name} version should equal {expected_version}. Versions: {versions}')
"""


def test_check_versions_script(
    tmp_path: Path, build_frontend_env_nouv: dict[str, str], capfd: pytest.CaptureFixture[str]
) -> None:
    if utils.get_platform() == "linux":
        pytest.skip("we don't test dependency versions on linux, refer to other tests")

    # sanity check that the CHECK_VERSIONS_SCRIPT fails when it should
    project_dir = tmp_path / "project"
    test_projects.new_c_project().generate(project_dir)

    expected_versions = {
        "pip": "0.0.1",
        "build": "0.0.2",
    }
    script = project_dir / "check_versions.py"
    script.write_text(CHECK_VERSIONS_SCRIPT)

    with pytest.raises(subprocess.CalledProcessError):
        utils.cibuildwheel_run(
            project_dir,
            add_env={
                "CIBW_BEFORE_BUILD": f"python {script.name}",
                "EXPECTED_VERSIONS": json.dumps(expected_versions),
                **build_frontend_env_nouv,
            },
        )

    captured = capfd.readouterr()

    assert (
        "error: pip version should equal 0.0.1" in captured.err
        or "error: build version should equal 0.0.2" in captured.err
    )


def get_versions_from_constraint_file(constraint_file: Path) -> dict[str, str]:
    constraint_file_text = constraint_file.read_text(encoding="utf-8")

    return dict(re.findall(VERSION_REGEX, constraint_file_text))


@pytest.mark.parametrize("python_version", ["3.8", "3.12"])
def test_pinned_versions(
    tmp_path: Path, python_version: str, build_frontend_env_nouv: dict[str, str]
) -> None:
    if utils.get_platform() == "linux":
        pytest.skip("linux doesn't pin individual tool versions, it pins manylinux images instead")
    if python_version != "3.12" and utils.get_platform() == "pyodide":
        pytest.skip(f"pyodide does not support Python {python_version}")
    if (
        python_version == "3.8"
        and utils.get_platform() == "windows"
        and platform.machine() == "ARM64"
    ):
        pytest.skip(f"Windows ARM64 does not support Python {python_version}")

    project_dir = tmp_path / "project"
    test_projects.new_c_project().generate(project_dir)

    # read the expected versions from the appropriate constraint file
    version_no_dot = python_version.replace(".", "")

    # create cross-platform Python before-build script to verify versions pre-build
    before_build_script = project_dir / "check_versions.py"
    before_build_script.write_text(CHECK_VERSIONS_SCRIPT)

    if utils.get_platform() == "pyodide":
        constraint_filename = f"constraints-pyodide{version_no_dot}.txt"
    else:
        constraint_filename = f"constraints-python{version_no_dot}.txt"
    constraint_file = resources.PATH / constraint_filename
    constraint_versions = get_versions_from_constraint_file(constraint_file)

    # build and test the wheels (dependency version check occurs before-build)
    actual_wheels = utils.cibuildwheel_run(
        project_dir,
        add_env={
            "CIBW_BUILD": f"[cp]p{version_no_dot}-*",
            "CIBW_BEFORE_BUILD": f"python {before_build_script.name}",
            "EXPECTED_VERSIONS": json.dumps(constraint_versions),
            **build_frontend_env_nouv,
        },
    )

    # also check that we got the right wheels
    expected_wheels = [
        w
        for w in utils.expected_wheels("spam", "0.1.0")
        if f"-cp{version_no_dot}" in w or f"-pp{version_no_dot}" in w
    ]

    assert set(actual_wheels) == set(expected_wheels)


@pytest.mark.parametrize("method", ["inline", "file"])
def test_dependency_constraints(
    method: str, tmp_path: Path, build_frontend_env_nouv: dict[str, str]
) -> None:
    if utils.get_platform() == "linux":
        pytest.skip("linux doesn't pin individual tool versions, it pins manylinux images instead")

    project_dir = tmp_path / "project"
    test_projects.new_c_project().generate(project_dir)

    tool_versions = {
        "pip": "23.1.2",
        "build": "1.2.2",
        "delocate": "0.10.3",
    }

    if method == "file":
        constraints_file = tmp_path / "constraints file.txt"
        constraints_file.write_text(
            textwrap.dedent(
                """
                pip=={pip}
                build=={build}
                delocate=={delocate}
                """.format(**tool_versions)
            )
        )
        dependency_version_option = str(constraints_file)
    elif method == "inline":
        dependency_version_option = "packages: " + " ".join(
            f"{k}=={v}" for k, v in tool_versions.items()
        )
    else:
        msg = f"Unknown method: {method}"
        raise ValueError(msg)

    skip = ""

    if (
        utils.get_platform() == "windows"
        and method == "file"
        and build_frontend_env_nouv["CIBW_BUILD_FRONTEND"] == "build"
    ):
        # GraalPy 24 fails to discover its standard library when a venv is created
        # from a virtualenv seeded executable. See
        # https://github.com/oracle/graalpython/issues/491 and remove this once
        # GraalPy 24 is dropped
        skip = "gp311*"

    # cross-platform Python script for dependency constraint checks
    before_build_script = project_dir / "check_versions.py"
    before_build_script.write_text(CHECK_VERSIONS_SCRIPT)

    # build and test the wheels (dependency version check occurs pre-build)
    actual_wheels = utils.cibuildwheel_run(
        project_dir,
        add_env={
            "CIBW_SKIP": skip,
            "CIBW_DEPENDENCY_VERSIONS": dependency_version_option,
            "CIBW_BEFORE_BUILD": f"python {before_build_script.name}",
            "EXPECTED_VERSIONS": json.dumps(tool_versions),
            **build_frontend_env_nouv,
        },
        single_python=True,
    )

    # also check that we got the right wheels
    expected_wheels = utils.expected_wheels("spam", "0.1.0", single_python=True)

    if skip == "gp*":
        # See reference to https://github.com/oracle/graalpython/issues/491
        # above
        expected_wheels = [w for w in expected_wheels if "graalpy311" not in w]

    assert set(actual_wheels) == set(expected_wheels)
