"""Tests for git worktree lifecycle (mocked subprocess)."""

from __future__ import annotations

import asyncio
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from cleave.orchestrator.errors import GitWorktreeError
from cleave.orchestrator.worktree import (
    create_worktree,
    ensure_clean_worktree,
    get_current_branch,
    remove_worktree,
    restore_dirty_state,
    snapshot_dirty_state,
)


def _mock_subprocess(returncode: int = 0, stdout: str = "", stderr: str = ""):
    """Create a mock asyncio.subprocess.Process."""
    proc = AsyncMock()
    proc.communicate = AsyncMock(
        return_value=(stdout.encode(), stderr.encode())
    )
    proc.returncode = returncode
    return proc


class TestGetCurrentBranch:
    def test_returns_branch_name(self) -> None:
        with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec") as mock_exec:
            mock_exec.return_value = _mock_subprocess(stdout="main")
            result = asyncio.run(get_current_branch(Path("/repo")))
            assert result == "main"

    def test_raises_on_error(self) -> None:
        with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec") as mock_exec:
            mock_exec.return_value = _mock_subprocess(returncode=1, stderr="not a repo")
            with pytest.raises(GitWorktreeError, match="not a repo"):
                asyncio.run(get_current_branch(Path("/repo")))


class TestEnsureCleanWorktree:
    def test_clean_passes(self) -> None:
        with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec") as mock_exec:
            mock_exec.return_value = _mock_subprocess(stdout="")
            asyncio.run(ensure_clean_worktree(Path("/repo")))

    def test_dirty_raises(self) -> None:
        with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec") as mock_exec:
            mock_exec.return_value = _mock_subprocess(stdout=" M src/main.py")
            with pytest.raises(GitWorktreeError, match="uncommitted"):
                asyncio.run(ensure_clean_worktree(Path("/repo")))


class TestCreateWorktree:
    def test_creates_worktree_and_returns_paths(self, tmp_path: Path) -> None:
        call_count = 0

        async def mock_exec(*args, **kwargs):
            nonlocal call_count
            call_count += 1
            return _mock_subprocess()

        with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec", side_effect=mock_exec):
            wt_path, branch = asyncio.run(
                create_worktree(tmp_path, "auth-core", "main", child_id=0)
            )
            assert branch == "cleave/auth-core-0"
            assert "auth-core-0" in str(wt_path)
            assert call_count >= 2  # branch -d + worktree add

    def test_sanitizes_label(self, tmp_path: Path) -> None:
        with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec") as mock_exec:
            mock_exec.return_value = _mock_subprocess()
            wt_path, branch = asyncio.run(
                create_worktree(tmp_path, "Fix Auth/Login Bug #123", "main", child_id=1)
            )
            assert "/" not in branch.split("/", 1)[1]  # after cleave/ prefix
            assert "#" not in branch

    def test_empty_label_gets_fallback(self, tmp_path: Path) -> None:
        with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec") as mock_exec:
            mock_exec.return_value = _mock_subprocess()
            wt_path, branch = asyncio.run(
                create_worktree(tmp_path, "@#$%", "main", child_id=5)
            )
            assert branch == "cleave/child-5"

    def test_collision_prevention(self, tmp_path: Path) -> None:
        """Labels that sanitize identically get unique branches via child_id."""
        with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec") as mock_exec:
            mock_exec.return_value = _mock_subprocess()
            _, branch_a = asyncio.run(
                create_worktree(tmp_path, "foo!", "main", child_id=0)
            )
            _, branch_b = asyncio.run(
                create_worktree(tmp_path, "foo@", "main", child_id=1)
            )
            assert branch_a != branch_b


class TestRemoveWorktree:
    def test_removes_existing(self, tmp_path: Path) -> None:
        wt_path = tmp_path / ".cleave-worktrees" / "test"
        wt_path.mkdir(parents=True)
        with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec") as mock_exec:
            mock_exec.return_value = _mock_subprocess()
            asyncio.run(remove_worktree(tmp_path, wt_path))

    def test_handles_nonexistent(self, tmp_path: Path) -> None:
        wt_path = tmp_path / "nonexistent"
        with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec") as mock_exec:
            mock_exec.return_value = _mock_subprocess()
            asyncio.run(remove_worktree(tmp_path, wt_path))  # Should not raise


class TestSnapshotDirtyState:
    def test_returns_pre_head_sha(self) -> None:
        git_calls = []

        async def mock_exec(*args, **kwargs):
            git_calls.append(args)
            # rev-parse HEAD → returns sha
            if "rev-parse" in args:
                return _mock_subprocess(stdout="abc123def456")
            # git add -A → success
            if "add" in args:
                return _mock_subprocess()
            # git commit → success
            if "commit" in args:
                return _mock_subprocess()
            return _mock_subprocess()

        with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec", side_effect=mock_exec):
            sha = asyncio.run(snapshot_dirty_state(Path("/repo")))

        assert sha == "abc123def456"
        # Should have called: rev-parse, add -A, commit
        sub_cmds = [a[1] if len(a) > 1 else "" for a in git_calls]
        assert "rev-parse" in sub_cmds
        assert "add" in sub_cmds
        assert "commit" in sub_cmds

    def test_handles_no_changes_gracefully(self) -> None:
        """If commit finds nothing to commit (rc!=0), snapshot still returns sha."""
        async def mock_exec(*args, **kwargs):
            if "rev-parse" in args:
                return _mock_subprocess(stdout="abc123")
            if "commit" in args:
                return _mock_subprocess(returncode=1, stderr="nothing to commit")
            return _mock_subprocess()

        with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec", side_effect=mock_exec):
            sha = asyncio.run(snapshot_dirty_state(Path("/repo")))

        assert sha == "abc123"


class TestRestoreDirtyState:
    def test_runs_two_step_reset(self) -> None:
        git_calls = []

        async def mock_exec(*args, **kwargs):
            git_calls.append(args)
            return _mock_subprocess()

        with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec", side_effect=mock_exec):
            asyncio.run(restore_dirty_state(Path("/repo"), "abc123"))

        # Should run: git reset --soft abc123, git reset
        assert any("--soft" in a and "abc123" in a for a in git_calls)
        # Second reset (bare) should also be present
        reset_calls = [a for a in git_calls if a[1] == "reset"]
        assert len(reset_calls) == 2


class TestCreateWorktreeVenvSymlink:
    def test_symlinks_venv_when_present(self, tmp_path: Path) -> None:
        # Create a .venv directory in the "repo"
        venv_dir = tmp_path / ".venv"
        venv_dir.mkdir()
        (venv_dir / "bin").mkdir()
        (venv_dir / "bin" / "python").touch()

        # Mock git worktree add to also create the worktree directory
        # (simulating real git behavior)
        async def mock_exec(*args, **kwargs):
            # When 'worktree add' is called, create the directory
            if "worktree" in args and "add" in args:
                for i, a in enumerate(args):
                    if a == "add":
                        # The path is 2 args after 'add' (after -b and branch name)
                        # Actually find the path by looking for .cleave-worktrees
                        for candidate in args:
                            if ".cleave-worktrees" in str(candidate):
                                Path(candidate).mkdir(parents=True, exist_ok=True)
                                break
            return _mock_subprocess()

        with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec", side_effect=mock_exec):
            wt_path, _ = asyncio.run(
                create_worktree(tmp_path, "test-task", "main", child_id=0)
            )

        # Verify symlink was created
        wt_venv = wt_path / ".venv"
        assert wt_venv.is_symlink()
        assert wt_venv.resolve() == venv_dir.resolve()

    def test_no_symlink_without_venv(self, tmp_path: Path) -> None:
        """create_worktree should not crash when no .venv exists."""
        with patch("cleave.orchestrator.worktree.asyncio.create_subprocess_exec") as mock_exec:
            mock_exec.return_value = _mock_subprocess()
            wt_path, _ = asyncio.run(
                create_worktree(tmp_path, "test-task", "main", child_id=0)
            )
        # No crash, no .venv dir in repo = nothing to symlink
        assert not (wt_path / ".venv").exists()
