"""Tests for agentmux.utils.git subprocess helpers.

All git operations are mocked via ``monkeypatch`` — no real git processes are
spawned, so tests are fast, hermetic, and require no network access.
"""

import subprocess
from pathlib import Path
from unittest.mock import MagicMock

import pytest

from agentmux.utils.git import (
    clone_repo,
    derive_name_from_source,
    diff_summary,
    fetch_repo,
    get_head_sha,
    get_remote_tracking_sha,
    get_remote_url,
    pull_repo,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_FAKE_SHA = "abc123def456abc123def456abc123def456abc123"
_FAKE_URL = "https://github.com/user/my-setup"


def _make_completed_process(stdout: str = "", returncode: int = 0, stderr: str = "") -> MagicMock:
    """Build a mock ``CompletedProcess`` object."""
    mock = MagicMock(spec=subprocess.CompletedProcess)
    mock.returncode = returncode
    mock.stdout = stdout
    mock.stderr = stderr
    return mock


# ---------------------------------------------------------------------------
# clone_repo
# ---------------------------------------------------------------------------


def test_clone_repo_calls_git_clone(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    """clone_repo issues a shallow clone with --depth 1."""
    captured: list[list[str]] = []

    def fake_run(
        args: list[str], *, cwd: Path | None, capture_output: bool, text: bool
    ) -> MagicMock:
        captured.append(args)
        return _make_completed_process(stdout="")

    monkeypatch.setattr(subprocess, "run", fake_run)

    target = tmp_path / "my-setup"
    result = clone_repo(_FAKE_URL, target)

    assert captured == [["git", "clone", "--depth", "1", _FAKE_URL, str(target)]]
    assert result == target.resolve()


def test_clone_repo_raises_on_failure(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    """clone_repo raises RuntimeError when git exits non-zero."""
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *a, **kw: _make_completed_process(returncode=128, stderr="repository not found"),
    )

    with pytest.raises(RuntimeError, match="repository not found"):
        clone_repo(_FAKE_URL, tmp_path / "setup")


# ---------------------------------------------------------------------------
# pull_repo
# ---------------------------------------------------------------------------


def test_pull_repo_calls_pull_then_rev_parse(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """pull_repo checks shallow status, skips unshallow on non-shallow repos, then pulls."""
    calls: list[list[str]] = []

    def fake_run(
        args: list[str], *, cwd: Path | None, capture_output: bool, text: bool
    ) -> MagicMock:
        calls.append(args)
        if "--is-shallow-repository" in args:
            return _make_completed_process(stdout="false")
        if "pull" in args:
            return _make_completed_process(stdout="Already up to date.")
        # rev-parse HEAD
        return _make_completed_process(stdout=_FAKE_SHA)

    monkeypatch.setattr(subprocess, "run", fake_run)

    sha = pull_repo(tmp_path)

    assert calls[0] == ["git", "rev-parse", "--is-shallow-repository"]
    assert calls[1] == ["git", "pull", "--ff-only"]
    assert calls[2] == ["git", "rev-parse", "HEAD"]
    assert sha == _FAKE_SHA


def test_pull_repo_unshallows_before_pulling(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """pull_repo runs git fetch --unshallow before git pull when repo is shallow."""
    calls: list[list[str]] = []

    def fake_run(
        args: list[str], *, cwd: Path | None, capture_output: bool, text: bool
    ) -> MagicMock:
        calls.append(args)
        if "--is-shallow-repository" in args:
            return _make_completed_process(stdout="true")
        if "pull" in args:
            return _make_completed_process(stdout="Already up to date.")
        # rev-parse HEAD
        return _make_completed_process(stdout=_FAKE_SHA)

    monkeypatch.setattr(subprocess, "run", fake_run)

    sha = pull_repo(tmp_path)

    assert calls[0] == ["git", "rev-parse", "--is-shallow-repository"]
    assert calls[1] == ["git", "fetch", "--unshallow"]
    assert calls[2] == ["git", "pull", "--ff-only"]
    assert calls[3] == ["git", "rev-parse", "HEAD"]
    assert sha == _FAKE_SHA


def test_pull_repo_raises_on_failure(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    """pull_repo raises RuntimeError if git pull fails."""
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *a, **kw: _make_completed_process(returncode=1, stderr="merge conflict"),
    )

    with pytest.raises(RuntimeError, match="merge conflict"):
        pull_repo(tmp_path)


# ---------------------------------------------------------------------------
# get_head_sha
# ---------------------------------------------------------------------------


def test_get_head_sha_returns_sha(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    """get_head_sha returns the stdout of git rev-parse HEAD."""
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *a, **kw: _make_completed_process(stdout=_FAKE_SHA),
    )

    sha = get_head_sha(tmp_path)

    assert sha == _FAKE_SHA


def test_get_head_sha_raises_on_failure(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    """get_head_sha raises RuntimeError when not inside a git repo."""
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *a, **kw: _make_completed_process(returncode=128, stderr="not a git repository"),
    )

    with pytest.raises(RuntimeError, match="not a git repository"):
        get_head_sha(tmp_path)


# ---------------------------------------------------------------------------
# get_remote_url
# ---------------------------------------------------------------------------


def test_get_remote_url_returns_url(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    """get_remote_url returns the origin fetch URL."""
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *a, **kw: _make_completed_process(stdout=_FAKE_URL),
    )

    url = get_remote_url(tmp_path)

    assert url == _FAKE_URL


def test_get_remote_url_raises_when_no_origin(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """get_remote_url raises RuntimeError when origin remote does not exist."""
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *a, **kw: _make_completed_process(returncode=2, stderr="No such remote 'origin'"),
    )

    with pytest.raises(RuntimeError, match="No such remote"):
        get_remote_url(tmp_path)


# ---------------------------------------------------------------------------
# diff_summary
# ---------------------------------------------------------------------------


def test_diff_summary_returns_diffstat(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    """diff_summary returns the --stat output between two SHAs.

    Note: _run_git strips leading/trailing whitespace from stdout, so the
    expected value must not have surrounding whitespace.
    """
    raw_stat = " agents/helper.md | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)"
    expected_stat = raw_stat.strip()
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *a, **kw: _make_completed_process(stdout=raw_stat),
    )

    result = diff_summary(tmp_path, "aaa111", "bbb222")

    assert result == expected_stat


def test_diff_summary_empty_when_no_changes(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """diff_summary returns an empty string when there are no differences."""
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *a, **kw: _make_completed_process(stdout=""),
    )

    result = diff_summary(tmp_path, _FAKE_SHA, _FAKE_SHA)

    assert result == ""


def test_diff_summary_passes_correct_range(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    """diff_summary passes the SHA range as '<from>..<to>' to git diff."""
    captured: list[list[str]] = []

    def fake_run(
        args: list[str], *, cwd: Path | None, capture_output: bool, text: bool
    ) -> MagicMock:
        captured.append(args)
        return _make_completed_process(stdout="")

    monkeypatch.setattr(subprocess, "run", fake_run)

    diff_summary(tmp_path, "from_sha", "to_sha")

    assert captured == [["git", "diff", "--stat", "from_sha..to_sha"]]


def test_diff_summary_raises_on_unknown_sha(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """diff_summary raises RuntimeError when a SHA is unknown."""
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *a, **kw: _make_completed_process(returncode=128, stderr="unknown revision"),
    )

    with pytest.raises(RuntimeError, match="unknown revision"):
        diff_summary(tmp_path, "bad_sha", "also_bad")


# ---------------------------------------------------------------------------
# _run_git error message format
# ---------------------------------------------------------------------------


def test_run_git_error_includes_subcommand_and_exit_code(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """RuntimeError messages include the git subcommand name and exit code."""
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *a, **kw: _make_completed_process(returncode=1, stderr="some error details"),
    )

    with pytest.raises(RuntimeError, match=r"'clone'.*exit 1.*some error details"):
        clone_repo(_FAKE_URL, tmp_path / "x")


def test_run_git_raises_runtime_error_when_git_not_installed(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """RuntimeError with a helpful message is raised when git binary is not found."""

    def raise_file_not_found(*args: object, **kwargs: object) -> None:
        raise FileNotFoundError(2, "No such file or directory")

    monkeypatch.setattr(subprocess, "run", raise_file_not_found)

    with pytest.raises(RuntimeError, match="git is not installed or not found on PATH"):
        clone_repo(_FAKE_URL, tmp_path / "x")


# ---------------------------------------------------------------------------
# fetch_repo
# ---------------------------------------------------------------------------


def test_fetch_repo_calls_git_fetch(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    """fetch_repo issues 'git fetch' with the repository directory as cwd."""
    captured: list[list[str]] = []

    def fake_run(
        args: list[str], *, cwd: Path | None, capture_output: bool, text: bool
    ) -> MagicMock:
        captured.append(args)
        return _make_completed_process(stdout="")

    monkeypatch.setattr(subprocess, "run", fake_run)

    fetch_repo(tmp_path)

    assert captured == [["git", "fetch"]]


def test_fetch_repo_raises_on_failure(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    """fetch_repo raises RuntimeError when git fetch fails."""
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *a, **kw: _make_completed_process(returncode=1, stderr="no remote"),
    )

    with pytest.raises(RuntimeError, match="no remote"):
        fetch_repo(tmp_path)


# ---------------------------------------------------------------------------
# get_remote_tracking_sha
# ---------------------------------------------------------------------------


def test_get_remote_tracking_sha_returns_sha(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """get_remote_tracking_sha returns the upstream SHA when tracking is configured."""
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *a, **kw: _make_completed_process(stdout=_FAKE_SHA + "\n"),
    )

    result = get_remote_tracking_sha(tmp_path)

    assert result == _FAKE_SHA


def test_get_remote_tracking_sha_returns_none_on_failure(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """get_remote_tracking_sha returns None when no upstream is configured."""
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *a, **kw: _make_completed_process(returncode=128, stderr="no upstream"),
    )

    result = get_remote_tracking_sha(tmp_path)

    assert result is None


def test_get_remote_tracking_sha_returns_none_for_empty_output(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """get_remote_tracking_sha returns None when stdout is empty."""
    monkeypatch.setattr(
        subprocess,
        "run",
        lambda *a, **kw: _make_completed_process(stdout=""),
    )

    result = get_remote_tracking_sha(tmp_path)

    assert result is None


# ---------------------------------------------------------------------------
# derive_name_from_source
# ---------------------------------------------------------------------------


def test_derive_name_strips_git_suffix() -> None:
    """derive_name_from_source removes the .git extension."""
    result = derive_name_from_source("https://github.com/user/my-setup.git")
    assert result == "my-setup"


def test_derive_name_from_local_path() -> None:
    """derive_name_from_source uses the last path component for local paths."""
    result = derive_name_from_source("/home/user/my-project")
    assert result == "my-project"


def test_derive_name_sanitizes_special_characters() -> None:
    """derive_name_from_source replaces invalid characters with hyphens."""
    result = derive_name_from_source("https://github.com/user/my setup!")
    assert " " not in result
    assert "!" not in result
