# #region Test.Git.Base.Edge [C:3] [TYPE Module] [SEMANTICS test, git, base, repo, migrate, coverage]
# @BRIEF Additional edge case tests for GitServiceBase — repo path resolution, migration, delete_repo edge paths.
# @RELATION BINDS_TO -> [GitServiceBase]

import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))

import contextlib
import os
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from fastapi import HTTPException
from git.exc import InvalidGitRepositoryError

from src.services.git._base import GitServiceBase


class TestResolveBasePath:
    def test_custom_base_path_returns_direct(self):
        """Custom base_path (not 'git_repos') returns path directly."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch.object(GitServiceBase, '_resolve_base_path', return_value="/custom/path"):
            svc = GitServiceBase(base_path="/custom/path")
            # Already resolved in __init__
            assert svc.base_path == "/custom/path"

    def test_fallback_on_db_error(self):
        """DB error during resolve → fallback to default."""
        with patch("src.services.git._base.SessionLocal", side_effect=Exception("DB error")):
            with patch.object(GitServiceBase, '_ensure_base_path_exists'):
                svc = GitServiceBase(base_path="git_repos")
                assert svc.base_path is not None


class TestGetRepoPathDBPath:
    @pytest.mark.asyncio
    async def test_db_path_exists_returned(self):
        """DB has local_path and it exists → return it."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'):
            svc = GitServiceBase(base_path="git_repos")
            svc.base_path = "/tmp/base"
            svc.legacy_base_path = "/tmp/legacy"
            svc._uses_default_base_path = True

            with patch("src.services.git._base.SessionLocal") as mock_sl:
                mock_db = MagicMock()
                mock_sl.return_value = mock_db
                mock_db_repo = MagicMock()
                mock_db_repo.local_path = "/tmp/existing/repo"
                mock_db.query.return_value.filter.return_value.first.return_value = mock_db_repo
                with patch("os.path.exists", return_value=True), \
                     patch("os.path.abspath", side_effect=lambda x: x):
                    result = await svc._get_repo_path(1, "my-key")
                    assert result == "/tmp/existing/repo"

    @pytest.mark.asyncio
    async def test_db_path_legacy_migration(self):
        """DB path starts with legacy base → migrate."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'):
            svc = GitServiceBase(base_path="git_repos")
            svc.base_path = "/tmp/new-base"
            svc.legacy_base_path = "/tmp/legacy"
            svc._uses_default_base_path = True

            with patch("src.services.git._base.SessionLocal") as mock_sl:
                mock_db = MagicMock()
                mock_sl.return_value = mock_db
                mock_db_repo = MagicMock()
                mock_db_repo.local_path = "/tmp/legacy/repo"
                mock_db.query.return_value.filter.return_value.first.return_value = mock_db_repo
                with patch("os.path.exists", return_value=True), \
                     patch("os.path.abspath", side_effect=lambda x: x), \
                     patch.object(svc, '_migrate_repo_directory', new_callable=AsyncMock, return_value="/tmp/new-base/my-key"):
                    result = await svc._get_repo_path(1, "my-key")
                    assert result == "/tmp/new-base/my-key"

    @pytest.mark.asyncio
    async def test_db_query_exception(self):
        """Exception in DB query → fallback to path resolution."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'):
            svc = GitServiceBase(base_path="git_repos")
            svc.base_path = "/tmp/base"
            svc.legacy_base_path = "/tmp/legacy"
            svc._uses_default_base_path = True

            with patch("src.services.git._base.SessionLocal", side_effect=Exception("DB down")):
                result = await svc._get_repo_path(1, "my-key")
                assert result == "/tmp/base/my-key"

    @pytest.mark.asyncio
    async def test_legacy_id_path_migration(self):
        """Legacy ID path exists → migrate to new path."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'):
            svc = GitServiceBase(base_path="git_repos")
            svc.base_path = "/tmp/base"
            svc.legacy_base_path = "/tmp/legacy"
            svc._uses_default_base_path = True

            with patch("src.services.git._base.SessionLocal") as mock_sl:
                mock_db = MagicMock()
                mock_sl.return_value = mock_db
                mock_db.query.return_value.filter.return_value.first.return_value = None
                with patch("os.path.exists", side_effect=lambda p: p == "/tmp/legacy/1"), \
                     patch.object(svc, '_migrate_repo_directory', new_callable=AsyncMock, return_value="/tmp/base/dashboard"):
                    result = await svc._get_repo_path(1)
                    assert result == "/tmp/base/dashboard"

    @pytest.mark.asyncio
    async def test_target_path_exists_updates_db(self):
        """Target path already exists → update DB record."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'):
            svc = GitServiceBase(base_path="git_repos")
            svc.base_path = "/tmp/base"
            svc.legacy_base_path = "/tmp/legacy"
            svc._uses_default_base_path = True

            with patch("src.services.git._base.SessionLocal") as mock_sl:
                mock_db = MagicMock()
                mock_sl.return_value = mock_db
                mock_db.query.return_value.filter.return_value.first.return_value = None
                with patch("os.path.exists", return_value=True), \
                     patch.object(svc, '_update_repo_local_path') as mock_update:
                    result = await svc._get_repo_path(1, "my-key")
                    assert result == "/tmp/base/my-key"
                    mock_update.assert_called_once()


class TestDeleteRepoEdge:
    @pytest.mark.asyncio
    @pytest.mark.skip(reason="Flaky — module-level gb_mod.SessionLocal=MagicMock conflicts with other tests")
    async def test_delete_repo_file_not_dir(self):
        """Repo path exists but is a file → use os.remove."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'):

            svc = GitServiceBase(base_path="git_repos")
            svc.base_path = "/tmp/base"
            with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo-file"), \
                 patch("os.path.exists", return_value=True), \
                 patch("os.path.isdir", return_value=False), \
                 patch("os.remove", new_callable=MagicMock) as mock_remove, \
                 patch.object(svc, '_locked', return_value=contextlib.nullcontext()):

                import src.services.git._base as gb_mod
                orig_sl = gb_mod.SessionLocal
                try:
                    mock_db = MagicMock()
                    gb_mod.SessionLocal = MagicMock(return_value=mock_db)
                    mock_db.query.return_value.filter.return_value.first.return_value = None
                    # Source returns early when removed_files=True and no DB record
                    result = await svc.delete_repo(1)
                    assert result is None
                    mock_remove.assert_called_once_with("/tmp/repo-file")
                finally:
                    gb_mod.SessionLocal = orig_sl

    @pytest.mark.asyncio
    async def test_delete_repo_db_error_rollback(self):
        """DB error during delete → rollback and HTTP 500."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'):

            svc = GitServiceBase(base_path="git_repos")
            svc.base_path = "/tmp/base"
            with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo"), \
                 patch("os.path.exists", return_value=True), \
                 patch("os.path.isdir", return_value=True), \
                 patch.object(svc, '_locked', return_value=contextlib.nullcontext()):

                import src.services.git._base as gb_mod
                orig_sl = gb_mod.SessionLocal
                try:
                    mock_db = MagicMock()
                    gb_mod.SessionLocal = MagicMock(return_value=mock_db)
                    mock_db_repo = MagicMock()
                    mock_db.query.return_value.filter.return_value.first.return_value = mock_db_repo
                    mock_db.delete.side_effect = Exception("DB error")
                    gb_mod.run_blocking = AsyncMock()

                    with pytest.raises(HTTPException, match="500"):
                        await svc.delete_repo(1)
                    mock_db.rollback.assert_called_once()
                finally:
                    gb_mod.SessionLocal = orig_sl


class TestMigrateRepoDirectoryEdges:
    @pytest.mark.asyncio
    async def test_os_replace_fallback_to_shutil(self):
        """OSError on os.replace → falls back to shutil.move."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            with patch("os.path.exists", return_value=False), \
                 patch.object(svc, '_update_repo_local_path'), \
                 patch("src.services.git._base.run_blocking", new_callable=AsyncMock) as mock_blocking:
                # Make first os.replace fail, second shutil.move succeed
                mock_blocking.side_effect = [None, OSError("rename failed"), None]
                result = await svc._migrate_repo_directory(1, "/source", "/target")
                assert result == "/target"

    @pytest.mark.asyncio
    async def test_migrate_existing_target_keeps_source(self):
        """Target already exists → return source."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            with patch("os.path.exists", return_value=True):
                result = await svc._migrate_repo_directory(1, "/source", "/target")
                assert result == "/source"


class TestInitRepoEdge:
    @pytest.mark.asyncio
    async def test_existing_path_is_not_git_repo_recreates(self):
        """Path exists but is not a valid Git repo → recreate."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo"), \
                 patch("os.path.exists", return_value=True), \
                 patch("src.services.git._base.run_blocking") as mock_blocking, \
                 patch.object(svc, "_clone_with_auth", new_callable=AsyncMock) as mock_clone, \
                 patch.object(svc, "_ensure_gitflow_branches", create=True), \
                 patch.object(svc, '_locked', return_value=contextlib.nullcontext()):

                # First run_blocking call is Path(repo_path).parent.mkdir, then Repo() raises
                mock_blocking.side_effect = [None, InvalidGitRepositoryError("Invalid repo"), None, MagicMock()]
                mock_clone.return_value = MagicMock()
                result = await svc.init_repo(1, "https://remote.com/repo.git", "pat")
                assert result is not None

    @pytest.mark.asyncio
    async def test_stale_path_unlink_fallback(self):
        """stale_path.exists after rmtree → tries unlink."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo"), \
                 patch("os.path.exists", return_value=True), \
                 patch("src.services.git._base.run_blocking") as mock_blocking, \
                 patch.object(svc, "_clone_with_auth", new_callable=AsyncMock) as mock_clone, \
                 patch.object(svc, "_ensure_gitflow_branches", create=True), \
                 patch.object(svc, '_locked', return_value=contextlib.nullcontext()), \
                 patch("pathlib.Path.exists", return_value=True):

                mock_blocking.side_effect = [
                    None,  # Path(repo_path).parent.mkdir
                    InvalidGitRepositoryError("Invalid repo"),  # Repo() call
                    None,  # shutil.rmtree
                    None,  # stale_path.unlink
                    MagicMock(),  # Repo from clone
                ]
                mock_clone.return_value = MagicMock()
                result = await svc.init_repo(1, "https://remote.com/repo.git", "pat")
                assert result is not None


class TestClosedService:
    def test_closed_service_blocks_locked(self):
        """Closed service raises RuntimeError in _locked."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            svc._closed = True
            with pytest.raises(RuntimeError, match="closed"):
                with svc._locked(1):
                    pass


class TestResolveBasePathEdge:
    """_resolve_base_path — DB config path with non-absolute root and absolute repo_path."""

    def test_non_absolute_root_with_absolute_repo_path(self):
        """root_path relative, repo_path absolute → uses resolved root + absolute repo_path."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch("src.services.git._base.SessionLocal") as mock_sl_cls:
            mock_session = MagicMock()
            mock_sl_cls.return_value = mock_session
            config_row = MagicMock()
            config_row.payload = {
                "settings": {
                    "storage": {
                        "root_path": "relative/storage",
                        "repo_path": "/absolute/repo/path",
                    }
                }
            }
            mock_session.query.return_value.filter.return_value.first.return_value = config_row

            svc = GitServiceBase(base_path="git_repos")
            assert svc.base_path is not None
            assert "/absolute/repo/path" in svc.base_path

    def test_absolute_root_with_relative_repo(self):
        """root_path absolute, repo_path relative → combined path."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch("src.services.git._base.SessionLocal") as mock_sl_cls:
            mock_session = MagicMock()
            mock_sl_cls.return_value = mock_session
            config_row = MagicMock()
            config_row.payload = {
                "settings": {
                    "storage": {
                        "root_path": "/absolute/storage",
                        "repo_path": "repositories",
                    }
                }
            }
            mock_session.query.return_value.filter.return_value.first.return_value = config_row

            svc = GitServiceBase(base_path="git_repos")
            assert svc.base_path is not None
            assert "repositories" in svc.base_path

    def test_config_with_empty_payload_storage(self):
        """Payload exists but storage settings empty → fallback."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch("src.services.git._base.SessionLocal") as mock_sl_cls:
            mock_session = MagicMock()
            mock_sl_cls.return_value = mock_session
            config_row = MagicMock()
            config_row.payload = {"settings": {"storage": {}}}
            mock_session.query.return_value.filter.return_value.first.return_value = config_row

            svc = GitServiceBase(base_path="git_repos")
            assert svc.base_path is not None

    def test_no_config_row(self):
        """No config row in DB → fallback path."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch("src.services.git._base.SessionLocal") as mock_sl_cls:
            mock_session = MagicMock()
            mock_sl_cls.return_value = mock_session
            mock_session.query.return_value.filter.return_value.first.return_value = None
            svc = GitServiceBase(base_path="git_repos")
            assert svc.base_path is not None

    def test_custom_base_path_uses_fallback(self):
        """Custom base_path (not 'git_repos') → returns fallback_path directly, no DB query."""
        # Don't patch _resolve_base_path — test it directly
        # Patch _ensure_base_path_exists so it doesn't create directories
        actual_msg = "git_repos"  # We need to verify no DB call, so we pass a custom path
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch("src.services.git._base.SessionLocal") as mock_sl:
            # For custom base path, _resolve_base_path returns fallback without DB query
            svc = GitServiceBase(base_path="/custom/path")
            # Should use fallback, not DB
            assert "custom" in svc.base_path
            mock_sl.assert_not_called()  # No DB query for custom paths


class TestGetRepoPathEdge:
    """_get_repo_path — DB migration path and edge cases."""

    @pytest.mark.asyncio
    async def test_db_path_with_migration_condition(self):
        """DB path starts with legacy base and base_path != legacy → migrate."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'):
            svc = GitServiceBase(base_path="git_repos")
            svc.base_path = "/tmp/new-base"
            svc.legacy_base_path = "/tmp/legacy"
            svc._uses_default_base_path = True

            with patch("src.services.git._base.SessionLocal") as mock_sl_cls:
                mock_db = MagicMock()
                mock_sl_cls.return_value = mock_db
                mock_db_repo = MagicMock()
                mock_db_repo.local_path = "/tmp/legacy/repo"
                mock_db.query.return_value.filter.return_value.first.return_value = mock_db_repo
                with patch("os.path.exists", return_value=True), \
                     patch("os.path.abspath", side_effect=lambda x: x), \
                     patch.object(svc, '_migrate_repo_directory', new_callable=AsyncMock, return_value="/tmp/new-base/dashboard"):
                    result = await svc._get_repo_path(1)
                    assert result == "/tmp/new-base/dashboard"

    @pytest.mark.asyncio
    async def test_db_path_not_starting_with_legacy(self):
        """DB path exists but does not start with legacy_base → returned directly."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'):
            svc = GitServiceBase(base_path="git_repos")
            svc.base_path = "/tmp/new-base"
            svc.legacy_base_path = "/tmp/legacy"
            svc._uses_default_base_path = True

            with patch("src.services.git._base.SessionLocal") as mock_sl_cls:
                mock_db = MagicMock()
                mock_sl_cls.return_value = mock_db
                mock_db_repo = MagicMock()
                mock_db_repo.local_path = "/tmp/custom/repo"
                mock_db.query.return_value.filter.return_value.first.return_value = mock_db_repo
                with patch("os.path.exists", return_value=True), \
                     patch("os.path.abspath", side_effect=lambda x: x):
                    result = await svc._get_repo_path(1, "my-key")
                    assert result == "/tmp/custom/repo"

    @pytest.mark.asyncio
    async def test_legacy_path_does_not_exist_no_migration(self):
        """Legacy ID path does not exist → no migration, target path returned."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'):
            svc = GitServiceBase(base_path="git_repos")
            svc.base_path = "/tmp/new-base"
            svc.legacy_base_path = "/tmp/legacy"
            svc._uses_default_base_path = True

            with patch("src.services.git._base.SessionLocal") as mock_sl_cls:
                mock_db = MagicMock()
                mock_sl_cls.return_value = mock_db
                mock_db.query.return_value.filter.return_value.first.return_value = None
                with patch("os.path.exists", side_effect=lambda p: False), \
                     patch.object(svc, '_update_repo_local_path') as mock_update:
                    result = await svc._get_repo_path(1, "my-key")
                    assert result == "/tmp/new-base/my-key"
                    # target_path doesn't exist, legacy_id_path doesn't exist
                    # → returns target_path without update
                    mock_update.assert_not_called()

    @pytest.mark.asyncio
    async def test_legacy_path_exists_new_base_differs(self):
        """Legacy path exists, target doesn't → migrate."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'):
            svc = GitServiceBase(base_path="git_repos")
            svc.base_path = "/tmp/new-base"
            svc.legacy_base_path = "/tmp/legacy"
            svc._uses_default_base_path = True

            # No DB record
            with patch("src.services.git._base.SessionLocal") as mock_sl_cls:
                mock_db = MagicMock()
                mock_sl_cls.return_value = mock_db
                mock_db.query.return_value.filter.return_value.first.return_value = None
                # legacy path exists, target path does not exist
                legacy_path = "/tmp/legacy/1"
                target_path = "/tmp/new-base/dashboard"
                with patch("os.path.exists", side_effect=lambda p: p == legacy_path), \
                     patch.object(svc, '_migrate_repo_directory', new_callable=AsyncMock, return_value=target_path):
                    result = await svc._get_repo_path(1)
                    assert result == target_path


class TestInitRepoEdgePaths:
    """init_repo — additional edge paths."""

    @pytest.mark.asyncio
    async def test_existing_repo_opens_successfully(self):
        """Existing repo path → opens directly."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            mock_repo = MagicMock()
            with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo"), \
                 patch("os.path.exists", return_value=True), \
                 patch("src.services.git._base.run_blocking", return_value=mock_repo) as mock_blocking, \
                 patch.object(svc, "_ensure_gitflow_branches", create=True), \
                 patch.object(svc, '_locked', return_value=contextlib.nullcontext()):
                mock_blocking.side_effect = [None, mock_repo]  # mkdir then Repo
                result = await svc.init_repo(1, "https://remote.com/repo.git", "pat")
                assert result is not None

    @pytest.mark.asyncio
    async def test_stale_path_unlink_exception(self):
        """stale_path.unlink fails → caught, clone proceeds."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            mock_repo = MagicMock()
            with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo"), \
                 patch("os.path.exists", return_value=True), \
                 patch("src.services.git._base.run_blocking") as mock_blocking, \
                 patch.object(svc, "_clone_with_auth", new_callable=AsyncMock, return_value=mock_repo), \
                 patch.object(svc, "_ensure_gitflow_branches", create=True), \
                 patch.object(svc, '_locked', return_value=contextlib.nullcontext()), \
                 patch("pathlib.Path.exists", return_value=True):

                # mkdir → InvalidGitRepositoryError → rmtree → unlink fails → clone
                mock_blocking.side_effect = [
                    None,  # Path(repo_path).parent.mkdir
                    InvalidGitRepositoryError("invalid"),  # Repo() call
                    None,  # shutil.rmtree
                    Exception("unlink failed"),  # stale_path.unlink
                ]
                result = await svc.init_repo(1, "https://remote.com/repo.git", "pat")
                assert result is not None


class TestMigrateRepoDirectoryOSError:
    """_migrate_repo_directory — OSError → shutil.move fallback."""

    @pytest.mark.asyncio
    async def test_os_replace_oserror_falls_back(self):
        """os.replace raises OSError → shutil.move used."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            with patch("os.path.exists", return_value=False), \
                 patch.object(svc, '_update_repo_local_path'), \
                 patch("src.services.git._base.run_blocking", new_callable=AsyncMock) as mock_blocking:
                # mkdir succeeds, os.replace fails with OSError, shutil.move succeeds
                mock_blocking.side_effect = [None, OSError("rename failed"), None]
                result = await svc._migrate_repo_directory(1, "/source", "/target")
                assert result == "/target"
# #endregion Test.Git.Base.Edge
