"""Contract tests for the future no-new-getattr CLI wrapper."""

from __future__ import annotations

import importlib.util
import re
import subprocess
import sys
import textwrap
import types
from pathlib import Path

import pytest

REPO_ROOT = Path(__file__).resolve().parents[4]
SCRIPTS_DIR = REPO_ROOT / "scripts"
SCRIPT_PATH = SCRIPTS_DIR / "check_no_new_getattr.py"
GUARD_HELPER_PATH = SCRIPTS_DIR / "ast_grep_guard.py"


def load_guard_module() -> types.ModuleType:
    if str(SCRIPTS_DIR) not in sys.path:
        sys.path.insert(0, str(SCRIPTS_DIR))
    spec = importlib.util.spec_from_file_location("ast_grep_guard_under_test", GUARD_HELPER_PATH)
    assert spec is not None
    assert spec.loader is not None
    module = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = module
    spec.loader.exec_module(module)
    return module


def git(repo: Path, *args: str) -> str:
    completed = subprocess.run(
        ["git", *args],
        cwd=repo,
        text=True,
        capture_output=True,
        check=True,
    )
    return completed.stdout.strip()


def run_script(repo: Path, *args: str) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        ["python3", str(SCRIPT_PATH), *args],
        cwd=repo,
        text=True,
        capture_output=True,
        check=False,
    )


def write_repo_file(repo: Path, relative_path: str, content: str) -> None:
    path = repo / relative_path
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(textwrap.dedent(content).lstrip(), encoding="utf-8")


def commit_all(repo: Path, message: str) -> None:
    git(repo, "add", ".")
    git(repo, "commit", "-m", message)


def init_repo(repo: Path) -> None:
    git(repo, "init", "-b", "main")
    git(repo, "config", "user.name", "Tester")
    git(repo, "config", "user.email", "tester@example.com")


def checkout_feature_branch(repo: Path) -> None:
    git(repo, "checkout", "-b", "feature/test-branch")


def stderr_lines(result: subprocess.CompletedProcess[str]) -> list[str]:
    return [line for line in result.stderr.splitlines() if line.strip()]


def assert_has_actionable_violation(stderr: str, path: str) -> None:
    assert re.search(rf"{re.escape(path)}:\d+:", stderr), stderr
    assert "no-new-getattr" in stderr


def main_branch_rev(repo: Path) -> str:
    return git(repo, "rev-parse", "main")


@pytest.mark.parametrize(
    ("source_line", "rule_id"),
    [
        (
            "value = getattr(module, name)  # guard-ignore: no-new-getattr -- lazy export proxy",
            "no-new-getattr",
        ),
        (
            "session.rollback()  # guard-ignore: no-new-controller-sqlalchemy -- decorator owns rollback",
            "no-new-controller-sqlalchemy",
        ),
    ],
)
def test_has_reasoned_guard_ignore_accepts_custom_rules(source_line: str, rule_id: str) -> None:
    module = load_guard_module()

    assert module.has_reasoned_guard_ignore(source_line, rule_id)


@pytest.mark.parametrize(
    ("source_line", "rule_id"),
    [
        ("value = getattr(module, name)  # noqa: no-new-getattr legacy marker", "no-new-getattr"),
        ("value = getattr(module, name)  # guard-ignore: no-new-getattr", "no-new-getattr"),
        (
            "value = getattr(module, name)  # guard-ignore: another-rule -- wrong rule",
            "no-new-getattr",
        ),
    ],
)
def test_has_reasoned_guard_ignore_rejects_invalid_markers(source_line: str, rule_id: str) -> None:
    module = load_guard_module()

    assert not module.has_reasoned_guard_ignore(source_line, rule_id)


def test_resolve_ast_grep_command_prefers_ast_grep(monkeypatch: pytest.MonkeyPatch) -> None:
    module = load_guard_module()
    monkeypatch.setattr(
        module.shutil,
        "which",
        lambda name: f"/usr/bin/{name}" if name == "ast-grep" else None,
    )

    assert module.resolve_ast_grep_command() == ["ast-grep"]


def test_resolve_ast_grep_command_falls_back_to_uvx(monkeypatch: pytest.MonkeyPatch) -> None:
    module = load_guard_module()
    monkeypatch.setattr(
        module.shutil,
        "which",
        lambda name: "/usr/bin/uvx" if name == "uvx" else None,
    )

    assert module.resolve_ast_grep_command() == ["uvx", "--from", "ast-grep-cli", "ast-grep"]


def test_resolve_ast_grep_command_never_uses_sg(monkeypatch: pytest.MonkeyPatch) -> None:
    module = load_guard_module()
    monkeypatch.setattr(
        module.shutil,
        "which",
        lambda name: f"/usr/bin/{name}" if name in {"sg", "uvx"} else None,
    )

    assert module.resolve_ast_grep_command() == ["uvx", "--from", "ast-grep-cli", "ast-grep"]


def test_resolve_ast_grep_command_rejects_sg_only(monkeypatch: pytest.MonkeyPatch) -> None:
    module = load_guard_module()
    monkeypatch.setattr(
        module.shutil,
        "which",
        lambda name: "/usr/bin/sg" if name == "sg" else None,
    )

    with pytest.raises(RuntimeError, match="ast-grep executable not found"):
        module.resolve_ast_grep_command()


def test_resolve_ast_grep_command_raises_without_explicit_binary(monkeypatch: pytest.MonkeyPatch) -> None:
    module = load_guard_module()
    monkeypatch.setattr(module.shutil, "which", lambda _name: None)

    with pytest.raises(RuntimeError, match="ast-grep executable not found"):
        module.resolve_ast_grep_command()


def test_cli_requires_explicit_diff_source(tmp_path: Path) -> None:
    result = run_script(tmp_path)

    assert result.returncode == 2
    assert "one of the arguments --staged --base-rev is required" in result.stderr


def test_cli_rejects_mixed_diff_sources(tmp_path: Path) -> None:
    result = run_script(tmp_path, "--staged", "--base-rev", "deadbeef")

    assert result.returncode == 2
    assert "not allowed with argument" in result.stderr


def test_cli_help_exposes_only_new_diff_source_flags(tmp_path: Path) -> None:
    help_result = run_script(tmp_path, "--help")

    assert help_result.returncode == 0
    assert "--staged" in help_result.stdout
    assert "--base-rev" in help_result.stdout
    assert "--mode" not in help_result.stdout
    assert "--merge-target" not in help_result.stdout

    result = run_script(tmp_path, "--staged", "--mode", "ci")

    assert result.returncode == 2
    assert "unrecognized arguments: --mode ci" in result.stderr

    result = run_script(tmp_path, "--base-rev", "deadbeef", "--merge-target", "main")

    assert result.returncode == 2
    assert "unrecognized arguments: --merge-target main" in result.stderr


def test_style_workflow_wires_no_new_getattr_guard() -> None:
    workflow = (REPO_ROOT / ".github" / "workflows" / "style.yml").read_text(encoding="utf-8")
    assert re.search(
        r"(?ms)^on:\n  workflow_call:\n    inputs:\n      base-rev:\n        required: true\n        type: string\n",
        workflow,
    )
    python_style_job = re.search(
        r"(?ms)^  python-style:\n(?P<job>.*?)(?=^  [a-z0-9-]+:\n|\Z)",
        workflow,
    )
    assert python_style_job is not None

    job_text = python_style_job.group("job")
    checkout_step = re.search(
        r"(?ms)^      - name: Checkout code\n(?P<step>.*?)(?=^      - name: |\Z)",
        job_text,
    )
    assert checkout_step is not None
    assert "fetch-depth: 0" in checkout_step.group("step")

    changed_files_step = re.search(
        r"(?ms)^      - name: Check changed files\n.*?^          files: \|\n(?P<files>(?:^            \S[^\n]*\n)+)",
        job_text,
    )
    assert changed_files_step is not None

    files_block = changed_files_step.group("files")
    assert "api/**\n" in files_block
    assert "scripts/check_no_new_getattr.py\n" in files_block
    assert "scripts/ast_grep_rules/no_new_getattr.yml\n" in files_block
    assert ".github/workflows/style.yml\n" in files_block
    assert ".github/workflows/main-ci.yml\n" in files_block

    guard_command = 'scripts/check_no_new_getattr.py --base-rev "${{ inputs.base-rev }}"'
    assert guard_command in job_text

    guard_step = re.search(
        rf"(?ms)^      - name: Run No New Getattr Guard\n"
        rf"(?P<step>.*?{re.escape(guard_command)}.*?)(?=^      - name: |\Z)",
        job_text,
    )
    assert guard_step is not None

    assert "GITHUB_BASE_SHA" not in guard_step.group("step")


def test_main_ci_passes_style_base_rev_input() -> None:
    workflow = (REPO_ROOT / ".github" / "workflows" / "main-ci.yml").read_text(encoding="utf-8")
    style_job = re.search(
        r"(?ms)^  style-check:\n(?P<job>.*?)(?=^  [a-z0-9-]+:\n|\Z)",
        workflow,
    )
    assert style_job is not None
    assert "uses: ./.github/workflows/style.yml" in style_job.group("job")
    assert (
        "base-rev: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}"
        in style_job.group("job")
    )

    api_filter = re.search(
        r"(?ms)^            api:\n(?P<filter>(?:^              - '[^']+'\n)+)",
        workflow,
    )
    assert api_filter is not None
    filter_text = api_filter.group("filter")
    assert "scripts/check_no_new_getattr.py" in filter_text
    assert "scripts/ast_grep_rules/no_new_getattr.yml" in filter_text
    assert ".github/workflows/style.yml" in filter_text
    assert ".github/workflows/main-ci.yml" in filter_text


def test_base_rev_mode_passes_when_only_legacy_getattr_exists(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/legacy.py",
        """
        def read_value(obj):
            return getattr(obj, "legacy_name", None)
        """,
    )
    commit_all(tmp_path, "baseline")
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "pkg/other.py",
        """
        def meaning() -> int:
            return 42
        """,
    )
    commit_all(tmp_path, "unrelated change")

    result = run_script(tmp_path, "--base-rev", main_branch_rev(tmp_path))

    assert result.returncode == 0, result.stderr


def test_base_rev_mode_fails_for_new_file_with_getattr(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/existing.py",
        """
        def stable() -> str:
            return "ok"
        """,
    )
    commit_all(tmp_path, "baseline")
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "pkg/new_usage.py",
        """
        def read_value(obj):
            return getattr(obj, "new_name", None)
        """,
    )
    commit_all(tmp_path, "add new getattr usage")

    result = run_script(tmp_path, "--base-rev", main_branch_rev(tmp_path))

    assert result.returncode == 1
    assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")


def test_base_rev_mode_fails_for_new_file_with_two_arg_getattr(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/existing.py",
        """
        def stable() -> str:
            return "ok"
        """,
    )
    commit_all(tmp_path, "baseline")
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "pkg/new_usage.py",
        """
        def read_value(obj):
            return getattr(obj, "dynamic_name")
        """,
    )
    commit_all(tmp_path, "add new two-arg getattr usage")

    result = run_script(tmp_path, "--base-rev", main_branch_rev(tmp_path))

    assert result.returncode == 1
    assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")


def test_base_rev_mode_fails_for_new_file_with_builtins_getattr(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/existing.py",
        """
        def stable() -> str:
            return "ok"
        """,
    )
    commit_all(tmp_path, "baseline")
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "pkg/new_usage.py",
        """
        import builtins


        def read_value(obj):
            return builtins.getattr(obj, "dynamic_name", None)
        """,
    )
    commit_all(tmp_path, "add new builtins getattr usage")

    result = run_script(tmp_path, "--base-rev", main_branch_rev(tmp_path))

    assert result.returncode == 1
    assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")


def test_base_rev_mode_fails_for_new_file_with_two_arg_builtins_getattr(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/existing.py",
        """
        def stable() -> str:
            return "ok"
        """,
    )
    commit_all(tmp_path, "baseline")
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "pkg/new_usage.py",
        """
        import builtins


        def read_value(obj):
            return builtins.getattr(obj, "dynamic_name")
        """,
    )
    commit_all(tmp_path, "add new two-arg builtins getattr usage")

    result = run_script(tmp_path, "--base-rev", main_branch_rev(tmp_path))

    assert result.returncode == 1
    assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")


def test_base_rev_mode_fails_for_new_file_with_dunder_builtins_getattr(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/existing.py",
        """
        def stable() -> str:
            return "ok"
        """,
    )
    commit_all(tmp_path, "baseline")
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "pkg/new_usage.py",
        """
        def read_value(obj):
            return __builtins__.getattr(obj, "dynamic_name", None)
        """,
    )
    commit_all(tmp_path, "add new dunder builtins getattr usage")

    result = run_script(tmp_path, "--base-rev", main_branch_rev(tmp_path))

    assert result.returncode == 1
    assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")


def test_base_rev_mode_fails_for_new_file_with_two_arg_dunder_builtins_getattr(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/existing.py",
        """
        def stable() -> str:
            return "ok"
        """,
    )
    commit_all(tmp_path, "baseline")
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "pkg/new_usage.py",
        """
        def read_value(obj):
            return __builtins__.getattr(obj, "dynamic_name")
        """,
    )
    commit_all(tmp_path, "add new two-arg dunder builtins getattr usage")

    result = run_script(tmp_path, "--base-rev", main_branch_rev(tmp_path))

    assert result.returncode == 1
    assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")


def test_base_rev_mode_uses_provided_base_revision_not_head_parent(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/existing.py",
        """
        def stable() -> str:
            return "ok"
        """,
    )
    commit_all(tmp_path, "baseline")
    base_rev = main_branch_rev(tmp_path)
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "pkg/introduced_earlier.py",
        """
        def read_value(obj):
            return getattr(obj, "introduced_in_first_feature_commit", None)
        """,
    )
    commit_all(tmp_path, "introduce violating getattr in first feature commit")

    write_repo_file(
        tmp_path,
        "pkg/other.py",
        """
        def meaning() -> int:
            return 42
        """,
    )
    commit_all(tmp_path, "later feature commit does not touch violating file")

    result = run_script(tmp_path, "--base-rev", base_rev)

    assert result.returncode == 1
    assert_has_actionable_violation(result.stderr, "pkg/introduced_earlier.py")


def test_base_rev_mode_works_without_local_main_branch(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/existing.py",
        """
        def stable() -> str:
            return "ok"
        """,
    )
    commit_all(tmp_path, "baseline")
    base_rev = main_branch_rev(tmp_path)
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "pkg/other.py",
        """
        def meaning() -> int:
            return 42
        """,
    )
    commit_all(tmp_path, "feature change")

    git(tmp_path, "checkout", "--detach", "HEAD")
    git(tmp_path, "branch", "-D", "main")

    result = run_script(tmp_path, "--base-rev", base_rev)

    assert result.returncode == 0, result.stderr


def test_base_rev_mode_ignores_github_base_sha_environment(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/existing.py",
        """
        def stable() -> str:
            return "ok"
        """,
    )
    commit_all(tmp_path, "baseline")
    base_rev = main_branch_rev(tmp_path)
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "pkg/other.py",
        """
        def meaning() -> int:
            return 42
        """,
    )
    commit_all(tmp_path, "feature change")
    monkeypatch.setenv("GITHUB_BASE_SHA", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")

    result = run_script(tmp_path, "--base-rev", base_rev)

    assert result.returncode == 0, result.stderr


def test_staged_mode_reads_staged_content_only(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/module.py",
        """
        def read_value(obj):
            return obj.value
        """,
    )
    commit_all(tmp_path, "baseline")

    write_repo_file(
        tmp_path,
        "pkg/module.py",
        """
        def read_value(obj):
            return obj.value + 1
        """,
    )
    git(tmp_path, "add", "pkg/module.py")

    write_repo_file(
        tmp_path,
        "pkg/module.py",
        """
        def read_value(obj):
            return getattr(obj, "value", None)
        """,
    )

    result = run_script(tmp_path, "--staged")

    assert result.returncode == 0, result.stderr


def test_staged_mode_fails_for_staged_two_arg_getattr(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/module.py",
        """
        def read_value(obj):
            return obj.value
        """,
    )
    commit_all(tmp_path, "baseline")

    write_repo_file(
        tmp_path,
        "pkg/module.py",
        """
        def read_value(obj):
            return getattr(obj, "value")
        """,
    )
    git(tmp_path, "add", "pkg/module.py")

    result = run_script(tmp_path, "--staged")

    assert result.returncode == 1
    assert_has_actionable_violation(result.stderr, "pkg/module.py")


def test_staged_mode_fails_for_staged_builtins_getattr(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/module.py",
        """
        def read_value(obj):
            return obj.value
        """,
    )
    commit_all(tmp_path, "baseline")

    write_repo_file(
        tmp_path,
        "pkg/module.py",
        """
        import builtins


        def read_value(obj):
            return builtins.getattr(obj, "value", None)
        """,
    )
    git(tmp_path, "add", "pkg/module.py")

    result = run_script(tmp_path, "--staged")

    assert result.returncode == 1
    assert_has_actionable_violation(result.stderr, "pkg/module.py")


def test_staged_mode_fails_for_staged_two_arg_builtins_getattr(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/module.py",
        """
        def read_value(obj):
            return obj.value
        """,
    )
    commit_all(tmp_path, "baseline")

    write_repo_file(
        tmp_path,
        "pkg/module.py",
        """
        import builtins


        def read_value(obj):
            return builtins.getattr(obj, "value")
        """,
    )
    git(tmp_path, "add", "pkg/module.py")

    result = run_script(tmp_path, "--staged")

    assert result.returncode == 1
    assert_has_actionable_violation(result.stderr, "pkg/module.py")


def test_modified_hunk_with_same_getattr_count_is_allowed(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/sample.py",
        """
        def resolve_name(user):
            name = getattr(user, "display_name", None)
            if name:
                return name
            return "unknown"
        """,
    )
    commit_all(tmp_path, "baseline")
    base_rev = main_branch_rev(tmp_path)
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "pkg/sample.py",
        """
        def resolve_name(user):
            name = getattr(user, "display_name", None)
            if name:
                return name.strip()
            return "unknown user"
        """,
    )
    commit_all(tmp_path, "touch legacy getattr hunk")

    result = run_script(tmp_path, "--base-rev", base_rev)

    assert result.returncode == 0, result.stderr


def test_modified_hunk_with_decreased_getattr_count_is_allowed(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/sample.py",
        """
        def resolve_name(user):
            primary = getattr(user, "display_name", None)
            return primary or getattr(user, "username", None)
        """,
    )
    commit_all(tmp_path, "baseline")
    base_rev = main_branch_rev(tmp_path)
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "pkg/sample.py",
        """
        def resolve_name(user):
            return getattr(user, "display_name", None)
        """,
    )
    commit_all(tmp_path, "remove one legacy getattr")

    result = run_script(tmp_path, "--base-rev", base_rev)

    assert result.returncode == 0, result.stderr


def test_modified_hunk_with_increased_getattr_count_fails(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/sample.py",
        """
        def resolve_name(user):
            return getattr(user, "display_name", None)
        """,
    )
    commit_all(tmp_path, "baseline")
    base_rev = main_branch_rev(tmp_path)
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "pkg/sample.py",
        """
        def resolve_name(user):
            primary = getattr(user, "display_name", None)
            return primary or getattr(user, "username", None)
        """,
    )
    commit_all(tmp_path, "add one more getattr")

    result = run_script(tmp_path, "--base-rev", base_rev)

    assert result.returncode == 1
    assert_has_actionable_violation(result.stderr, "pkg/sample.py")
    assert "net-new getattr" in result.stderr


def test_inline_guard_ignore_with_explanatory_text_skips_added_getattr(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/existing.py",
        """
        def stable() -> str:
            return "ok"
        """,
    )
    commit_all(tmp_path, "baseline")
    base_rev = main_branch_rev(tmp_path)
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "pkg/existing.py",
        """
        def read_value(obj):
            return getattr(obj, "dynamic_name", None)  # guard-ignore: no-new-getattr -- plugin-defined attributes
        """,
    )
    commit_all(tmp_path, "add suppressed getattr")

    result = run_script(tmp_path, "--base-rev", base_rev)

    assert "guard-ignore: no-new-getattr -- plugin-defined attributes" in (tmp_path / "pkg/existing.py").read_text(
        encoding="utf-8"
    )
    assert result.returncode == 0, stderr_lines(result)


def test_inline_guard_ignore_without_explanatory_text_is_not_sufficient(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "pkg/existing.py",
        """
        def stable() -> str:
            return "ok"
        """,
    )
    commit_all(tmp_path, "baseline")
    base_rev = main_branch_rev(tmp_path)
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "pkg/existing.py",
        """
        def read_value(obj):
            return getattr(obj, "dynamic_name", None)  # guard-ignore: no-new-getattr
        """,
    )
    commit_all(tmp_path, "add bare guard ignore getattr")

    result = run_script(tmp_path, "--base-rev", base_rev)

    assert result.returncode == 1
    assert_has_actionable_violation(result.stderr, "pkg/existing.py")


def test_non_python_file_with_getattr_text_does_not_fail_guard(tmp_path: Path) -> None:
    init_repo(tmp_path)
    write_repo_file(
        tmp_path,
        "docs/example.txt",
        """
        Existing documentation.
        """,
    )
    commit_all(tmp_path, "baseline")
    base_rev = main_branch_rev(tmp_path)
    checkout_feature_branch(tmp_path)

    write_repo_file(
        tmp_path,
        "docs/example.txt",
        """
        getattr(obj, "dynamic_name", None)
        """,
    )
    commit_all(tmp_path, "document getattr example")

    result = run_script(tmp_path, "--base-rev", base_rev)

    assert result.returncode == 0, result.stderr
