"""A git mutation op killed by ``_run_git``'s timeout best-effort removes
orphaned ``.git/**/*.lock`` files before re-raising — the SIGKILL'd git
process can't clean up itself, and the locks wedge every subsequent op.
"""

from __future__ import annotations

import subprocess
from typing import TYPE_CHECKING
from unittest.mock import MagicMock

import pytest
from roboco.exceptions import GitTimeoutError
from roboco.services.git import GitService

if TYPE_CHECKING:
    from pathlib import Path


def _svc() -> GitService:
    return GitService(MagicMock())


def _seed_locks(workspace: Path) -> dict[str, Path]:
    """Create a realistic set of orphaned git lock files under .git/."""
    git_dir = workspace / ".git"
    refs_heads = git_dir / "refs" / "heads"
    refs_heads.mkdir(parents=True, exist_ok=True)
    locks = {
        "index": git_dir / "index.lock",
        "head": git_dir / "HEAD.lock",
        "packed_refs": git_dir / "packed-refs.lock",
        "ref": refs_heads / "feature.lock",
    }
    for p in locks.values():
        p.write_text("stale")
    # A non-lock file that must be left untouched.
    keep = git_dir / "config"
    keep.write_text("[core]\n")
    return {**locks, "keep_config": keep}


@pytest.mark.asyncio
async def test_timeout_removes_stale_git_locks(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """A timed-out git op clears orphaned .git locks before re-raising."""
    workspace = tmp_path / "ws"
    (workspace / ".git").mkdir(parents=True)
    locks = _seed_locks(workspace)

    def _boom(*_a: object, **_k: object) -> subprocess.CompletedProcess[str]:
        raise subprocess.TimeoutExpired(cmd=["git", "commit"], timeout=5)

    monkeypatch.setattr("roboco.services.git.subprocess.run", _boom)

    with pytest.raises(GitTimeoutError):
        await _svc()._run_git(workspace, ["commit", "-m", "x"], check=True)

    # Every orphaned lock file is gone — the workspace is un-wedged.
    for name in ("index", "head", "packed_refs", "ref"):
        assert not locks[name].exists(), f"{name} lock was not cleaned up"
    # Non-lock .git files are untouched.
    assert locks["keep_config"].exists()


@pytest.mark.asyncio
async def test_timeout_lock_cleanup_is_best_effort_no_git_dir(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """A timeout with no .git/ directory must not error the cleanup path."""
    workspace = tmp_path / "ws"
    workspace.mkdir()

    def _boom(*_a: object, **_k: object) -> subprocess.CompletedProcess[str]:
        raise subprocess.TimeoutExpired(cmd=["git", "reset", "--hard"], timeout=5)

    monkeypatch.setattr("roboco.services.git.subprocess.run", _boom)

    with pytest.raises(GitTimeoutError):
        await _svc()._run_git(workspace, ["reset", "--hard", "origin/main"])
    # No crash — that's the assertion (best-effort cleanup on a missing .git).


@pytest.mark.asyncio
async def test_clean_exit_does_not_touch_locks(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """A normal (non-timed-out) git op must NOT delete lock files — a concurrent
    real git process could be holding one. Cleanup is timeout-only."""
    workspace = tmp_path / "ws"
    (workspace / ".git").mkdir(parents=True)
    locks = _seed_locks(workspace)

    ok = subprocess.CompletedProcess(
        args=["git", "status"], returncode=0, stdout="", stderr=""
    )
    monkeypatch.setattr("roboco.services.git.subprocess.run", lambda *_a, **_k: ok)
    # _ensure_agent_owned runs after a clean exit — no-op it.
    monkeypatch.setattr(
        "roboco.services.workspace._ensure_agent_owned", lambda _ws: None
    )

    await _svc()._run_git(workspace, ["status"], check=True)

    for name in ("index", "head", "packed_refs", "ref"):
        assert locks[name].exists(), f"{name} lock was wrongly removed on clean exit"
