"""Security regression test for GitHub issue #207.

`gate_3_local_tests` in scripts/agent_review_and_merge.py interpolated the
PR's `headRefName` (attacker-controllable — it's the head branch name of a
PR anyone can open) into shell command strings run via `_run(cmd)`, which
executes with `shell=True` whenever `cmd` is a string. Git permits `;`, `|`,
`&`, `$()`, and backticks in branch names, so a PR opened from a branch like
`a;touch /tmp/pwned` executed arbitrary shell on the reviewer's machine when
they ran the mandatory merge gate (`scripts/agent_review_and_merge.py`).

The fix: (1) the branch-interpolating git calls now use list-argv form, so
no shell parses the branch name, and (2) `branch` and `current` are
validated up front against a strict charset (first char alphanumeric, rest
`[A-Za-z0-9._/-]`) so a leading `-` can't be parsed by git as a flag either.

These tests run with NO real git or network access — `_run` is monkeypatched
with a spy/stub throughout.
"""
import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))

import agent_review_and_merge as arm


PWNED_PATH = "/tmp/pwned"


def _pr(head):
    """Minimal PR dict shaped like gh_pr_view()'s output."""
    return {
        "number": 42,
        "title": "Test PR",
        "state": "OPEN",
        "mergeable": "MERGEABLE",
        "mergeStateStatus": "CLEAN",
        "headRefName": head,
        "baseRefName": "main",
        "author": {"login": "attacker"},
        "statusCheckRollup": [],
    }


class _RunSpy:
    """Records every command passed to _run and returns a canned success."""

    def __init__(self, current_branch="main", pytest_stdout="1 passed\n"):
        self.calls = []
        self._current_branch = current_branch
        self._pytest_stdout = pytest_stdout

    def __call__(self, cmd, check=True, capture=True):
        self.calls.append(cmd)

        class _Result:
            pass

        r = _Result()
        r.returncode = 0
        r.stderr = ""
        if cmd == "git rev-parse --abbrev-ref HEAD":
            r.stdout = self._current_branch + "\n"
        elif isinstance(cmd, str) and "pytest" in cmd:
            r.stdout = self._pytest_stdout
        else:
            r.stdout = ""
        return r


def setup_function(_func):
    # Belt-and-suspenders: make sure no earlier failure to neutralize the
    # injection left evidence on disk before we assert its absence below.
    if os.path.exists(PWNED_PATH):
        os.remove(PWNED_PATH)


def teardown_function(_func):
    if os.path.exists(PWNED_PATH):
        os.remove(PWNED_PATH)


class TestBranchInjectionNeutralized:
    """A hostile headRefName must not reach a shell."""

    def test_hostile_branch_cannot_inject_shell_commands(self, monkeypatch):
        hostile = "a;touch /tmp/pwned"
        spy = _RunSpy()
        monkeypatch.setattr(arm, "_run", spy)

        arm.gate_3_local_tests(_pr(hostile))

        for cmd in spy.calls:
            if isinstance(cmd, str):
                assert hostile not in cmd, (
                    f"hostile branch name leaked into a shell string: {cmd!r}"
                )
            else:
                # list-argv form: the hostile string may appear as ONE
                # argv element (inert — never parsed by a shell), which is
                # fine. What matters is it is never part of a str command.
                assert isinstance(cmd, list)

        assert not os.path.exists(PWNED_PATH), (
            "injection succeeded: /tmp/pwned was created"
        )

    def test_hostile_branch_with_backticks_cannot_inject(self, monkeypatch):
        hostile = "a`touch /tmp/pwned`"
        spy = _RunSpy()
        monkeypatch.setattr(arm, "_run", spy)

        arm.gate_3_local_tests(_pr(hostile))

        for cmd in spy.calls:
            if isinstance(cmd, str):
                assert hostile not in cmd
        assert not os.path.exists(PWNED_PATH)

    def test_hostile_branch_with_dollar_paren_cannot_inject(self, monkeypatch):
        hostile = "a$(touch /tmp/pwned)"
        spy = _RunSpy()
        monkeypatch.setattr(arm, "_run", spy)

        arm.gate_3_local_tests(_pr(hostile))

        for cmd in spy.calls:
            if isinstance(cmd, str):
                assert hostile not in cmd
        assert not os.path.exists(PWNED_PATH)


class TestLeadingDashRejected:
    """A branch name that looks like a git flag must be rejected outright."""

    def test_leading_dash_branch_fails_gate(self, monkeypatch):
        spy = _RunSpy()
        monkeypatch.setattr(arm, "_run", spy)

        ok, msg = arm.gate_3_local_tests(_pr("--upload-pack=evil"))

        assert ok is False
        assert "unsafe" in msg.lower() or "branch" in msg.lower()

    def test_leading_dash_branch_never_reaches_git_checkout(self, monkeypatch):
        spy = _RunSpy()
        monkeypatch.setattr(arm, "_run", spy)

        arm.gate_3_local_tests(_pr("--upload-pack=evil"))

        # Rejected before any git fetch/checkout/pull of the hostile ref.
        for cmd in spy.calls:
            cmd_str = cmd if isinstance(cmd, str) else " ".join(cmd)
            assert "--upload-pack=evil" not in cmd_str or "rev-parse" in cmd_str


class TestNormalBranchStillWorks:
    """A well-formed branch name must still pass the branch-name check and run."""

    def test_normal_branch_passes_and_runs_pytest(self, monkeypatch):
        spy = _RunSpy(current_branch="main", pytest_stdout="1843 passed\n")
        monkeypatch.setattr(arm, "_run", spy)

        ok, msg = arm.gate_3_local_tests(_pr("fix/foo-bar"))

        assert ok is True
        assert "passed" in msg.lower()

        # Confirm git was actually invoked (in list form) with the branch.
        git_calls = [c for c in spy.calls if isinstance(c, list) and c[:1] == ["git"]]
        assert any(c[:2] == ["git", "checkout"] and "fix/foo-bar" in c for c in git_calls)

    def test_normal_branch_name_passes_validation_helper_directly(self):
        assert arm._is_safe_branch_name("fix/foo-bar") is True
        assert arm._is_safe_branch_name("main") is True
        assert arm._is_safe_branch_name("release/v7.49.1") is True

    def test_hostile_names_fail_validation_helper_directly(self):
        assert arm._is_safe_branch_name("a;touch /tmp/pwned") is False
        assert arm._is_safe_branch_name("--upload-pack=evil") is False
        assert arm._is_safe_branch_name("") is False
        assert arm._is_safe_branch_name(None) is False
