"""Tests for fix_detached command."""

from pathlib import Path
from unittest.mock import MagicMock

from typer.testing import CliRunner

from repo_sync_kitty.cli import app
from repo_sync_kitty.commands.fix_detached import (
    _check_repo_clean,
    fix_detached_repo,
)


class TestCheckRepoClean:
    """Tests for _check_repo_clean helper."""

    def test_clean_repo_returns_none(self) -> None:
        mgr = MagicMock()
        mgr.has_untracked_files.return_value = False
        mgr.has_modified_files.return_value = False
        mgr.has_staged_changes.return_value = False
        mgr.get_in_progress_operation.return_value = None
        assert _check_repo_clean(mgr) is None

    def test_untracked_files(self) -> None:
        mgr = MagicMock()
        mgr.has_untracked_files.return_value = True
        mgr.has_modified_files.return_value = False
        mgr.has_staged_changes.return_value = False
        mgr.get_in_progress_operation.return_value = None
        result = _check_repo_clean(mgr)
        assert result is not None
        assert "untracked" in result.lower()

    def test_modified_files(self) -> None:
        mgr = MagicMock()
        mgr.has_untracked_files.return_value = False
        mgr.has_modified_files.return_value = True
        mgr.has_staged_changes.return_value = False
        mgr.get_in_progress_operation.return_value = None
        result = _check_repo_clean(mgr)
        assert result is not None
        assert "modified" in result.lower()

    def test_staged_changes(self) -> None:
        mgr = MagicMock()
        mgr.has_untracked_files.return_value = False
        mgr.has_modified_files.return_value = False
        mgr.has_staged_changes.return_value = True
        mgr.get_in_progress_operation.return_value = None
        result = _check_repo_clean(mgr)
        assert result is not None
        assert "staged" in result.lower()

    def test_in_progress_operation(self) -> None:
        mgr = MagicMock()
        mgr.has_untracked_files.return_value = False
        mgr.has_modified_files.return_value = False
        mgr.has_staged_changes.return_value = False
        mgr.get_in_progress_operation.return_value = "rebase"
        result = _check_repo_clean(mgr)
        assert result is not None
        assert "rebase" in result.lower()

    def test_multiple_issues(self) -> None:
        mgr = MagicMock()
        mgr.has_untracked_files.return_value = True
        mgr.has_modified_files.return_value = True
        mgr.has_staged_changes.return_value = False
        mgr.get_in_progress_operation.return_value = None
        result = _check_repo_clean(mgr)
        assert result is not None
        assert "untracked" in result.lower()
        assert "modified" in result.lower()


class TestFixDetachedRepo:
    """Tests for fix_detached_repo function."""

    def _make_project(
        self, path: str = "myrepo", slug: str = "owner/myrepo", branch: str = "main"
    ) -> MagicMock:
        p = MagicMock()
        p.path = Path(path)
        p.slug = slug
        p.branch = branch
        return p

    def test_skips_when_repo_not_exists(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        project = self._make_project()
        with patch("repo_sync_kitty.commands.fix_detached.RepoManager") as mock_cls:
            mock_cls.return_value.exists.return_value = False
            result = fix_detached_repo(project, tmp_path)
        assert result.action == "skipped"

    def test_skips_when_not_detached(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        project = self._make_project()
        with patch("repo_sync_kitty.commands.fix_detached.RepoManager") as mock_cls:
            mgr = mock_cls.return_value
            mgr.exists.return_value = True
            mgr.is_detached.return_value = False
            result = fix_detached_repo(project, tmp_path)
        assert result.action == "skipped"

    def test_error_when_repo_dirty(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        project = self._make_project()
        with patch("repo_sync_kitty.commands.fix_detached.RepoManager") as mock_cls:
            mgr = mock_cls.return_value
            mgr.exists.return_value = True
            mgr.is_detached.return_value = True
            mgr.has_untracked_files.return_value = True
            mgr.has_modified_files.return_value = False
            mgr.has_staged_changes.return_value = False
            mgr.get_in_progress_operation.return_value = None
            result = fix_detached_repo(project, tmp_path)
        assert result.action == "error"

    def test_fixes_single_local_branch(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        project = self._make_project()
        with patch("repo_sync_kitty.commands.fix_detached.RepoManager") as mock_cls:
            mgr = mock_cls.return_value
            mgr.exists.return_value = True
            mgr.is_detached.return_value = True
            mgr.has_untracked_files.return_value = False
            mgr.has_modified_files.return_value = False
            mgr.has_staged_changes.return_value = False
            mgr.get_in_progress_operation.return_value = None
            mgr.get_branches_at_head.return_value = ["main"]
            result = fix_detached_repo(project, tmp_path)
        assert result.action == "fixed"
        mgr.checkout.assert_called_once_with("main")

    def test_dry_run_does_not_checkout(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        project = self._make_project()
        with patch("repo_sync_kitty.commands.fix_detached.RepoManager") as mock_cls:
            mgr = mock_cls.return_value
            mgr.exists.return_value = True
            mgr.is_detached.return_value = True
            mgr.has_untracked_files.return_value = False
            mgr.has_modified_files.return_value = False
            mgr.has_staged_changes.return_value = False
            mgr.get_in_progress_operation.return_value = None
            mgr.get_branches_at_head.return_value = ["main"]
            result = fix_detached_repo(project, tmp_path, dry_run=True)
        assert result.action == "fixed"
        mgr.checkout.assert_not_called()

    def test_prefers_expected_branch(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        project = self._make_project(branch="main")
        with patch("repo_sync_kitty.commands.fix_detached.RepoManager") as mock_cls:
            mgr = mock_cls.return_value
            mgr.exists.return_value = True
            mgr.is_detached.return_value = True
            mgr.has_untracked_files.return_value = False
            mgr.has_modified_files.return_value = False
            mgr.has_staged_changes.return_value = False
            mgr.get_in_progress_operation.return_value = None
            mgr.get_branches_at_head.return_value = ["develop", "main"]
            result = fix_detached_repo(project, tmp_path)
        assert result.action == "fixed"
        mgr.checkout.assert_called_once_with("main")

    def test_ambiguous_local_branches_error(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        project = self._make_project(branch="main")
        with patch("repo_sync_kitty.commands.fix_detached.RepoManager") as mock_cls:
            mgr = mock_cls.return_value
            mgr.exists.return_value = True
            mgr.is_detached.return_value = True
            mgr.has_untracked_files.return_value = False
            mgr.has_modified_files.return_value = False
            mgr.has_staged_changes.return_value = False
            mgr.get_in_progress_operation.return_value = None
            mgr.get_branches_at_head.return_value = ["develop", "feature"]
            result = fix_detached_repo(project, tmp_path)
        assert result.action == "error"
        assert "ambiguous" in result.message

    def test_no_branches_at_head_error(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        project = self._make_project()
        with patch("repo_sync_kitty.commands.fix_detached.RepoManager") as mock_cls:
            mgr = mock_cls.return_value
            mgr.exists.return_value = True
            mgr.is_detached.return_value = True
            mgr.has_untracked_files.return_value = False
            mgr.has_modified_files.return_value = False
            mgr.has_staged_changes.return_value = False
            mgr.get_in_progress_operation.return_value = None
            mgr.get_branches_at_head.return_value = []
            mgr.get_remote_branches_at_head.return_value = []
            mgr.get_head_commit.return_value = "abc12345"
            result = fix_detached_repo(project, tmp_path)
        assert result.action == "error"
        assert "abc1" in result.message

    def test_remote_branch_matching_expected(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        project = self._make_project(branch="main")
        with patch("repo_sync_kitty.commands.fix_detached.RepoManager") as mock_cls:
            mgr = mock_cls.return_value
            mgr.exists.return_value = True
            mgr.is_detached.return_value = True
            mgr.has_untracked_files.return_value = False
            mgr.has_modified_files.return_value = False
            mgr.has_staged_changes.return_value = False
            mgr.get_in_progress_operation.return_value = None
            mgr.get_branches_at_head.return_value = []
            mgr.get_remote_branches_at_head.return_value = [("origin", "main")]
            result = fix_detached_repo(project, tmp_path)
        assert result.action == "fixed"
        mgr.create_tracking_branch.assert_called_once_with("main", "origin")
        mgr.checkout.assert_called_once_with("main")

    def test_remote_branch_dry_run(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        project = self._make_project(branch="main")
        with patch("repo_sync_kitty.commands.fix_detached.RepoManager") as mock_cls:
            mgr = mock_cls.return_value
            mgr.exists.return_value = True
            mgr.is_detached.return_value = True
            mgr.has_untracked_files.return_value = False
            mgr.has_modified_files.return_value = False
            mgr.has_staged_changes.return_value = False
            mgr.get_in_progress_operation.return_value = None
            mgr.get_branches_at_head.return_value = []
            mgr.get_remote_branches_at_head.return_value = [("origin", "main")]
            result = fix_detached_repo(project, tmp_path, dry_run=True)
        assert result.action == "fixed"
        mgr.create_tracking_branch.assert_not_called()

    def test_checkout_failure(self, tmp_path: Path) -> None:
        from unittest.mock import patch

        project = self._make_project()
        with patch("repo_sync_kitty.commands.fix_detached.RepoManager") as mock_cls:
            mgr = mock_cls.return_value
            mgr.exists.return_value = True
            mgr.is_detached.return_value = True
            mgr.has_untracked_files.return_value = False
            mgr.has_modified_files.return_value = False
            mgr.has_staged_changes.return_value = False
            mgr.get_in_progress_operation.return_value = None
            mgr.get_branches_at_head.return_value = ["main"]
            mgr.checkout.side_effect = Exception("checkout failed")
            result = fix_detached_repo(project, tmp_path)
        assert result.action == "error"
        assert "checkout failed" in result.message


class TestFixDetachedCommand:
    """Tests for fix_detached CLI command."""

    def test_no_manifest_shows_error(
        self, cli_runner: CliRunner, tmp_path: Path
    ) -> None:
        import os

        original = os.getcwd()
        os.chdir(tmp_path)
        try:
            result = cli_runner.invoke(app, ["fix-detached"])
            assert result.exit_code == 2
        finally:
            os.chdir(original)

    def test_no_detached_repos(self, cli_runner: CliRunner, tmp_manifest: Path) -> None:
        from unittest.mock import patch

        with patch("repo_sync_kitty.commands.fix_detached.RepoManager") as mock_cls:
            mgr = mock_cls.return_value
            mgr.exists.return_value = True
            mgr.is_detached.return_value = False
            result = cli_runner.invoke(
                app, ["fix-detached", "-m", str(tmp_manifest), "-v"]
            )
        assert result.exit_code == 0

    def test_dry_run_mode(self, cli_runner: CliRunner, tmp_manifest: Path) -> None:
        from unittest.mock import patch

        with patch("repo_sync_kitty.commands.fix_detached.RepoManager") as mock_cls:
            mgr = mock_cls.return_value
            mgr.exists.return_value = True
            mgr.is_detached.return_value = True
            mgr.has_untracked_files.return_value = False
            mgr.has_modified_files.return_value = False
            mgr.has_staged_changes.return_value = False
            mgr.get_in_progress_operation.return_value = None
            mgr.get_branches_at_head.return_value = ["main"]
            result = cli_runner.invoke(
                app, ["fix-detached", "-m", str(tmp_manifest), "--dry-run", "-v"]
            )
        assert result.exit_code == 0
