from __future__ import annotations

import importlib.metadata
import logging
import os
import re
import subprocess
import sys
import textwrap

from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any

import pytest

from packaging.version import Version
from setuptools_scm._integration import setuptools as setuptools_integration
from setuptools_scm._integration.pyproject_reading import PyProjectData
from setuptools_scm._integration.setup_cfg import SetuptoolsBasicData
from setuptools_scm._integration.setup_cfg import read_setup_cfg
from vcs_versioning._requirement_cls import extract_package_name

if TYPE_CHECKING:
    import setuptools

from setuptools_scm import Configuration
from vcs_versioning._overrides import PRETEND_KEY
from vcs_versioning._overrides import PRETEND_KEY_NAMED
from vcs_versioning._run_cmd import run
from vcs_versioning.test_api import WorkDir

c = Configuration()


def _run_setuptools_setup(cwd: Path) -> subprocess.CompletedProcess[str]:
    """Invoke ``setuptools.setup()`` with no commands to trigger inference hooks.

    Exits non-zero ("no commands supplied") but the hooks still fire,
    writing version files to the source tree as a side effect.
    """
    result = subprocess.run(
        [sys.executable, "-c", "import setuptools; setuptools.setup()"],
        cwd=cwd,
        capture_output=True,
        text=True,
        check=False,
    )
    assert result.returncode != 0, "setup() with no commands should exit non-zero"
    return result


# Module-level fixture for git SCM setup
@pytest.fixture
def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> WorkDir:
    """Set up git for integration tests."""
    return wd.setup_git(monkeypatch)


@pytest.mark.issue("1314")
def test_get_version_no_implicit_global_overrides_warning_subprocess(
    wd: WorkDir,
) -> None:
    """setuptools_scm.get_version must not rely on implicit GlobalOverrides auto-create.

    Pytest's vcs_versioning plugin installs a global ``GlobalOverrides`` context, so
    this check runs in a subprocess without that fixture (see GitHub issue #1314).
    """
    root = os.fspath(wd.cwd)
    script = textwrap.dedent(
        f"""
        import warnings

        warnings.filterwarnings(
            "error",
            message="No GlobalOverrides context is active",
            category=UserWarning,
        )
        import setuptools_scm

        setuptools_scm.get_version(root={root!r})
        """
    )
    proc = subprocess.run(
        [sys.executable, "-c", script],
        capture_output=True,
        text=True,
        check=False,
    )
    assert proc.returncode == 0, (proc.stdout, proc.stderr)


def test_pyproject_support(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
    pkg = tmp_path / "package"
    pkg.mkdir()
    pkg.joinpath("pyproject.toml").write_text(
        textwrap.dedent(
            """
            [tool.setuptools_scm]
            fallback_version = "12.34"
            [project]
            name = "foo"
            description = "Factory ⸻ A code generator 🏭"
            authors = [{name = "Łukasz Langa"}]
            dynamic = ["version"]
            """
        ),
        encoding="utf-8",
    )
    pkg.joinpath("setup.py").write_text(
        "__import__('setuptools').setup()", encoding="utf-8"
    )
    res = run([sys.executable, "setup.py", "--version"], pkg)
    assert res.stdout == "12.34"


def test_pretend_version(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None:
    monkeypatch.setenv(PRETEND_KEY, "1.0.0")

    assert wd.get_version() == "1.0.0"
    assert wd.get_version(dist_name="ignored") == "1.0.0"


def test_pretend_version_named(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None:
    monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test".upper()), "1.0.0")
    monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test2".upper()), "2.0.0")
    assert wd.get_version(dist_name="test") == "1.0.0"
    assert wd.get_version(dist_name="test2") == "2.0.0"


def test_pretend_version_name_takes_precedence(
    monkeypatch: pytest.MonkeyPatch, wd: WorkDir
) -> None:
    monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test".upper()), "1.0.0")
    monkeypatch.setenv(PRETEND_KEY, "2.0.0")
    assert wd.get_version(dist_name="test") == "1.0.0"


def test_pretend_version_rejects_invalid_string(
    monkeypatch: pytest.MonkeyPatch, wd: WorkDir
) -> None:
    """Test that invalid pretend versions raise errors and bubble up."""
    monkeypatch.setenv(PRETEND_KEY, "dummy")

    # With strict validation, invalid pretend versions should raise errors
    with pytest.raises(Exception, match=r".*dummy.*"):
        wd.get_version(write_to="test.py")


def test_pretend_metadata_with_version(
    monkeypatch: pytest.MonkeyPatch, wd: WorkDir
) -> None:
    """Test pretend metadata overrides work with pretend version."""
    from vcs_versioning._overrides import PRETEND_METADATA_KEY

    monkeypatch.setenv(PRETEND_KEY, "1.2.3.dev4+g1337beef")
    monkeypatch.setenv(PRETEND_METADATA_KEY, '{node="g1337beef", distance=4}')

    version = wd.get_version()
    assert version == "1.2.3.dev4+g1337beef"

    # Test version file template functionality
    wd("mkdir -p src")
    version_file_content = """
version = '{version}'
major = {version_tuple[0]}
minor = {version_tuple[1]}
patch = {version_tuple[2]}
commit_hash = '{scm_version.short_node}'
num_commit = {scm_version.distance}
"""  # noqa: RUF027
    # Use write_to with template to create version file
    version = wd.get_version(
        write_to="src/version.py", write_to_template=version_file_content
    )

    content = (wd.cwd / "src/version.py").read_text(encoding="utf-8")
    assert "commit_hash = 'g1337beef'" in content
    assert "num_commit = 4" in content


def test_pretend_metadata_named(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None:
    """Test pretend metadata with named package support."""
    from vcs_versioning._overrides import PRETEND_METADATA_KEY_NAMED

    monkeypatch.setenv(
        PRETEND_KEY_NAMED.format(name="test".upper()), "1.2.3.dev5+gabcdef12"
    )
    monkeypatch.setenv(
        PRETEND_METADATA_KEY_NAMED.format(name="test".upper()),
        '{node="gabcdef12", distance=5, dirty=true}',
    )

    version = wd.get_version(dist_name="test")
    assert version == "1.2.3.dev5+gabcdef12"


def test_pretend_metadata_without_version_warns(
    monkeypatch: pytest.MonkeyPatch, wd: WorkDir, caplog: pytest.LogCaptureFixture
) -> None:
    """Test that pretend metadata without any base version logs a warning."""
    from vcs_versioning._overrides import PRETEND_METADATA_KEY

    # Only set metadata, no version - but there will be a git repo so there will be a base version
    # Let's create an empty git repo without commits to truly have no base version
    monkeypatch.setenv(PRETEND_METADATA_KEY, '{node="g1234567", distance=2}')

    with caplog.at_level(logging.WARNING):
        version = wd.get_version()
        assert version is not None

    # In this case, metadata was applied to a fallback version, so no warning about missing base


def test_pretend_metadata_with_scm_version(
    wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test that pretend metadata works with actual SCM-detected version."""
    from vcs_versioning._overrides import PRETEND_METADATA_KEY

    # Set up a git repo with a tag so we have a base version
    wd("git init")
    wd("git config user.name test")
    wd("git config user.email test@example.com")
    wd.write("file.txt", "content")
    wd("git add file.txt")
    wd("git commit -m 'initial'")
    wd("git tag v1.0.0")

    # Now add metadata overrides
    monkeypatch.setenv(PRETEND_METADATA_KEY, '{node="gcustom123", distance=7}')

    # Test that the metadata gets applied to the actual SCM version
    version = wd.get_version()
    # The version becomes 1.0.1.dev7+gcustom123 due to version scheme and metadata overrides
    assert "1.0.1.dev7+gcustom123" == version

    # Test version file to see if metadata was applied
    wd("mkdir -p src")
    version_file_content = """
version = '{version}'
commit_hash = '{scm_version.short_node}'
num_commit = {scm_version.distance}
"""  # noqa: RUF027
    version = wd.get_version(
        write_to="src/version.py", write_to_template=version_file_content
    )

    content = (wd.cwd / "src/version.py").read_text(encoding="utf-8")
    assert "commit_hash = 'gcustom123'" in content
    assert "num_commit = 7" in content


def test_pretend_metadata_type_conversion(
    monkeypatch: pytest.MonkeyPatch, wd: WorkDir
) -> None:
    """Test that pretend metadata properly uses TOML native types."""
    from vcs_versioning._overrides import PRETEND_METADATA_KEY

    monkeypatch.setenv(PRETEND_KEY, "2.0.0")
    monkeypatch.setenv(
        PRETEND_METADATA_KEY,
        '{distance=10, dirty=true, node="gfedcba98", branch="feature-branch"}',
    )

    version = wd.get_version()
    # The version should be formatted properly with the metadata
    assert "2.0.0" in version


def test_pretend_metadata_invalid_fields_filtered(
    monkeypatch: pytest.MonkeyPatch, wd: WorkDir, caplog: pytest.LogCaptureFixture
) -> None:
    """Test that invalid metadata fields are filtered out with a warning."""
    from vcs_versioning._overrides import PRETEND_METADATA_KEY

    monkeypatch.setenv(PRETEND_KEY, "1.0.0")
    monkeypatch.setenv(
        PRETEND_METADATA_KEY,
        '{node="g123456", distance=3, invalid_field="should_be_ignored", another_bad_field=42}',
    )

    with caplog.at_level(logging.WARNING):
        version = wd.get_version()
        assert version == "1.0.0"

    assert "Invalid fields in TOML data" in caplog.text
    assert "invalid_field" in caplog.text
    assert "another_bad_field" in caplog.text


def test_pretend_metadata_date_parsing(
    monkeypatch: pytest.MonkeyPatch, wd: WorkDir
) -> None:
    """Test that TOML date values work in pretend metadata."""
    from vcs_versioning._overrides import PRETEND_METADATA_KEY

    monkeypatch.setenv(PRETEND_KEY, "1.5.0")
    monkeypatch.setenv(
        PRETEND_METADATA_KEY, '{node="g987654", distance=7, node_date=2024-01-15}'
    )

    version = wd.get_version()
    assert version == "1.5.0"


def test_pretend_metadata_invalid_toml_error(
    monkeypatch: pytest.MonkeyPatch, wd: WorkDir, caplog: pytest.LogCaptureFixture
) -> None:
    """Test that invalid TOML in pretend metadata logs an error."""
    from vcs_versioning._overrides import PRETEND_METADATA_KEY

    monkeypatch.setenv(PRETEND_KEY, "1.0.0")
    monkeypatch.setenv(PRETEND_METADATA_KEY, "{invalid toml syntax here}")

    with caplog.at_level(logging.ERROR):
        version = wd.get_version()
        # Should fall back to basic pretend version
        assert version == "1.0.0"

    assert "Failed to parse pretend metadata" in caplog.text


def test_git_tag_with_local_build_data_preserved(wd: WorkDir) -> None:
    """Test that git tags containing local build data are preserved in final version."""
    wd.commit_testfile()

    # Create a git tag that includes local build data
    # This simulates a CI system that creates tags with build metadata
    wd("git tag 1.0.0+build.123")

    # The version should preserve the build metadata from the tag
    version = wd.get_version()

    # Validate it's a proper PEP 440 version
    parsed_version = Version(version)
    assert str(parsed_version) == version, (
        f"Version should parse correctly as PEP 440: {version}"
    )

    assert version == "1.0.0+build.123", (
        f"Expected build metadata preserved, got {version}"
    )

    # Validate the local part is correct
    assert parsed_version.local == "build.123", (
        f"Expected local part 'build.123', got {parsed_version.local}"
    )


def test_git_tag_with_commit_hash_preserved(wd: WorkDir) -> None:
    """Test that git tags with commit hash data are preserved."""
    wd.commit_testfile()

    # Create a git tag that includes commit hash metadata
    wd("git tag 2.0.0+sha.abcd1234")

    # The version should preserve the commit hash from the tag
    version = wd.get_version()

    # Validate it's a proper PEP 440 version
    parsed_version = Version(version)
    assert str(parsed_version) == version, (
        f"Version should parse correctly as PEP 440: {version}"
    )

    assert version == "2.0.0+sha.abcd1234"

    # Validate the local part is correct
    assert parsed_version.local == "sha.abcd1234", (
        f"Expected local part 'sha.abcd1234', got {parsed_version.local}"
    )


def test_git_tag_with_local_build_data_preserved_dirty_workdir(wd: WorkDir) -> None:
    """Test that git tags with local build data are preserved even with dirty working directory."""
    wd.commit_testfile()

    # Create a git tag that includes local build data
    wd("git tag 1.5.0+build.456")

    # Make working directory dirty
    wd.write("modified_file.txt", "some changes")

    # The version should preserve the build metadata from the tag
    # even when working directory is dirty
    version = wd.get_version()

    # Validate it's a proper PEP 440 version
    parsed_version = Version(version)
    assert str(parsed_version) == version, (
        f"Version should parse correctly as PEP 440: {version}"
    )

    assert version == "1.5.0+build.456", (
        f"Expected build metadata preserved with dirty workdir, got {version}"
    )

    # Validate the local part is correct
    assert parsed_version.local == "build.456", (
        f"Expected local part 'build.456', got {parsed_version.local}"
    )


def test_git_tag_with_local_build_data_preserved_with_distance(wd: WorkDir) -> None:
    """Test that git tags with local build data are preserved with distance."""
    wd.commit_testfile()

    # Create a git tag that includes local build data
    wd("git tag 3.0.0+ci.789")

    # Add another commit after the tag to create distance
    wd.commit_testfile("after-tag")

    # The version should use version scheme for distance but preserve original tag's build data
    version = wd.get_version()

    # Validate it's a proper PEP 440 version
    parsed_version = Version(version)
    assert str(parsed_version) == version, (
        f"Version should parse correctly as PEP 440: {version}"
    )

    # Tag local data should be preserved and combined with SCM data
    assert version.startswith("3.0.1.dev1"), (
        f"Expected dev version with distance, got {version}"
    )

    # Use regex to validate the version format with both tag build data and SCM node data
    # Expected format: 3.0.1.dev1+ci.789.g<commit_hash>
    version_pattern = r"^3\.0\.1\.dev1\+ci\.789\.g[a-f0-9]+$"
    assert re.match(version_pattern, version), (
        f"Version should match pattern {version_pattern}, got {version}"
    )

    # The original tag's local data (+ci.789) should be preserved and combined with SCM data
    assert "+ci.789" in version, f"Tag local data should be preserved, got {version}"

    # Validate the local part contains both tag and SCM node information
    assert parsed_version.local is not None, (
        f"Expected local version part, got {parsed_version.local}"
    )
    assert "ci.789" in parsed_version.local, (
        f"Expected local part to contain tag data 'ci.789', got {parsed_version.local}"
    )
    assert "g" in parsed_version.local, (
        f"Expected local part to contain SCM node data 'g...', got {parsed_version.local}"
    )

    # Note: This test verifies that local build data from tags is preserved and combined
    # with SCM data when there's distance, which is the desired behavior for issue 1019.


@pytest.mark.issue(611)
def test_distribution_provides_extras() -> None:
    from importlib.metadata import distribution

    dist = distribution("setuptools_scm")
    pe: list[str] = dist.metadata.get_all("Provides-Extra", [])
    assert sorted(pe) == ["rich", "simple", "toml"]


@pytest.mark.issue(760)
def test_unicode_in_setup_cfg(tmp_path: Path) -> None:
    cfg = tmp_path / "setup.cfg"
    cfg.write_text(
        textwrap.dedent(
            """
            [metadata]
            name = configparser
            author = Łukasz Langa
            """
        ),
        encoding="utf-8",
    )
    from setuptools_scm._integration.setup_cfg import read_setup_cfg

    name = read_setup_cfg(cfg).name
    assert name == "configparser"

    # also ensure we can parse a version if present (legacy projects)
    cfg.write_text(
        textwrap.dedent(
            """
            [metadata]
            name = configparser
            version = 1.2.3
            """
        ),
        encoding="utf-8",
    )

    data = read_setup_cfg(cfg)
    assert isinstance(data, SetuptoolsBasicData)
    assert data.name == "configparser"
    assert data.version == "1.2.3"


@pytest.mark.issue(1216)
def test_setup_cfg_dynamic_version_warns_and_ignores(tmp_path: Path) -> None:
    cfg = tmp_path / "setup.cfg"
    cfg.write_text(
        textwrap.dedent(
            """
            [metadata]
            name = example-broken
            version = attr: example_broken.__version__
            """
        ),
        encoding="utf-8",
    )

    with pytest.warns(
        UserWarning,
        match=r"setup\.cfg: at \[metadata\]",
    ):
        legacy_data = read_setup_cfg(cfg)

    assert legacy_data.version is None


def test_setup_cfg_version_prevents_inference_version_keyword(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    # Legacy project setup - we construct the data directly since files are not read anyway
    monkeypatch.chdir(tmp_path)

    dist = create_clean_distribution("legacy-proj")

    # Using keyword should detect an existing version via legacy data and avoid inferring
    from setuptools_scm._integration import setuptools as setuptools_integration
    from setuptools_scm._integration.pyproject_reading import PyProjectData
    from setuptools_scm._integration.setup_cfg import SetuptoolsBasicData

    # Construct PyProjectData directly without requiring build backend inference
    pyproject_data = PyProjectData.for_testing(
        tool_name="setuptools_scm",
        is_required=False,  # setuptools-scm not required
        section_present=False,  # no [tool.setuptools_scm] section
        project_present=False,  # no [project] section
    )

    # Construct legacy data with version from setup.cfg
    legacy_data = SetuptoolsBasicData(
        path=tmp_path / "setup.cfg", name="legacy-proj", version="0.9.0"
    )

    with pytest.warns(UserWarning, match="version of legacy-proj already set"):
        setuptools_integration.version_keyword(
            dist,
            "use_scm_version",
            True,
            _given_pyproject_data=pyproject_data,
            _given_legacy_data=legacy_data,
        )

    # setuptools_scm should not set a version when setup.cfg already provided one
    assert dist.metadata.version is None


def test_setuptools_version_keyword_ensures_regex(
    wd: WorkDir,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    wd.commit_testfile("test")
    wd("git tag 1.0")
    monkeypatch.chdir(wd.cwd)

    dist = create_clean_distribution("test")
    setuptools_integration.version_keyword(
        dist, "use_scm_version", {"tag_regex": "(1.0)"}
    )
    assert dist.metadata.version == "1.0"


@pytest.mark.parametrize(
    "ep_name", ["setuptools_scm.parse_scm", "setuptools_scm.parse_scm_fallback"]
)
def test_git_archival_plugin_ignored(tmp_path: Path, ep_name: str) -> None:
    tmp_path.joinpath(".git_archival.txt").write_text("broken", encoding="utf-8")
    try:
        dist = importlib.metadata.distribution("setuptools_scm_git_archive")
    except importlib.metadata.PackageNotFoundError:
        pytest.skip("setuptools_scm_git_archive not installed")
    else:
        print(dist.metadata["Name"], dist.version)
    from setuptools_scm.discover import iter_matching_entrypoints

    found = list(iter_matching_entrypoints(tmp_path, config=c, entrypoint=ep_name))
    imports = [item.value for item in found]
    assert "setuptools_scm_git_archive:parse" not in imports


@pytest.mark.parametrize("base_name", ["setuptools_scm", "setuptools-scm"])
@pytest.mark.parametrize(
    "requirements",
    ["", ">=8", "[toml]>=7", "~=9.0", "[rich,toml]>=8"],
    ids=["empty", "version", "extras", "fuzzy", "multiple-extras"],
)
def test_extract_package_name(base_name: str, requirements: str) -> None:
    """Test the _extract_package_name helper function"""
    assert extract_package_name(f"{base_name}{requirements}") == "setuptools-scm"


# Helper function for creating and managing distribution objects
def create_clean_distribution(name: str) -> setuptools.Distribution:
    """Create a clean distribution object without any setuptools_scm effects.

    This function creates a new setuptools Distribution and ensures it's completely
    clean from any previous setuptools_scm version inference effects, including:
    - Clearing any existing version
    - Removing the _setuptools_scm_version_set_by_infer flag
    """
    import setuptools

    dist = setuptools.Distribution({"name": name})

    # Clean all setuptools_scm effects
    dist.metadata.version = None
    if hasattr(dist, "_setuptools_scm_version_set_by_infer"):
        del dist._setuptools_scm_version_set_by_infer

    return dist


def version_keyword_default(
    dist: setuptools.Distribution, pyproject_data: PyProjectData | None = None
) -> None:
    """Helper to call version_keyword with default config and return the result."""

    setuptools_integration.version_keyword(
        dist, "use_scm_version", True, _given_pyproject_data=pyproject_data
    )


def version_keyword_calver(
    dist: setuptools.Distribution, pyproject_data: PyProjectData | None = None
) -> None:
    """Helper to call version_keyword with calver-by-date scheme and return the result."""

    setuptools_integration.version_keyword(
        dist,
        "use_scm_version",
        {"version_scheme": "calver-by-date"},
        _given_pyproject_data=pyproject_data,
    )


def infer_version_with_data(
    dist: setuptools.Distribution, pyproject_data: PyProjectData | None = None
) -> None:
    """Helper to call infer_version with pyproject data."""

    setuptools_integration.infer_version(dist, _given_pyproject_data=pyproject_data)


@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/1022")
@pytest.mark.filterwarnings("ignore:version of .* already set:UserWarning")
@pytest.mark.filterwarnings(
    "ignore:.* does not correspond to a valid versioning date.*:UserWarning"
)
@pytest.mark.parametrize(
    ("first_integration", "second_integration", "expected_final_version"),
    [
        # infer_version and version_keyword can be called in either order
        (infer_version_with_data, version_keyword_default, "1.0.1.dev1"),
        (infer_version_with_data, version_keyword_calver, "9.2.13.0.dev1"),
        (version_keyword_default, infer_version_with_data, "1.0.1.dev1"),
        (version_keyword_calver, infer_version_with_data, "9.2.13.0.dev1"),
    ],
)
def test_integration_function_call_order(
    wd: WorkDir,
    monkeypatch: pytest.MonkeyPatch,
    first_integration: Any,
    second_integration: Any,
    expected_final_version: str,
) -> None:
    """Test that integration functions can be called in any order.

    version_keyword should always win when it specifies configuration, but currently doesn't.
    Some tests will fail, showing the bug.
    """
    # Set up controlled environment for deterministic versions
    monkeypatch.setenv("SOURCE_DATE_EPOCH", "1234567890")  # 2009-02-13T23:31:30+00:00
    # Override node_date to get consistent calver versions
    monkeypatch.setenv(
        "SETUPTOOLS_SCM_PRETEND_METADATA_FOR_TEST_CALL_ORDER", "{node_date=2009-02-13}"
    )

    # Set up a git repository with a tag and known commit hash
    wd.commit_testfile("test")
    wd("git tag 1.0.0")
    wd.commit_testfile("test2")  # Add another commit to get distance
    monkeypatch.chdir(wd.cwd)

    # Create PyProjectData with equivalent configuration - no file I/O!
    project_name = "test-call-order"
    pyproject_data = PyProjectData.for_testing(
        tool_name="setuptools_scm",
        project_name=project_name,
        has_dynamic_version=True,
        project_present=True,
        section_present=True,
        local_scheme="no-local-version",
    )

    dist = create_clean_distribution(project_name)

    # Call both integration functions in order with direct data injection
    first_integration(dist, pyproject_data)
    second_integration(dist, pyproject_data)

    # Get the final version directly from the distribution
    final_version = dist.metadata.version

    # Assert the final version matches expectation
    # Some tests will fail here, demonstrating the bug where version_keyword doesn't override
    assert final_version == expected_final_version, (
        f"Expected version '{expected_final_version}' but got '{final_version}'"
    )


@pytest.mark.issue("xmlsec-regression")
@pytest.mark.skipif(
    sys.implementation.name == "pypy", reason="xmlsec build fails on PyPy in CI"
)
def test_xmlsec_download_regression(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test that pip download works for xmlsec package without causing setuptools_scm regression.

    This test ensures that downloading and building xmlsec from source doesn't fail
    due to setuptools_scm issues when using --no-build-isolation.
    """
    # Set up environment with setuptools_scm debug enabled
    monkeypatch.setenv("SETUPTOOLS_SCM_DEBUG", "1")
    monkeypatch.setenv("COLUMNS", "150")

    # Run pip download command with no-binary and no-build-isolation
    try:
        subprocess.run(
            [
                *(sys.executable, "-m", "pip", "download"),
                *("--no-binary", "xmlsec"),
                "--no-build-isolation",
                "-v",
                "xmlsec==1.3.16",
            ],
            cwd=tmp_path,
            timeout=300,
            check=True,
        )
    except subprocess.CalledProcessError as e:
        pytest.fail(f"pip download failed: {e}", pytrace=False)

    # The success of the subprocess.run call above means the regression is fixed.
    # pip download succeeded without setuptools_scm causing version conflicts.


@pytest.mark.issue(1252)
def test_version_file_written_to_build_directory(
    wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test that version files are written to both source and build directory.

    Version files are written to source during inference (for development)
    and to the build directory during build_py (for wheel contents).
    """
    monkeypatch.chdir(wd.cwd)

    wd.write(
        "pyproject.toml",
        textwrap.dedent("""\
            [build-system]
            requires = ["setuptools>=61", "setuptools-scm"]
            build-backend = "setuptools.build_meta"

            [project]
            name = "test-pkg"
            dynamic = ["version"]

            [tool.setuptools_scm]
            version_file = "test_pkg/_version.py"

            [tool.setuptools.packages.find]
            where = ["."]
        """),
    )

    pkg_dir = wd.cwd / "test_pkg"
    pkg_dir.mkdir()
    (pkg_dir / "__init__.py").write_text("from ._version import __version__\n")

    wd.commit_testfile()
    wd("git tag v1.0.0")

    _run_setuptools_setup(wd.cwd)

    version_file = pkg_dir / "_version.py"
    assert version_file.exists(), (
        "Version file should be written to source tree during inference"
    )
    assert "1.0.0" in version_file.read_text()

    # Build should also include version file in the wheel
    build_result = subprocess.run(
        [sys.executable, "-m", "build", "--wheel", "--no-isolation"],
        cwd=wd.cwd,
        capture_output=True,
        text=True,
        check=False,
    )

    # Build should succeed
    assert build_result.returncode == 0, (
        f"Build failed:\nstdout: {build_result.stdout}\nstderr: {build_result.stderr}"
    )

    # Verify wheel was created and contains the version file
    dist_dir = wd.cwd / "dist"
    wheels = list(dist_dir.glob("*.whl"))
    assert len(wheels) == 1, f"Expected 1 wheel, found {len(wheels)}"

    # Check wheel contents for version file
    import zipfile

    with zipfile.ZipFile(wheels[0], "r") as whl:
        names = whl.namelist()
        version_file_in_wheel = any("_version.py" in name for name in names)
        assert version_file_in_wheel, (
            f"Version file not found in wheel. Contents: {names}"
        )


@pytest.mark.issue(1252)
def test_version_file_src_layout_path_transformation(
    wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test that version files work correctly with src/ layout.

    When version_file is specified as 'src/mypackage/_version.py', the path
    should be transformed to 'mypackage/_version.py' in the build directory.
    """
    monkeypatch.chdir(wd.cwd)

    # Create a src/ layout project
    wd.write(
        "pyproject.toml",
        textwrap.dedent("""\
            [build-system]
            requires = ["setuptools>=61", "setuptools-scm"]
            build-backend = "setuptools.build_meta"

            [project]
            name = "test-src-pkg"
            dynamic = ["version"]

            [tool.setuptools_scm]
            # Path includes src/ prefix as user would configure it
            version_file = "src/test_src_pkg/_version.py"

            [tool.setuptools.packages.find]
            where = ["src"]
        """),
    )

    # Create src/ layout package structure
    src_dir = wd.cwd / "src"
    src_dir.mkdir()
    pkg_dir = src_dir / "test_src_pkg"
    pkg_dir.mkdir()
    (pkg_dir / "__init__.py").write_text("from ._version import __version__\n")

    # Commit so we have a valid git state
    wd.commit_testfile()
    wd("git tag v2.0.0")

    # Build wheel
    build_result = subprocess.run(
        [sys.executable, "-m", "build", "--wheel", "--no-isolation"],
        cwd=wd.cwd,
        capture_output=True,
        text=True,
        check=False,
    )

    assert build_result.returncode == 0, (
        f"Build failed:\nstdout: {build_result.stdout}\nstderr: {build_result.stderr}"
    )

    # Verify wheel contains version file at correct path (without src/ prefix)
    import zipfile

    dist_dir = wd.cwd / "dist"
    wheels = list(dist_dir.glob("*.whl"))
    assert len(wheels) == 1

    with zipfile.ZipFile(wheels[0], "r") as whl:
        names = whl.namelist()
        # Should be test_src_pkg/_version.py, NOT src/test_src_pkg/_version.py
        assert any(name == "test_src_pkg/_version.py" for name in names), (
            f"Expected 'test_src_pkg/_version.py' in wheel, got: {names}"
        )
        assert not any("src/" in name for name in names), (
            f"Found 'src/' prefix in wheel paths: {names}"
        )


@pytest.mark.issue(1252)
@pytest.mark.parametrize("editable_mode", ["compat", "strict"])
def test_editable_install_version_file(
    editable_mode: str, wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test that editable installs write version files to source by default.

    Version files are written to the source tree during inference, making
    them importable for editable installs without any extra configuration.
    """
    monkeypatch.chdir(wd.cwd)

    wd.write(
        "pyproject.toml",
        textwrap.dedent("""\
            [build-system]
            requires = ["setuptools>=64", "setuptools-scm"]
            build-backend = "setuptools.build_meta"

            [project]
            name = "test-editable-pkg"
            dynamic = ["version"]

            [tool.setuptools_scm]
            version_file = "test_editable_pkg/_version.py"

            [tool.setuptools.packages.find]
            where = ["."]
        """),
    )

    pkg_dir = wd.cwd / "test_editable_pkg"
    pkg_dir.mkdir()
    (pkg_dir / "__init__.py").write_text(
        "try:\n"
        "    from ._version import __version__\n"
        "except ImportError:\n"
        "    from importlib.metadata import version\n"
        "    __version__ = version('test-editable-pkg')\n"
    )

    wd.commit_testfile()
    wd("git tag v3.0.0")

    build_result = subprocess.run(
        [
            sys.executable,
            "-m",
            "pip",
            "wheel",
            "--no-build-isolation",
            "-e",
            ".",
            "-w",
            "dist",
            f"--config-settings=editable_mode={editable_mode}",
        ],
        cwd=wd.cwd,
        capture_output=True,
        text=True,
        check=False,
    )

    assert build_result.returncode == 0, (
        f"Editable build ({editable_mode}) failed:\n"
        f"stdout: {build_result.stdout}\nstderr: {build_result.stderr}"
    )

    version_file = pkg_dir / "_version.py"
    assert version_file.exists(), (
        f"Version file should be written to source by default. Mode: {editable_mode}"
    )

    content = version_file.read_text()
    assert "3.0.0" in content, f"Version file should contain '3.0.0': {content}"


@pytest.mark.issue(1298)
def test_editable_strict_includes_version_file(
    wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test that strict editable installs include version files in both locations.

    Version files are written to source during inference AND to the persistent
    auxiliary directory via build_py's get_outputs() registration.
    """
    monkeypatch.chdir(wd.cwd)

    wd.write(
        "pyproject.toml",
        textwrap.dedent("""\
            [build-system]
            requires = ["setuptools>=64", "setuptools-scm"]
            build-backend = "setuptools.build_meta"

            [project]
            name = "test-editable-strict-pkg"
            dynamic = ["version"]

            [tool.setuptools_scm]
            version_file = "test_editable_strict_pkg/_version.py"

            [tool.setuptools.packages.find]
            where = ["."]
        """),
    )

    pkg_dir = wd.cwd / "test_editable_strict_pkg"
    pkg_dir.mkdir()
    (pkg_dir / "__init__.py").write_text(
        "try:\n"
        "    from ._version import __version__\n"
        "except ImportError:\n"
        "    from importlib.metadata import version\n"
        "    __version__ = version('test-editable-strict-pkg')\n"
    )

    wd.commit_testfile()
    wd("git tag v4.0.0")

    dist_dir = wd.cwd / "dist"
    dist_dir.mkdir(exist_ok=True)

    build_result = subprocess.run(
        [
            sys.executable,
            "-c",
            (
                "from setuptools.build_meta import build_editable; "
                "build_editable('dist', config_settings={'editable_mode': 'strict'})"
            ),
        ],
        cwd=wd.cwd,
        capture_output=True,
        text=True,
        check=False,
    )

    assert build_result.returncode == 0, (
        f"Strict editable build failed:\n"
        f"stdout: {build_result.stdout}\nstderr: {build_result.stderr}"
    )

    # Version file should be in source (written during inference by default)
    version_file = pkg_dir / "_version.py"
    assert version_file.exists(), (
        "Version file should be written to source tree during inference"
    )
    assert "4.0.0" in version_file.read_text()

    # Version file should ALSO be in the persistent auxiliary directory
    build_dir = wd.cwd / "build"
    editable_dirs = list(build_dir.glob("__editable__.*"))
    assert editable_dirs, f"Expected editable build dir under {build_dir}"
    aux_version_file = editable_dirs[0] / "test_editable_strict_pkg" / "_version.py"
    assert aux_version_file.exists(), (
        f"Version file should be in auxiliary dir for strict editable installs. "
        f"Checked: {aux_version_file}"
    )
    assert "4.0.0" in aux_version_file.read_text()


@pytest.mark.issue(1252)
def test_readonly_source_directory_build(
    wd: WorkDir, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """Test building from a read-only source directory succeeds.

    This is the core scenario from issue #1252: building a package when the
    source directory is read-only (e.g., Bazel builds). Version files should
    be written to the build directory, not the source tree.
    """
    import stat

    monkeypatch.chdir(wd.cwd)

    # Create a minimal pyproject.toml with version_file configured
    wd.write(
        "pyproject.toml",
        textwrap.dedent("""\
            [build-system]
            requires = ["setuptools>=61", "setuptools-scm"]
            build-backend = "setuptools.build_meta"

            [project]
            name = "readonly-test-pkg"
            dynamic = ["version"]

            [tool.setuptools_scm]
            version_file = "readonly_test_pkg/_version.py"

            [tool.setuptools.packages.find]
            where = ["."]
        """),
    )

    # Create minimal package structure
    pkg_dir = wd.cwd / "readonly_test_pkg"
    pkg_dir.mkdir()
    (pkg_dir / "__init__.py").write_text("from ._version import __version__\n")

    # Commit so we have a valid git state
    wd.commit_testfile()
    wd("git tag v5.0.0")

    # Make the package directory read-only to simulate Bazel scenario
    original_mode = pkg_dir.stat().st_mode
    try:
        # Remove write permissions
        pkg_dir.chmod(stat.S_IRUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IROTH)

        # Verify we can't write to the directory.
        # On some Windows environments chmod() does not enforce directory
        # writability for newly created files, so skip if we cannot simulate
        # the read-only constraint reliably.
        version_file = pkg_dir / "_version.py"
        try:
            version_file.write_text("test")
            version_file.unlink(missing_ok=True)
            pytest.skip(
                "cannot enforce read-only directory semantics in this environment"
            )
        except PermissionError:
            pass  # Expected - directory is read-only

        # Disable writing to source since the directory is read-only.
        # build_py will still write version files to build_lib.
        build_result = subprocess.run(
            [sys.executable, "-m", "build", "--wheel", "--no-isolation"],
            cwd=wd.cwd,
            capture_output=True,
            text=True,
            check=False,
            env={**os.environ, "SETUPTOOLS_SCM_WRITE_TO_SOURCE": "0"},
        )

        # Build should succeed despite read-only source
        assert build_result.returncode == 0, (
            f"Build from read-only source failed:\n"
            f"stdout: {build_result.stdout}\nstderr: {build_result.stderr}"
        )

        # Verify wheel was created and contains the version file
        import zipfile

        dist_dir = wd.cwd / "dist"
        wheels = list(dist_dir.glob("*.whl"))
        assert len(wheels) == 1

        with zipfile.ZipFile(wheels[0], "r") as whl:
            names = whl.namelist()
            assert any("_version.py" in name for name in names), (
                f"Version file not found in wheel. Contents: {names}"
            )

            # Read and verify version content
            version_content = None
            for name in names:
                if "_version.py" in name:
                    version_content = whl.read(name).decode("utf-8")
                    break
            assert version_content is not None
            assert "5.0.0" in version_content

    finally:
        # Restore original permissions for cleanup
        pkg_dir.chmod(original_mode)


@pytest.mark.issue(1252)
def test_legacy_write_to_build_directory(
    wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test that legacy write_to configuration also writes to build directory.

    The write_to option is the older way to configure version file writing.
    It should also be deferred to build_py just like version_file.
    """
    monkeypatch.chdir(wd.cwd)

    # Create a pyproject.toml using legacy write_to
    wd.write(
        "pyproject.toml",
        textwrap.dedent("""\
            [build-system]
            requires = ["setuptools>=61", "setuptools-scm"]
            build-backend = "setuptools.build_meta"

            [project]
            name = "legacy-write-to-pkg"
            dynamic = ["version"]

            [tool.setuptools_scm]
            write_to = "legacy_pkg/_version.py"

            [tool.setuptools.packages.find]
            where = ["."]
        """),
    )

    # Create minimal package structure
    pkg_dir = wd.cwd / "legacy_pkg"
    pkg_dir.mkdir()
    (pkg_dir / "__init__.py").write_text("from ._version import __version__\n")

    # Commit so we have a valid git state
    wd.commit_testfile()
    wd("git tag v6.0.0")

    _run_setuptools_setup(wd.cwd)

    version_file = pkg_dir / "_version.py"
    assert version_file.exists(), (
        "Legacy write_to should write to source during inference"
    )
    assert "6.0.0" in version_file.read_text()

    # Build wheel should also include it
    build_result = subprocess.run(
        [sys.executable, "-m", "build", "--wheel", "--no-isolation"],
        cwd=wd.cwd,
        capture_output=True,
        text=True,
        check=False,
    )

    assert build_result.returncode == 0, (
        f"Build failed:\nstdout: {build_result.stdout}\nstderr: {build_result.stderr}"
    )

    # Verify wheel contains version file
    import zipfile

    dist_dir = wd.cwd / "dist"
    wheels = list(dist_dir.glob("*.whl"))
    assert len(wheels) == 1

    with zipfile.ZipFile(wheels[0], "r") as whl:
        names = whl.namelist()
        assert any("_version.py" in name for name in names), (
            f"Version file not found in wheel. Contents: {names}"
        )


@pytest.mark.issue(1252)
def test_version_file_template_in_build(
    wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test that custom version_file_template works during build_py.

    Verifies that the template is correctly applied when writing the version
    file to the build directory.
    """
    monkeypatch.chdir(wd.cwd)

    # Create a pyproject.toml with custom template
    wd.write(
        "pyproject.toml",
        textwrap.dedent("""\
            [build-system]
            requires = ["setuptools>=61", "setuptools-scm"]
            build-backend = "setuptools.build_meta"

            [project]
            name = "template-test-pkg"
            dynamic = ["version"]

            [tool.setuptools_scm]
            version_file = "template_pkg/_version.py"
            version_file_template = '''
            # Custom template for testing
            __version__ = "{version}"
            VERSION_TUPLE = {version_tuple}
            CUSTOM_MARKER = "build_py_template_test"
            '''

            [tool.setuptools.packages.find]
            where = ["."]
        """),
    )

    # Create minimal package structure
    pkg_dir = wd.cwd / "template_pkg"
    pkg_dir.mkdir()
    (pkg_dir / "__init__.py").write_text("from ._version import __version__\n")

    # Commit so we have a valid git state
    wd.commit_testfile()
    wd("git tag v7.1.2")

    # Build wheel
    build_result = subprocess.run(
        [sys.executable, "-m", "build", "--wheel", "--no-isolation"],
        cwd=wd.cwd,
        capture_output=True,
        text=True,
        check=False,
    )

    assert build_result.returncode == 0, (
        f"Build failed:\nstdout: {build_result.stdout}\nstderr: {build_result.stderr}"
    )

    # Verify wheel contains version file with custom template content
    import zipfile

    dist_dir = wd.cwd / "dist"
    wheels = list(dist_dir.glob("*.whl"))
    assert len(wheels) == 1

    with zipfile.ZipFile(wheels[0], "r") as whl:
        names = whl.namelist()

        # Find and read version file
        version_content = None
        for name in names:
            if "_version.py" in name:
                version_content = whl.read(name).decode("utf-8")
                break

        assert version_content is not None, f"Version file not found in {names}"
        assert "CUSTOM_MARKER" in version_content, (
            f"Custom template marker not found. Content:\n{version_content}"
        )
        assert "build_py_template_test" in version_content
        assert "7.1.2" in version_content
        assert "VERSION_TUPLE" in version_content


@pytest.mark.issue(1252)
def test_custom_build_py_still_writes_version_file(
    wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Custom build_py should not disable setuptools-scm version file writing."""
    monkeypatch.chdir(wd.cwd)

    wd.write(
        "pyproject.toml",
        textwrap.dedent("""\
            [build-system]
            requires = ["setuptools>=61", "setuptools-scm"]
            build-backend = "setuptools.build_meta"
        """),
    )

    wd.write(
        "setup.py",
        textwrap.dedent("""\
            from pathlib import Path

            from setuptools import setup
            from setuptools.command.build_py import build_py as _build_py


            class CustomBuildPy(_build_py):
                def run(self):
                    super().run()
                    Path("custom_build_py_ran.txt").write_text("ran", encoding="utf-8")


            setup(
                name="custom-build-py-pkg",
                packages=["custom_build_py_pkg"],
                use_scm_version={"version_file": "custom_build_py_pkg/_version.py"},
                cmdclass={"build_py": CustomBuildPy},
            )
        """),
    )

    pkg_dir = wd.cwd / "custom_build_py_pkg"
    pkg_dir.mkdir()
    (pkg_dir / "__init__.py").write_text("from ._version import __version__\n")

    wd.commit_testfile()
    wd("git tag v8.0.0")

    build_result = subprocess.run(
        [sys.executable, "-m", "build", "--wheel", "--no-isolation"],
        cwd=wd.cwd,
        capture_output=True,
        text=True,
        check=False,
    )

    assert build_result.returncode == 0, (
        f"Build failed:\nstdout: {build_result.stdout}\nstderr: {build_result.stderr}"
    )
    assert (wd.cwd / "custom_build_py_ran.txt").exists(), (
        "Project custom build_py should still run"
    )

    import zipfile

    dist_dir = wd.cwd / "dist"
    wheels = list(dist_dir.glob("*.whl"))
    assert len(wheels) == 1

    with zipfile.ZipFile(wheels[0], "r") as whl:
        names = whl.namelist()
        assert "custom_build_py_pkg/_version.py" in names, (
            f"Expected version file in wheel, got: {names}"
        )


@pytest.mark.issue(1298)
def test_version_file_contains_commit_node_in_wheel(
    wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test that the commit node (hash) is present in the version file inside a wheel.

    The default .py template writes commit_id = {scm_version.short_node!r}.
    This verifies the full pipeline: git commit -> ScmVersion.node -> version file
    in the built wheel.
    """
    monkeypatch.chdir(wd.cwd)

    wd.write(
        "pyproject.toml",
        textwrap.dedent("""\
            [build-system]
            requires = ["setuptools>=64", "setuptools-scm"]
            build-backend = "setuptools.build_meta"

            [project]
            name = "node-test-pkg"
            dynamic = ["version"]

            [tool.setuptools_scm]
            version_file = "node_test_pkg/_version.py"

            [tool.setuptools.packages.find]
            where = ["."]
        """),
    )

    pkg_dir = wd.cwd / "node_test_pkg"
    pkg_dir.mkdir()
    (pkg_dir / "__init__.py").write_text(
        "from ._version import __version__, commit_id\n"
    )

    wd.commit_testfile()
    wd("git tag v1.0.0")

    # Get the commit hash so we can verify it in the version file
    git_node = wd("git rev-parse HEAD").strip()
    short_node = git_node[:7]

    build_result = subprocess.run(
        [sys.executable, "-m", "build", "--wheel", "--no-isolation"],
        cwd=wd.cwd,
        capture_output=True,
        text=True,
        check=False,
    )

    assert build_result.returncode == 0, (
        f"Build failed:\nstdout: {build_result.stdout}\nstderr: {build_result.stderr}"
    )

    import zipfile

    dist_dir = wd.cwd / "dist"
    wheels = list(dist_dir.glob("*.whl"))
    assert len(wheels) == 1

    with zipfile.ZipFile(wheels[0], "r") as whl:
        version_content = None
        for name in whl.namelist():
            if "_version.py" in name:
                version_content = whl.read(name).decode("utf-8")
                break

        assert version_content is not None, "Version file not found in wheel"
        assert "1.0.0" in version_content
        assert "commit_id" in version_content
        assert short_node in version_content, (
            f"Expected short node '{short_node}' in version file. "
            f"Content:\n{version_content}"
        )


@pytest.mark.issue(1298)
def test_version_file_contains_commit_node_in_editable_strict(
    wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Test that the commit node is present in the version file for strict editable installs.

    In strict mode, version files should be copied to the persistent auxiliary
    directory with the full commit_id included.
    """
    monkeypatch.chdir(wd.cwd)

    wd.write(
        "pyproject.toml",
        textwrap.dedent("""\
            [build-system]
            requires = ["setuptools>=64", "setuptools-scm"]
            build-backend = "setuptools.build_meta"

            [project]
            name = "node-editable-pkg"
            dynamic = ["version"]

            [tool.setuptools_scm]
            version_file = "node_editable_pkg/_version.py"

            [tool.setuptools.packages.find]
            where = ["."]
        """),
    )

    pkg_dir = wd.cwd / "node_editable_pkg"
    pkg_dir.mkdir()
    (pkg_dir / "__init__.py").write_text(
        "from ._version import __version__, commit_id\n"
    )

    wd.commit_testfile()
    wd("git tag v2.0.0")

    git_node = wd("git rev-parse HEAD").strip()
    short_node = git_node[:7]

    dist_dir = wd.cwd / "dist"
    dist_dir.mkdir(exist_ok=True)

    build_result = subprocess.run(
        [
            sys.executable,
            "-c",
            (
                "from setuptools.build_meta import build_editable; "
                "build_editable('dist', config_settings={'editable_mode': 'strict'})"
            ),
        ],
        cwd=wd.cwd,
        capture_output=True,
        text=True,
        check=False,
    )

    assert build_result.returncode == 0, (
        f"Strict editable build failed:\n"
        f"stdout: {build_result.stdout}\nstderr: {build_result.stderr}"
    )

    # Version file should be in source (written during inference)
    source_version_file = pkg_dir / "_version.py"
    assert source_version_file.exists()
    content = source_version_file.read_text()
    assert "2.0.0" in content
    assert "commit_id" in content
    assert short_node in content, (
        f"Expected short node '{short_node}' in source version file. Content:\n{content}"
    )

    # Version file should ALSO be in the auxiliary directory
    build_dir = wd.cwd / "build"
    editable_dirs = list(build_dir.glob("__editable__.*"))
    assert editable_dirs, f"Expected editable build dir under {build_dir}"
    aux_version_file = editable_dirs[0] / "node_editable_pkg" / "_version.py"
    assert aux_version_file.exists(), (
        f"Version file missing from auxiliary dir: {aux_version_file}"
    )
    aux_content = aux_version_file.read_text()
    assert short_node in aux_content


def _sdist_names(wd: WorkDir) -> list[str]:
    """Build an sdist in *wd* and return the list of archive member names."""
    import shutil
    import tarfile

    dist_dir = wd.cwd / "dist"
    if dist_dir.exists():
        shutil.rmtree(dist_dir)
    egg_info_dirs = list(wd.cwd.glob("*.egg-info"))
    for d in egg_info_dirs:
        shutil.rmtree(d)

    build_result = subprocess.run(
        [sys.executable, "-m", "build", "--sdist", "--no-isolation"],
        cwd=wd.cwd,
        capture_output=True,
        text=True,
        check=False,
    )
    assert build_result.returncode == 0, (
        f"sdist build failed:\nstdout: {build_result.stdout}\n"
        f"stderr: {build_result.stderr}"
    )

    sdists = list(dist_dir.glob("*.tar.gz"))
    assert len(sdists) == 1, f"Expected 1 sdist, found {len(sdists)}"

    with tarfile.open(sdists[0], "r:gz") as tar:
        return tar.getnames()


def test_manifest_in_excludes_scm_tracked_files(
    wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
    """MANIFEST.in exclusions must be honoured even when the egg_info mixin
    supplies tracked files directly (bypassing walk_revctrl).

    First verifies that without the exclusion the file IS included,
    then adds ``exclude secret.txt`` and verifies the file is gone.
    """
    monkeypatch.chdir(wd.cwd)

    wd.write(
        "pyproject.toml",
        textwrap.dedent("""\
            [build-system]
            requires = ["setuptools>=61", "setuptools-scm"]
            build-backend = "setuptools.build_meta"

            [project]
            name = "test-pkg"
            dynamic = ["version"]

            [tool.setuptools_scm]
        """),
    )

    pkg_dir = wd.cwd / "test_pkg"
    pkg_dir.mkdir()
    (pkg_dir / "__init__.py").write_text("")

    wd.write("secret.txt", "do-not-ship")

    # Empty MANIFEST.in -- no exclusions
    wd.write("MANIFEST.in", "")

    wd(wd.add_command)
    wd.commit()
    wd("git tag v1.0.0")

    # -- Phase 1: empty MANIFEST.in → file is included ----------------------
    names = _sdist_names(wd)

    assert any("secret.txt" in n for n in names), (
        f"secret.txt should be included when MANIFEST.in is empty: {names}"
    )
    assert any("test_pkg/__init__.py" in n for n in names), (
        f"test_pkg/__init__.py should be in sdist: {names}"
    )

    # -- Phase 2: MANIFEST.in excludes the file → gone ----------------------
    wd.write("MANIFEST.in", "exclude secret.txt\n")
    wd(wd.add_command)
    wd.commit()

    names = _sdist_names(wd)

    assert not any("secret.txt" in n for n in names), (
        f"secret.txt should have been excluded by MANIFEST.in: {names}"
    )
    assert any("test_pkg/__init__.py" in n for n in names), (
        f"test_pkg/__init__.py should still be in sdist: {names}"
    )


class TestIsInsidePackage:
    """Unit tests for _is_inside_package."""

    def test_root_level_file_not_inside_package(self) -> None:
        from setuptools_scm._integration.build_py import _is_inside_package

        assert not _is_inside_package("VERSION", ["mypackage"])

    def test_root_level_py_file_not_inside_package(self) -> None:
        from setuptools_scm._integration.build_py import _is_inside_package

        assert not _is_inside_package("_version.py", ["mypackage"])

    def test_file_inside_package(self) -> None:
        from setuptools_scm._integration.build_py import _is_inside_package

        assert _is_inside_package("mypackage/_version.py", ["mypackage"])

    def test_file_inside_nested_package(self) -> None:
        from setuptools_scm._integration.build_py import _is_inside_package

        assert _is_inside_package("mypackage/sub/_version.py", ["mypackage"])

    def test_file_inside_dotted_package(self) -> None:
        from setuptools_scm._integration.build_py import _is_inside_package

        assert _is_inside_package("mypackage/sub/_version.py", ["mypackage.sub"])

    def test_file_not_in_any_package(self) -> None:
        from setuptools_scm._integration.build_py import _is_inside_package

        assert not _is_inside_package("other/version.h", ["mypackage"])

    def test_no_packages(self) -> None:
        from setuptools_scm._integration.build_py import _is_inside_package

        assert not _is_inside_package("mypackage/_version.py", None)
        assert not _is_inside_package("mypackage/_version.py", [])

    @pytest.mark.skipif(os.name != "posix", reason="posix absolute path form")
    def test_rejects_absolute_path(self) -> None:
        from setuptools_scm._integration.build_py import _is_inside_package

        with pytest.raises(ValueError, match="must be relative"):
            _is_inside_package("/etc/passwd", ["mypackage"])

    @pytest.mark.skipif(os.name != "nt", reason="windows absolute path form")
    def test_rejects_absolute_path_windows(self) -> None:
        from setuptools_scm._integration.build_py import _is_inside_package

        with pytest.raises(ValueError, match="must be relative"):
            _is_inside_package("C:\\Users\\evil.py", ["mypackage"])

    def test_rejects_traversal(self) -> None:
        from setuptools_scm._integration.build_py import _is_inside_package

        with pytest.raises(ValueError, match="must not contain"):
            _is_inside_package("mypackage/../../etc/passwd", ["mypackage"])


class TestSanitizeRelativePath:
    """Unit tests for _sanitize_relative_path."""

    def test_accepts_simple_relative(self) -> None:
        from setuptools_scm._integration.build_py import _sanitize_relative_path

        result = _sanitize_relative_path("mypackage/_version.py")
        assert result == Path("mypackage/_version.py")

    @pytest.mark.skipif(os.name != "posix", reason="posix absolute path form")
    def test_rejects_absolute_posix(self) -> None:
        from setuptools_scm._integration.build_py import _sanitize_relative_path

        with pytest.raises(ValueError, match="must be relative"):
            _sanitize_relative_path("/tmp/evil.py")

    @pytest.mark.skipif(os.name != "nt", reason="windows absolute path form")
    def test_rejects_absolute_windows(self) -> None:
        from setuptools_scm._integration.build_py import _sanitize_relative_path

        with pytest.raises(ValueError, match="must be relative"):
            _sanitize_relative_path("C:\\Windows\\evil.py")

    def test_rejects_dotdot(self) -> None:
        from setuptools_scm._integration.build_py import _sanitize_relative_path

        with pytest.raises(ValueError, match="must not contain"):
            _sanitize_relative_path("pkg/../../outside.py")

    def test_accepts_root_level(self) -> None:
        from setuptools_scm._integration.build_py import _sanitize_relative_path

        result = _sanitize_relative_path("VERSION.txt")
        assert result == Path("VERSION.txt")


@pytest.mark.issue(1364)
def test_root_level_version_file_not_in_wheel(
    wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Root-level version files (e.g. VERSION) must not end up in the wheel.

    Regression test for GH-1364: upgrading to setuptools-scm 10 caused
    files like ``VERSION`` to appear at the wheel root because build_py
    unconditionally wrote them to ``build_lib``.
    """
    monkeypatch.chdir(wd.cwd)

    wd.write(
        "pyproject.toml",
        textwrap.dedent("""\
            [build-system]
            requires = ["setuptools>=61", "setuptools-scm"]
            build-backend = "setuptools.build_meta"

            [project]
            name = "root-version-file-pkg"
            dynamic = ["version"]

            [tool.setuptools_scm]
            write_to = "VERSION.txt"

            [tool.setuptools.packages.find]
            where = ["."]
        """),
    )

    pkg_dir = wd.cwd / "root_version_file_pkg"
    pkg_dir.mkdir()
    (pkg_dir / "__init__.py").write_text("")

    wd.commit_testfile()
    wd("git tag v1.0.0")

    build_result = subprocess.run(
        [sys.executable, "-m", "build", "--wheel", "--no-isolation"],
        cwd=wd.cwd,
        capture_output=True,
        text=True,
        check=False,
    )

    assert build_result.returncode == 0, (
        f"Build failed:\nstdout: {build_result.stdout}\nstderr: {build_result.stderr}"
    )

    import zipfile

    dist_dir = wd.cwd / "dist"
    wheels = list(dist_dir.glob("*.whl"))
    assert len(wheels) == 1

    with zipfile.ZipFile(wheels[0], "r") as whl:
        names = whl.namelist()
        assert not any(n == "VERSION.txt" for n in names), (
            f"Root-level VERSION.txt should NOT be in wheel, got: {names}"
        )
