"""Tests for launcher.worktree — git worktree management commands."""

import json
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, call, patch

import pytest

from launcher.worktree import (
    _branch_name,
    _ensure_gitignore,
    _worktree_path,
    cleanup,
    create,
    detect,
    diff,
    status,
    sync,
)


class TestHelpers:
    """Tests for worktree helper functions."""

    def test_branch_name(self) -> None:
        assert _branch_name("add-auth") == "spec/add-auth"

    def test_worktree_path_deterministic(self, tmp_path: Path) -> None:
        p1 = _worktree_path(tmp_path, "my-feature")
        p2 = _worktree_path(tmp_path, "my-feature")
        assert p1 == p2
        assert "spec-my-feature-" in str(p1)

    def test_worktree_path_different_slugs(self, tmp_path: Path) -> None:
        p1 = _worktree_path(tmp_path, "feature-a")
        p2 = _worktree_path(tmp_path, "feature-b")
        assert p1 != p2


class TestEnsureGitignore:
    """Tests for _ensure_gitignore."""

    def test_creates_gitignore_when_missing(self, tmp_path: Path) -> None:
        _ensure_gitignore(tmp_path)
        gi = tmp_path / ".gitignore"
        assert gi.exists()
        assert "/.worktrees/" in gi.read_text()

    def test_appends_to_existing_gitignore(self, tmp_path: Path) -> None:
        gi = tmp_path / ".gitignore"
        gi.write_text("node_modules/\n")
        _ensure_gitignore(tmp_path)
        content = gi.read_text()
        assert "node_modules/" in content
        assert "/.worktrees/" in content

    def test_idempotent_when_already_present(self, tmp_path: Path) -> None:
        gi = tmp_path / ".gitignore"
        gi.write_text("/.worktrees/\n")
        _ensure_gitignore(tmp_path)
        content = gi.read_text()
        assert content.count("/.worktrees/") == 1


class TestDetect:
    """Tests for worktree detect command."""

    def test_not_found_when_dir_missing(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        with patch("launcher.worktree._git_root", return_value=tmp_path):
            detect("nonexistent", as_json=True)
        output = json.loads(capsys.readouterr().out)
        assert output["found"] is False

    def test_found_when_dir_exists(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        wt_path = _worktree_path(tmp_path, "my-slug")
        wt_path.mkdir(parents=True)

        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._current_branch", return_value="main"):
                detect("my-slug", as_json=True)
        output = json.loads(capsys.readouterr().out)
        assert output["found"] is True
        assert output["branch"] == "spec/my-slug"

    def test_found_uses_state_file_base_branch(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        """base_branch in detect() output must come from state file, not current branch."""
        wt_path = _worktree_path(tmp_path, "my-slug")
        wt_path.mkdir(parents=True)
        state = {
            "slug": "my-slug",
            "path": str(wt_path),
            "branch": "spec/my-slug",
            "base_branch": "feature/original-base",
        }
        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._load_worktree_state", return_value=state):
                with patch("launcher.worktree._current_branch", return_value="main"):
                    detect("my-slug", as_json=True)
        output = json.loads(capsys.readouterr().out)
        assert output["found"] is True
        assert output["base_branch"] == "feature/original-base"

    def test_not_a_git_repo(self, capsys: pytest.CaptureFixture[str]) -> None:
        with patch("launcher.worktree._git_root", return_value=None):
            detect("any", as_json=True)
        output = json.loads(capsys.readouterr().out)
        assert output["found"] is False
        assert "error" in output


class TestCreate:
    """Tests for worktree create command."""

    def test_fails_on_dirty_working_tree(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        dirty_status = MagicMock(returncode=0, stdout=" M file.py\n", stderr="")
        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._git", return_value=dirty_status):
                with pytest.raises(SystemExit, match="1"):
                    create("my-feat", as_json=True)
        output = json.loads(capsys.readouterr().out)
        assert output["success"] is False
        assert output["error"] == "dirty"

    def test_returns_existing_when_path_exists(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        wt_path = _worktree_path(tmp_path, "existing")
        wt_path.mkdir(parents=True)

        clean_status = MagicMock(returncode=0, stdout="", stderr="")
        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._git", return_value=clean_status):
                with patch("launcher.worktree._current_branch", return_value="main"):
                    with patch("launcher.worktree._save_worktree_state"):
                        create("existing", as_json=True)
        output = json.loads(capsys.readouterr().out)
        assert output["success"] is True
        assert output["note"] == "already exists"


class TestCleanup:
    """Tests for worktree cleanup command."""

    def test_blocks_on_uncommitted_changes(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        wt_path = _worktree_path(tmp_path, "dirty-feat")
        wt_path.mkdir(parents=True)

        def mock_git(*args, **kwargs):
            if args[0] == "status" and args[1] == "--porcelain":
                return MagicMock(returncode=0, stdout=" M dirty.py\n", stderr="")
            return MagicMock(returncode=0, stdout=str(tmp_path) + "\n", stderr="")

        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._git", side_effect=mock_git):
                with pytest.raises(SystemExit, match="1"):
                    cleanup("dirty-feat", as_json=True)
        output = json.loads(capsys.readouterr().out)
        assert output["success"] is False
        assert output["error"] == "uncommitted_changes"

    def test_force_ignores_uncommitted_changes(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        wt_path = _worktree_path(tmp_path, "force-feat")
        wt_path.mkdir(parents=True)

        call_log: list[tuple] = []

        def mock_git(*args, **kwargs):
            call_log.append(args)
            if args[0] == "status" and args[1] == "--porcelain":
                return MagicMock(returncode=0, stdout=" M dirty.py\n", stderr="")
            return MagicMock(returncode=0, stdout="", stderr="")

        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._git", side_effect=mock_git):
                with patch("launcher.worktree._clear_worktree_state"):
                    # Force should skip the uncommitted check
                    cleanup("force-feat", as_json=True, force=True)
        output = json.loads(capsys.readouterr().out)
        assert output["success"] is True

    def test_succeeds_on_clean_worktree(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        wt_path = _worktree_path(tmp_path, "clean-feat")
        wt_path.mkdir(parents=True)

        def mock_git(*args, **kwargs):
            if args[0] == "status" and args[1] == "--porcelain":
                return MagicMock(returncode=0, stdout="", stderr="")
            return MagicMock(returncode=0, stdout="", stderr="")

        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._git", side_effect=mock_git):
                with patch("launcher.worktree._clear_worktree_state"):
                    cleanup("clean-feat", as_json=True)
        output = json.loads(capsys.readouterr().out)
        assert output["success"] is True


class TestWorktreeStatus:
    """Tests for worktree status command."""

    def test_inactive_when_no_state(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        with patch("launcher.worktree._load_worktree_state", return_value=None):
            status(as_json=True)
        output = json.loads(capsys.readouterr().out)
        assert output["active"] is False

    def test_active_when_state_exists(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        wt_dir = tmp_path / "worktree"
        wt_dir.mkdir()
        state = {
            "slug": "feat",
            "path": str(wt_dir),
            "branch": "spec/feat",
            "base_branch": "main",
        }
        with patch("launcher.worktree._load_worktree_state", return_value=state):
            status(as_json=True)
        output = json.loads(capsys.readouterr().out)
        assert output["active"] is True
        assert output["branch"] == "spec/feat"


class TestCreateGitError:
    """Edge cases for worktree create when git operations fail."""

    def test_fails_when_not_a_git_repo(self, capsys: pytest.CaptureFixture[str]) -> None:
        with patch("launcher.worktree._git_root", return_value=None):
            with pytest.raises(SystemExit, match="1"):
                create("my-feat", as_json=True)
        output = json.loads(capsys.readouterr().out)
        assert output["success"] is False
        assert output["error"] == "not a git repository"

    def test_fails_when_git_worktree_add_fails(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        """git worktree add non-zero exit → error JSON + SystemExit(1)."""
        call_count = [0]

        def mock_git(*args, **kwargs):
            call_count[0] += 1
            # First call is "status --porcelain" — return clean
            if args[0] == "status":
                return MagicMock(returncode=0, stdout="", stderr="")
            # Second call is "worktree add" — simulate failure
            return MagicMock(returncode=128, stdout="", stderr="fatal: branch already checked out")

        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._git", side_effect=mock_git):
                with patch("launcher.worktree._current_branch", return_value="main"):
                    with patch("launcher.worktree._ensure_gitignore"):
                        with pytest.raises(SystemExit, match="1"):
                            create("new-feat", as_json=True)

        output = json.loads(capsys.readouterr().out)
        assert output["success"] is False
        assert output["error"] == "git_error"
        assert "already checked out" in output["detail"]


class TestDiff:
    """Tests for worktree diff command."""

    def test_fails_when_not_a_git_repo(self, capsys: pytest.CaptureFixture[str]) -> None:
        with patch("launcher.worktree._git_root", return_value=None):
            with pytest.raises(SystemExit, match="1"):
                diff("my-feat", as_json=True)
        output = json.loads(capsys.readouterr().out)
        assert "error" in output

    def test_fails_when_worktree_missing(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with pytest.raises(SystemExit, match="1"):
                diff("missing-slug", as_json=True)
        output = json.loads(capsys.readouterr().out)
        assert output["error"] == "worktree not found"

    def test_returns_changed_files(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        wt_path = _worktree_path(tmp_path, "my-feat")
        wt_path.mkdir(parents=True)

        git_diff_output = "M\tsrc/foo.py\nA\tsrc/bar.py\n"
        mock_result = MagicMock(returncode=0, stdout=git_diff_output, stderr="")

        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._load_worktree_state", return_value={"base_branch": "main"}):
                with patch("launcher.worktree._git", return_value=mock_result):
                    diff("my-feat", as_json=True)

        output = json.loads(capsys.readouterr().out)
        assert output["count"] == 2
        assert output["files"][0] == {"status": "M", "path": "src/foo.py"}
        assert output["files"][1] == {"status": "A", "path": "src/bar.py"}

    def test_uses_main_as_base_when_no_state(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        wt_path = _worktree_path(tmp_path, "my-feat")
        wt_path.mkdir(parents=True)

        mock_result = MagicMock(returncode=0, stdout="M\tfile.py\n", stderr="")

        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._load_worktree_state", return_value=None):
                with patch("launcher.worktree._git", return_value=mock_result) as mock_g:
                    diff("my-feat", as_json=True)

        # Verify "main" was used as the base in the diff command
        mock_g.assert_called_once_with("diff", "--name-status", "main", cwd=wt_path)


class TestSync:
    """Tests for worktree sync command."""

    def test_fails_when_not_a_git_repo(self, capsys: pytest.CaptureFixture[str]) -> None:
        with patch("launcher.worktree._git_root", return_value=None):
            with pytest.raises(SystemExit, match="1"):
                sync("my-feat", as_json=True)
        output = json.loads(capsys.readouterr().out)
        assert output["success"] is False

    def test_fails_when_worktree_missing(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with pytest.raises(SystemExit, match="1"):
                sync("missing-slug", as_json=True)
        output = json.loads(capsys.readouterr().out)
        assert output["success"] is False
        assert output["error"] == "worktree not found"

    def test_fails_when_checkout_fails(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        wt_path = _worktree_path(tmp_path, "my-feat")
        wt_path.mkdir(parents=True)

        def mock_git(*args, **kwargs):
            if args[0] == "checkout":
                return MagicMock(returncode=1, stdout="", stderr="error: local changes")
            return MagicMock(returncode=0, stdout="", stderr="")

        state = {"base_branch": "main", "slug": "my-feat", "branch": "spec/my-feat"}
        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._load_worktree_state", return_value=state):
                with patch("launcher.worktree._git", side_effect=mock_git):
                    with pytest.raises(SystemExit, match="1"):
                        sync("my-feat", as_json=True)

        output = json.loads(capsys.readouterr().out)
        assert output["success"] is False
        assert output["error"] == "checkout_failed"

    def test_fails_when_merge_fails(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        wt_path = _worktree_path(tmp_path, "my-feat")
        wt_path.mkdir(parents=True)

        def mock_git(*args, **kwargs):
            if args[0] == "checkout":
                return MagicMock(returncode=0, stdout="", stderr="")
            if args[0] == "merge":
                return MagicMock(returncode=1, stdout="", stderr="CONFLICT (content)")
            return MagicMock(returncode=0, stdout="", stderr="")

        state = {"base_branch": "main", "slug": "my-feat", "branch": "spec/my-feat"}
        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._load_worktree_state", return_value=state):
                with patch("launcher.worktree._git", side_effect=mock_git):
                    with pytest.raises(SystemExit, match="1"):
                        sync("my-feat", as_json=True)

        output = json.loads(capsys.readouterr().out)
        assert output["success"] is False
        assert output["error"] == "merge_failed"

    def test_fails_when_nothing_to_commit(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        """Squash merge with no net changes → nothing_to_commit error."""
        wt_path = _worktree_path(tmp_path, "my-feat")
        wt_path.mkdir(parents=True)

        def mock_git(*args, **kwargs):
            if args[0] == "checkout":
                return MagicMock(returncode=0, stdout="", stderr="")
            if args[0] == "merge":
                return MagicMock(returncode=0, stdout="Squash commit -- not updating HEAD", stderr="")
            if args[0] == "commit":
                return MagicMock(returncode=1, stdout="", stderr="nothing to commit")
            return MagicMock(returncode=0, stdout="", stderr="")

        state = {"base_branch": "main", "slug": "my-feat", "branch": "spec/my-feat"}
        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._load_worktree_state", return_value=state):
                with patch("launcher.worktree._git", side_effect=mock_git):
                    with pytest.raises(SystemExit, match="1"):
                        sync("my-feat", as_json=True)

        output = json.loads(capsys.readouterr().out)
        assert output["success"] is False
        assert output["error"] == "nothing_to_commit"

    def test_succeeds_and_reports_files_changed(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        wt_path = _worktree_path(tmp_path, "my-feat")
        wt_path.mkdir(parents=True)

        def mock_git(*args, **kwargs):
            if args[0] == "checkout":
                return MagicMock(returncode=0, stdout="", stderr="")
            if args[0] == "merge":
                return MagicMock(returncode=0, stdout="", stderr="")
            if args[0] == "commit":
                return MagicMock(returncode=0, stdout="", stderr="")
            if args[0] == "rev-parse" and "--short" in args:
                return MagicMock(returncode=0, stdout="abc1234\n", stderr="")
            if args[0] == "diff-tree":
                return MagicMock(returncode=0, stdout="src/a.py\nsrc/b.py\n", stderr="")
            return MagicMock(returncode=0, stdout="", stderr="")

        state = {"base_branch": "main", "slug": "my-feat", "branch": "spec/my-feat"}
        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._load_worktree_state", return_value=state):
                with patch("launcher.worktree._git", side_effect=mock_git):
                    sync("my-feat", as_json=True)

        output = json.loads(capsys.readouterr().out)
        assert output["success"] is True
        assert output["files_changed"] == 2
        assert output["commit_hash"] == "abc1234"

    def test_uses_diff_tree_to_count_files(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        """sync should use diff-tree --no-commit-id (works on repos with only one commit)."""
        wt_path = _worktree_path(tmp_path, "my-feat")
        wt_path.mkdir(parents=True)

        git_calls: list[tuple] = []

        def mock_git(*args, **kwargs):
            git_calls.append(args)
            if args[0] == "checkout":
                return MagicMock(returncode=0, stdout="", stderr="")
            if args[0] == "merge":
                return MagicMock(returncode=0, stdout="", stderr="")
            if args[0] == "commit":
                return MagicMock(returncode=0, stdout="", stderr="")
            if args[0] == "rev-parse" and "--short" in args:
                return MagicMock(returncode=0, stdout="abc1234\n", stderr="")
            if args[0] == "diff-tree":
                return MagicMock(returncode=0, stdout="src/a.py\nsrc/b.py\n", stderr="")
            return MagicMock(returncode=0, stdout="", stderr="")

        state = {"base_branch": "main", "slug": "my-feat", "branch": "spec/my-feat"}
        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._load_worktree_state", return_value=state):
                with patch("launcher.worktree._git", side_effect=mock_git):
                    sync("my-feat", as_json=True)

        output = json.loads(capsys.readouterr().out)
        assert output["success"] is True
        assert output["files_changed"] == 2
        diff_tree_calls = [c for c in git_calls if c[0] == "diff-tree"]
        assert len(diff_tree_calls) == 1
        assert "--no-commit-id" in diff_tree_calls[0]
        assert "-r" in diff_tree_calls[0]
        assert "--name-only" in diff_tree_calls[0]
        assert "HEAD" in diff_tree_calls[0]


class TestCleanupEdgeCases:
    """Edge cases for cleanup not covered in TestCleanup."""

    def test_succeeds_when_worktree_path_already_gone(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
        """Cleanup should succeed even if the worktree directory no longer exists."""
        with patch("launcher.worktree._git_root", return_value=tmp_path):
            with patch("launcher.worktree._git", return_value=MagicMock(returncode=0, stdout="", stderr="")):
                with patch("launcher.worktree._clear_worktree_state"):
                    # The slug has no matching directory, so wt_path.exists() is False
                    cleanup("already-gone", as_json=True)

        output = json.loads(capsys.readouterr().out)
        assert output["success"] is True
