"""Per-branch sub-worktree lifecycle tests (SPEC §6.10.1).

These tests shell out to real `git worktree` commands (same as
`test_manager.py`) because the production code path does too. Each
test exercises `WorktreeManager.create_branch`, `list_branches`, or
`remove_branch` against a real git repo via the `seeded_repo` fixture
from `conftest.py`.
"""

import subprocess
from pathlib import Path

import pytest

from attractor.worktree import (
    WorktreeCommandError,
    WorktreeManager,
    WorktreeValidationError,
)

# Test access to module-private helpers is intentional — these helpers
# carry §6.10.1 invariants worth pinning directly.
from attractor.worktree.manager import (
    _full_branch_ref,  # pyright: ignore[reportPrivateUsage]
    _validate_branch_name,  # pyright: ignore[reportPrivateUsage]
)

# Two known-good v4 UUID strings keep test names readable and
# reproducible — no randomness in test data.
RUN_ID_A = "11111111-1111-4111-8111-111111111111"
RUN_ID_B = "22222222-2222-4222-8222-222222222222"


def _head_oid(repo_root: Path) -> str:
    """Return the current HEAD commit OID as a hex string."""
    return (
        subprocess.check_output(
            ["git", "-C", str(repo_root), "rev-parse", "HEAD"],
            text=True,
        )
        .strip()
    )


def _has_ref(repo_root: Path, ref: str) -> bool:
    """True if `ref` exists in the repo at `repo_root`."""
    result = subprocess.run(
        ["git", "show-ref", "--verify", "--quiet", ref],
        cwd=repo_root,
        capture_output=True,
    )
    return result.returncode == 0


def _resolve_ref(repo_root: Path, ref: str) -> str:
    """Dereference `ref` to its target OID string."""
    return (
        subprocess.check_output(
            ["git", "-C", str(repo_root), "rev-parse", ref],
            text=True,
        )
        .strip()
    )


# [unit->REQ-EXEC-PARALLEL-WORKTREE]
class TestBranchNameValidation:
    """`_validate_branch_name` enforces the `[a-z0-9][a-z0-9_-]*` rule."""

    def test_simple_name_passes(self) -> None:
        _validate_branch_name("main")

    def test_digits_only_passes(self) -> None:
        _validate_branch_name("123")

    def test_hyphens_and_digits_passes(self) -> None:
        _validate_branch_name("branch-1")

    def test_underscores_pass(self) -> None:
        _validate_branch_name("my_branch")

    def test_empty_raises(self) -> None:
        with pytest.raises(WorktreeValidationError, match="must match"):
            _validate_branch_name("")

    def test_slash_raises(self) -> None:
        with pytest.raises(WorktreeValidationError, match="must match"):
            _validate_branch_name("feat/foo")

    def test_uppercase_raises(self) -> None:
        with pytest.raises(WorktreeValidationError, match="must match"):
            _validate_branch_name("Uppercase")

    def test_leading_hyphen_raises(self) -> None:
        with pytest.raises(WorktreeValidationError, match="must match"):
            _validate_branch_name("-bad")

    def test_leading_underscore_raises(self) -> None:
        with pytest.raises(WorktreeValidationError, match="must match"):
            _validate_branch_name("_bad")


# [unit->REQ-EXEC-PARALLEL-WORKTREE]
class TestCreateBranch:
    """End-to-end: `create_branch` materialises directory + branch ref."""

    def test_creates_directory(self, seeded_repo: Path) -> None:
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        bw = mgr.create_branch(RUN_ID_A, "main", oid)
        assert bw.path.exists()
        assert bw.path.is_dir()

    def test_creates_branch_ref(self, seeded_repo: Path) -> None:
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        bw = mgr.create_branch(RUN_ID_A, "main", oid)
        assert _has_ref(seeded_repo, bw.branch_ref)

    def test_ref_resolves_to_fork_oid(self, seeded_repo: Path) -> None:
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        bw = mgr.create_branch(RUN_ID_A, "side", oid)
        assert _resolve_ref(seeded_repo, bw.branch_ref) == oid

    def test_path_layout(self, seeded_repo: Path) -> None:
        """Path is `.attractor/worktrees/<run-id>/branch/<name>/`."""
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        bw = mgr.create_branch(RUN_ID_A, "side", oid)
        assert bw.path.name == "side"
        assert bw.path.parent.name == "branch"
        assert bw.path.parent.parent.name == RUN_ID_A

    def test_short_branch_used_not_full_ref(self, seeded_repo: Path) -> None:
        """Guard against the rust-v0.1 regression: git must receive the
        SHORT branch name so it doesn't create `refs/heads/refs/heads/...`.
        """
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        bw = mgr.create_branch(RUN_ID_A, "side", oid)
        assert bw.branch_ref == _full_branch_ref(RUN_ID_A, "side")
        # The full ref should start with exactly one `refs/heads/`.
        assert bw.branch_ref.startswith("refs/heads/attractor/")
        assert not bw.branch_ref.startswith("refs/heads/refs/")

    def test_bad_run_id_raises_before_git(self, seeded_repo: Path) -> None:
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        with pytest.raises(WorktreeValidationError):
            mgr.create_branch("not-a-uuid", "main", oid)
        # No stray directory created.
        assert not (seeded_repo / ".attractor").exists()

    def test_duplicate_raises(self, seeded_repo: Path) -> None:
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        mgr.create_branch(RUN_ID_A, "side", oid)
        with pytest.raises(WorktreeValidationError, match="already exists"):
            mgr.create_branch(RUN_ID_A, "side", oid)

    def test_bad_fork_point_oid_raises_command_error(
        self, seeded_repo: Path
    ) -> None:
        mgr = WorktreeManager(seeded_repo)
        with pytest.raises(WorktreeCommandError):
            mgr.create_branch(RUN_ID_A, "side", "no-such-oid")


# [unit->REQ-EXEC-PARALLEL-WORKTREE]
class TestTwoBranches:
    """Two branches for the same run coexist without interference."""

    def test_both_directories_exist(self, seeded_repo: Path) -> None:
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        bw1 = mgr.create_branch(RUN_ID_A, "alpha", oid)
        bw2 = mgr.create_branch(RUN_ID_A, "beta", oid)
        assert bw1.path.exists()
        assert bw2.path.exists()
        assert bw1.path != bw2.path

    def test_both_refs_exist(self, seeded_repo: Path) -> None:
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        bw1 = mgr.create_branch(RUN_ID_A, "alpha", oid)
        bw2 = mgr.create_branch(RUN_ID_A, "beta", oid)
        assert _has_ref(seeded_repo, bw1.branch_ref)
        assert _has_ref(seeded_repo, bw2.branch_ref)

    def test_refs_are_distinct(self, seeded_repo: Path) -> None:
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        bw1 = mgr.create_branch(RUN_ID_A, "alpha", oid)
        bw2 = mgr.create_branch(RUN_ID_A, "beta", oid)
        assert bw1.branch_ref != bw2.branch_ref


# [unit->REQ-EXEC-PARALLEL-WORKTREE]
class TestListBranches:
    """`list_branches` reflects filesystem state deterministically."""

    def test_returns_created_worktree(self, seeded_repo: Path) -> None:
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        bw = mgr.create_branch(RUN_ID_A, "side", oid)
        listed = mgr.list_branches(RUN_ID_A)
        assert len(listed) == 1
        assert listed[0].branch_name == "side"
        assert listed[0].path == bw.path
        assert listed[0].branch_ref == bw.branch_ref

    def test_order_is_sorted(self, seeded_repo: Path) -> None:
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        # Create in reverse-alpha order to confirm sorting.
        mgr.create_branch(RUN_ID_A, "zeta", oid)
        mgr.create_branch(RUN_ID_A, "alpha", oid)
        mgr.create_branch(RUN_ID_A, "beta", oid)
        listed = mgr.list_branches(RUN_ID_A)
        names = [bw.branch_name for bw in listed]
        assert names == sorted(names)

    def test_unknown_run_returns_empty_list(self, seeded_repo: Path) -> None:
        mgr = WorktreeManager(seeded_repo)
        assert mgr.list_branches(RUN_ID_B) == []

    def test_run_with_no_branches_returns_empty_list(
        self, seeded_repo: Path
    ) -> None:
        """A valid run-id that has never had a branch worktree returns []."""
        mgr = WorktreeManager(seeded_repo)
        # RUN_ID_A has no branch directory at all.
        assert mgr.list_branches(RUN_ID_A) == []

    def test_returns_correct_run_id_on_each_entry(
        self, seeded_repo: Path
    ) -> None:
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        mgr.create_branch(RUN_ID_A, "alpha", oid)
        mgr.create_branch(RUN_ID_A, "beta", oid)
        for entry in mgr.list_branches(RUN_ID_A):
            assert entry.run_id == RUN_ID_A


# [unit->REQ-EXEC-PARALLEL-WORKTREE]
class TestRemoveBranch:
    """`remove_branch` drops the directory but retains the ref (§6.7 pattern)."""

    def test_removes_directory(self, seeded_repo: Path) -> None:
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        bw = mgr.create_branch(RUN_ID_A, "side", oid)
        assert bw.path.exists()
        mgr.remove_branch(bw)
        assert not bw.path.exists()

    def test_branch_ref_persists_after_remove(self, seeded_repo: Path) -> None:
        """The branch ref is the durable artifact (§6.7); removing the
        worktree directory must not touch it.
        """
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        bw = mgr.create_branch(RUN_ID_A, "side", oid)
        mgr.remove_branch(bw)
        assert _has_ref(seeded_repo, bw.branch_ref)

    def test_idempotent_second_remove(self, seeded_repo: Path) -> None:
        """Removing a branch whose directory is already absent is a no-op."""
        mgr = WorktreeManager(seeded_repo)
        oid = _head_oid(seeded_repo)
        bw = mgr.create_branch(RUN_ID_A, "side", oid)
        mgr.remove_branch(bw)
        # Second call must not raise.
        mgr.remove_branch(bw)
