import subprocess
import textwrap
from pathlib import Path

import pytest

from . import test_projects, utils

pyproject_toml = r"""
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
"""

limited_api_project = test_projects.new_c_project(
    setup_py_add=textwrap.dedent(
        r"""
        import sys
        import sysconfig

        IS_CPYTHON = sys.implementation.name == "cpython"
        Py_GIL_DISABLED = sysconfig.get_config_var("Py_GIL_DISABLED")
        CAN_USE_ABI3 = IS_CPYTHON and not Py_GIL_DISABLED
        setup_options = {}
        extension_kwargs = {}
        if CAN_USE_ABI3 and sys.version_info[:2] >= (3, 10):
            extension_kwargs["define_macros"] = [("Py_LIMITED_API", "0x030A0000")]
            extension_kwargs["py_limited_api"] = True
            setup_options = {"bdist_wheel": {"py_limited_api": "cp310"}}
    """
    ),
    setup_py_extension_args_add="**extension_kwargs",
    setup_py_setup_args_add="options=setup_options",
)

limited_api_project.files["pyproject.toml"] = pyproject_toml

# Project that claims abi3 but violates the stable ABI by calling
# PyUnicode_AsUTF8 (not in stable ABI until 3.13) without defining
# Py_LIMITED_API in the C code.
violating_abi3_project = test_projects.new_c_project(
    setup_py_add=textwrap.dedent(
        r"""
        import sys
        import sysconfig

        IS_CPYTHON = sys.implementation.name == "cpython"
        Py_GIL_DISABLED = sysconfig.get_config_var("Py_GIL_DISABLED")
        CAN_USE_ABI3 = IS_CPYTHON and not Py_GIL_DISABLED
        setup_options = {}
        extension_kwargs = {}
        if CAN_USE_ABI3 and sys.version_info[:2] >= (3, 10):
            # Intentionally NOT defining Py_LIMITED_API as a C macro,
            # but still tagging the wheel as abi3.
            extension_kwargs["py_limited_api"] = True
            setup_options = {"bdist_wheel": {"py_limited_api": "cp310"}}
    """
    ),
    spam_c_function_add=textwrap.dedent(
        r"""
        // Call a function not in the stable ABI until Python 3.13.
        // Without Py_LIMITED_API defined, the compiler allows it.
        PyObject *str_obj = PyUnicode_FromString(content);
        const char *utf8 = PyUnicode_AsUTF8(str_obj);
        (void)utf8;
        Py_DECREF(str_obj);
    """
    ),
    setup_py_extension_args_add="**extension_kwargs",
    setup_py_setup_args_add="options=setup_options",
)

violating_abi3_project.files["pyproject.toml"] = pyproject_toml


@utils.skip_if_pyodide("abi3audit is disabled on Pyodide (wasm shared objects are not supported)")
def test_abi3audit_runs_on_abi3_wheel(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
    """Test that abi3audit runs automatically on abi3 wheels."""
    project_dir = tmp_path / "project"
    limited_api_project.generate(project_dir)

    actual_wheels = utils.cibuildwheel_run(
        project_dir,
        add_env={
            # Let's only build one cpython version to keep the test fast.
            "CIBW_BUILD": "cp310-*",
            "CIBW_ARCHS": "native",
        },
    )

    assert len(actual_wheels) >= 1

    captured = capfd.readouterr()
    assert "Running audit command: abi3audit" in captured.out


def test_abi3audit_skipped_for_non_abi3_wheel(
    tmp_path: Path, capfd: pytest.CaptureFixture[str]
) -> None:
    """Test that abi3audit does not run for non-abi3 wheels."""
    project_dir = tmp_path / "project"
    basic_project = test_projects.new_c_project()
    basic_project.generate(project_dir)

    actual_wheels = utils.cibuildwheel_run(
        project_dir,
        add_env={
            "CIBW_ARCHS": "native",
        },
        single_python=True,
    )

    assert len(actual_wheels) >= 1

    captured = capfd.readouterr()
    assert "Running audit command: abi3audit" not in captured.out


@utils.skip_if_pyodide("abi3audit is disabled on Pyodide (wasm shared objects are not supported)")
def test_abi3audit_detects_violation(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
    """Test that abi3audit catches stable ABI violations and fails the build.

    This project tags the wheel as cp310-abi3 but uses PyUnicode_AsUTF8,
    which was not part of the stable ABI until Python 3.13.
    """
    project_dir = tmp_path / "project"
    violating_abi3_project.generate(project_dir)

    with pytest.raises(subprocess.CalledProcessError):
        utils.cibuildwheel_run(
            project_dir,
            add_env={
                "CIBW_BUILD": "cp310-*",
                "CIBW_ARCHS": "native",
            },
        )

    captured = capfd.readouterr()
    assert "Running audit command: abi3audit" in captured.out


def test_custom_audit_command(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
    project_dir = tmp_path / "project"
    test_projects.new_c_project().generate(project_dir)

    actual_wheels = utils.cibuildwheel_run(
        project_dir,
        add_env={
            "CIBW_AUDIT_COMMAND": "echo custom-audit-ok {wheel}",
            "CIBW_AUDIT_REQUIRES": "",
            "CIBW_ARCHS": "native",
        },
        single_python=True,
    )

    assert len(actual_wheels) >= 1
    captured = capfd.readouterr()
    assert "Auditing wheel" in captured.out
    assert "custom-audit-ok" in captured.out


def test_custom_audit_requires(tmp_path: Path, capfd: pytest.CaptureFixture[str]) -> None:
    project_dir = tmp_path / "project"
    test_projects.new_c_project().generate(project_dir)

    actual_wheels = utils.cibuildwheel_run(
        project_dir,
        add_env={
            "CIBW_AUDIT_REQUIRES": "pycowsay",
            "CIBW_AUDIT_COMMAND": "pycowsay moo {wheel}",
            "CIBW_ARCHS": "native",
        },
        single_python=True,
    )

    assert len(actual_wheels) >= 1
    captured = capfd.readouterr()
    assert "Installing audit dependencies: pycowsay" in captured.out
    assert "moo" in captured.out


def test_empty_audit_command_disables_audit(
    tmp_path: Path, capfd: pytest.CaptureFixture[str]
) -> None:
    project_dir = tmp_path / "project"
    test_projects.new_c_project().generate(project_dir)

    actual_wheels = utils.cibuildwheel_run(
        project_dir,
        add_env={
            "CIBW_AUDIT_COMMAND": "",
            "CIBW_ARCHS": "native",
        },
        single_python=True,
    )

    assert len(actual_wheels) >= 1
    captured = capfd.readouterr()
    assert "Auditing wheel" not in captured.out


def test_custom_audit_command_failure(tmp_path: Path) -> None:
    project_dir = tmp_path / "project"
    test_projects.new_c_project().generate(project_dir)

    with pytest.raises(subprocess.CalledProcessError):
        utils.cibuildwheel_run(
            project_dir,
            add_env={
                "CIBW_AUDIT_COMMAND": 'python -c "import sys; sys.exit(1)" {wheel}',
                "CIBW_AUDIT_REQUIRES": "",
                "CIBW_ARCHS": "native",
            },
            single_python=True,
        )
