"""Tests for move command helpers and CLI."""

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

from typer.testing import CliRunner

from repo_sync_kitty.cli import app
from repo_sync_kitty.commands.move import (
    _find_project_by_path,
    _slug_derivable_from_path,
    _update_manifest_path,
)


class TestSlugDerivableFromPath:
    """Tests for _slug_derivable_from_path helper."""

    def test_slug_matches_last_component(self) -> None:
        assert _slug_derivable_from_path("libs/hello-world", "hello-world") is True

    def test_slug_does_not_match(self) -> None:
        assert _slug_derivable_from_path("libs/hello-world", "other") is False

    def test_single_component_path(self) -> None:
        assert _slug_derivable_from_path("hello-world", "hello-world") is True

    def test_trailing_slash(self) -> None:
        assert _slug_derivable_from_path("libs/hello-world/", "hello-world") is True


class TestFindProjectByPath:
    """Tests for _find_project_by_path helper."""

    def test_exact_match(self, tmp_manifest: Path) -> None:
        result = _find_project_by_path(tmp_manifest, "hello-world")
        assert result is not None
        assert result[0] == "hello-world"

    def test_no_match(self, tmp_manifest: Path) -> None:
        result = _find_project_by_path(tmp_manifest, "nonexistent")
        assert result is None


class TestUpdateManifestPath:
    """Tests for _update_manifest_path helper."""

    def test_array_format_simple_rename(self, tmp_path: Path) -> None:
        manifest = tmp_path / "manifest.toml"
        manifest.write_text(
            '[common]\nroot_path = "/tmp"\nbranch = "main"\nremote = "origin"\n\n'
            '[[remotes]]\nname = "origin"\nbase_url = "https://github.com/"\n\n'
            '[[projects]]\npath = "old/path"\nslug = "owner/repo"\n'
        )
        result = _update_manifest_path(manifest, "old/path", "new/path", "owner/repo")
        assert result is True
        content = manifest.read_text()
        assert 'path = "new/path"' in content
        assert 'path = "old/path"' not in content

    def test_array_format_add_slug_when_not_derivable(self, tmp_path: Path) -> None:
        manifest = tmp_path / "manifest.toml"
        manifest.write_text(
            '[common]\nroot_path = "/tmp"\nbranch = "main"\nremote = "origin"\n\n'
            '[[remotes]]\nname = "origin"\nbase_url = "https://github.com/"\n\n'
            '[[projects]]\npath = "myrepo"\n'
        )
        result = _update_manifest_path(manifest, "myrepo", "archive/old", "myrepo")
        assert result is True
        content = manifest.read_text()
        assert 'path = "archive/old"' in content
        assert 'slug = "myrepo"' in content

    def test_array_format_remove_slug_when_derivable(self, tmp_path: Path) -> None:
        manifest = tmp_path / "manifest.toml"
        manifest.write_text(
            '[common]\nroot_path = "/tmp"\nbranch = "main"\nremote = "origin"\n\n'
            '[[remotes]]\nname = "origin"\nbase_url = "https://github.com/"\n\n'
            '[[projects]]\npath = "archive/old"\nslug = "myrepo"\n'
        )
        result = _update_manifest_path(manifest, "archive/old", "myrepo", "myrepo")
        assert result is True
        content = manifest.read_text()
        assert 'path = "myrepo"' in content
        assert 'slug = "myrepo"' not in content

    def test_dict_format_simple_rename(self, tmp_path: Path) -> None:
        manifest = tmp_path / "manifest.toml"
        manifest.write_text(
            '[common]\nroot_path = "/tmp"\nbranch = "main"\nremote = "origin"\n\n'
            '[remotes]\norigin = { base_url = "https://github.com/" }\n\n'
            '[projects]\n"old/path" = { slug = "owner/repo" }\n'
        )
        result = _update_manifest_path(manifest, "old/path", "new/path", "owner/repo")
        assert result is True
        content = manifest.read_text()
        assert '"new/path"' in content
        assert '"old/path"' not in content

    def test_returns_false_when_path_not_found(self, tmp_path: Path) -> None:
        manifest = tmp_path / "manifest.toml"
        manifest.write_text(
            '[common]\nroot_path = "/tmp"\nbranch = "main"\nremote = "origin"\n\n'
            '[[remotes]]\nname = "origin"\nbase_url = "https://github.com/"\n\n'
            '[[projects]]\npath = "some/other"\n'
        )
        result = _update_manifest_path(manifest, "nonexistent", "new/path", "slug")
        assert result is False


class TestMoveCommand:
    """Tests for move CLI command."""

    def test_move_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, ["move", "old", "new"])
            assert result.exit_code == 2
        finally:
            os.chdir(original)

    def test_move_project_not_found(
        self, cli_runner: CliRunner, tmp_manifest: Path
    ) -> None:
        result = cli_runner.invoke(
            app, ["move", "nonexistent", "new/path", "-m", str(tmp_manifest)]
        )
        assert result.exit_code == 2

    @patch("repo_sync_kitty.commands.move.RepoManager")
    def test_move_source_not_exists(
        self,
        _mock_mgr_cls: MagicMock,
        cli_runner: CliRunner,
        tmp_manifest: Path,
    ) -> None:
        result = cli_runner.invoke(
            app, ["move", "hello-world", "new/path", "-m", str(tmp_manifest)]
        )
        assert result.exit_code == 2

    @patch("repo_sync_kitty.commands.move._update_manifest_path")
    @patch("repo_sync_kitty.commands.move.os.rename")
    @patch("repo_sync_kitty.commands.move.RepoManager")
    def test_move_dry_run(
        self,
        mock_mgr_cls: MagicMock,
        mock_rename: MagicMock,
        _mock_update: MagicMock,
        cli_runner: CliRunner,
        tmp_manifest: Path,
    ) -> None:
        root_path = Path("/tmp/test-repos")
        root_path.mkdir(parents=True, exist_ok=True)
        src_path = root_path / "hello-world"
        src_path.mkdir(parents=True, exist_ok=True)

        mgr = mock_mgr_cls.return_value
        mgr.exists.return_value = True
        mgr.is_clean.return_value = True
        mgr.get_in_progress_operation.return_value = None

        result = cli_runner.invoke(
            app,
            ["move", "hello-world", "new/path", "-m", str(tmp_manifest), "--dry-run"],
        )
        assert result.exit_code == 0
        assert "Dry run" in result.stdout or "dry run" in result.stdout.lower()
        mock_rename.assert_not_called()

    @patch("repo_sync_kitty.commands.move.RepoManager")
    def test_move_same_path_warns(
        self,
        _mock_mgr_cls: MagicMock,
        cli_runner: CliRunner,
        tmp_manifest: Path,
    ) -> None:
        result = cli_runner.invoke(
            app, ["move", "hello-world", "hello-world", "-m", str(tmp_manifest)]
        )
        assert result.exit_code == 0
        assert "same" in result.stdout.lower()

    @patch("repo_sync_kitty.commands.move.RepoManager")
    def test_move_destination_exists_error(
        self,
        mock_mgr_cls: MagicMock,
        cli_runner: CliRunner,
        tmp_manifest: Path,
    ) -> None:
        root_path = Path("/tmp/test-repos")
        root_path.mkdir(parents=True, exist_ok=True)
        src_path = root_path / "hello-world"
        src_path.mkdir(parents=True, exist_ok=True)
        dest_path = root_path / "new-dest"
        dest_path.mkdir(parents=True, exist_ok=True)

        mgr = mock_mgr_cls.return_value
        mgr.exists.return_value = True

        result = cli_runner.invoke(
            app, ["move", "hello-world", "new-dest", "-m", str(tmp_manifest)]
        )
        assert result.exit_code == 2

    @patch("repo_sync_kitty.commands.move.RepoManager")
    def test_move_dirty_repo_without_force(
        self,
        mock_mgr_cls: MagicMock,
        cli_runner: CliRunner,
        tmp_manifest: Path,
    ) -> None:
        root_path = Path("/tmp/test-repos")
        root_path.mkdir(parents=True, exist_ok=True)
        src_path = root_path / "hello-world"
        src_path.mkdir(parents=True, exist_ok=True)

        mgr = mock_mgr_cls.return_value
        mgr.exists.return_value = True
        mgr.is_clean.return_value = False
        mgr.get_in_progress_operation.return_value = None

        result = cli_runner.invoke(
            app, ["move", "hello-world", "moved/path", "-m", str(tmp_manifest)]
        )
        assert result.exit_code == 2
        assert "uncommitted" in result.stdout.lower()

    @patch("repo_sync_kitty.commands.move.RepoManager")
    def test_move_in_progress_operation(
        self,
        mock_mgr_cls: MagicMock,
        cli_runner: CliRunner,
        tmp_manifest: Path,
    ) -> None:
        root_path = Path("/tmp/test-repos")
        root_path.mkdir(parents=True, exist_ok=True)
        src_path = root_path / "hello-world"
        src_path.mkdir(parents=True, exist_ok=True)

        mgr = mock_mgr_cls.return_value
        mgr.exists.return_value = True
        mgr.is_clean.return_value = True
        mgr.get_in_progress_operation.return_value = "rebase"

        result = cli_runner.invoke(
            app, ["move", "hello-world", "moved/path", "-m", str(tmp_manifest)]
        )
        assert result.exit_code == 2
        assert "rebase" in result.stdout.lower()

    @patch("repo_sync_kitty.commands.move._update_manifest_path")
    @patch("repo_sync_kitty.commands.move.os.rename")
    @patch("repo_sync_kitty.commands.move.RepoManager")
    def test_move_success(
        self,
        mock_mgr_cls: MagicMock,
        mock_rename: MagicMock,
        mock_update: MagicMock,
        cli_runner: CliRunner,
        tmp_manifest: Path,
    ) -> None:
        root_path = Path("/tmp/test-repos")
        root_path.mkdir(parents=True, exist_ok=True)
        src_path = root_path / "hello-world"
        src_path.mkdir(parents=True, exist_ok=True)

        mgr = mock_mgr_cls.return_value
        mgr.exists.return_value = True
        mgr.is_clean.return_value = True
        mgr.get_in_progress_operation.return_value = None
        mock_update.return_value = True

        result = cli_runner.invoke(
            app, ["move", "hello-world", "moved/path", "-m", str(tmp_manifest)]
        )
        assert result.exit_code == 0
        mock_rename.assert_called_once()

    @patch("repo_sync_kitty.commands.move.RepoManager")
    def test_move_source_not_on_disk(
        self,
        _mock_mgr_cls: MagicMock,
        cli_runner: CliRunner,
        tmp_manifest: Path,
    ) -> None:
        # Source path doesn't exist on disk (not cloned)
        import shutil

        src = Path("/tmp/test-repos/hello-world")
        if src.exists():
            shutil.rmtree(src)

        result = cli_runner.invoke(
            app, ["move", "hello-world", "new/path", "-m", str(tmp_manifest)]
        )
        assert result.exit_code == 2
