# #region Test.Git.Base.Coverage2 [C:3] [TYPE Module] [SEMANTICS test, git, base, coverage, resolve, migrate]
# @BRIEF Additional coverage for GitServiceBase uncovered lines:
#   _resolve_base_path (lines 139, 151), _migrate_repo_directory (lines 212-221),
#   _get_repo_path (lines 258, 264, 266), delete_repo (lines 320, 334).
# @RELATION BINDS_TO -> [GitServiceBase]

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

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

import pytest

from src.services.git._base import GitServiceBase


class TestResolveBasePathCoverage2:
    """Cover _resolve_base_path lines 139 and 151."""

    # Line 139: if base_path != "git_repos" → return fallback_path
    # To hit this we actually call the real _resolve_base_path via __init__
    # with a non-git_repos base_path.
    def test_line139_custom_base_path_returns_fallback(self):
        """base_path != 'git_repos' → returns fallback_path (line 139)."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'):
            svc = GitServiceBase(base_path="/custom/path")
            assert "custom" in svc.base_path

    # Line 151: if not root_path → return fallback_path
    # When storage config has empty root_path
    def test_line151_empty_root_path_returns_fallback(self):
        """storage root_path is empty → falls back to default (line 151)."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch("src.services.git._base.SessionLocal") as mock_sl:
            mock_db = MagicMock()
            mock_sl.return_value = mock_db
            config = MagicMock()
            config.payload = {
                "settings": {
                    "storage": {
                        "root_path": "",
                        "repo_path": "",
                    }
                }
            }
            mock_db.query.return_value.filter.return_value.first.return_value = config
            svc = GitServiceBase(base_path="git_repos")
            # With empty root_path, line 150 is True → line 151 returns fallback
            assert svc.base_path is not None


class TestMigrateRepoDirectoryCoverage:
    """Cover _migrate_repo_directory lines 212-221."""

    @pytest.mark.asyncio
    async def test_migrate_full_path_mkdir_and_replace(self):
        """Full migration: mkdir parent, os.replace, update local path, log, return target (lines 212-221)."""
        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("os.path.abspath", side_effect=lambda x: x), \
                 patch.object(svc, '_update_repo_local_path') as mock_update, \
                 patch("src.services.git._base.run_blocking", new_callable=AsyncMock) as mock_blocking, \
                 patch("src.services.git._base.logger.info") as mock_log:
                # mkdir succeeds, os.replace succeeds
                mock_blocking.side_effect = [None, None]
                result = await svc._migrate_repo_directory(1, "/source/path", "/target/path")
                assert result == "/target/path"
                mock_update.assert_called_once_with(1, "/target/path")
                mock_log.assert_called_once()

    @pytest.mark.asyncio
    async def test_migrate_os_replace_fallback_to_move(self):
        """os.replace OSError → shutil.move fallback (line 215-216)."""
        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("os.path.abspath", side_effect=lambda x: x), \
                 patch.object(svc, '_update_repo_local_path'), \
                 patch("src.services.git._base.run_blocking", new_callable=AsyncMock) as mock_blocking:
                # mkdir succeeds, os.replace raises OSError, shutil.move succeeds
                mock_blocking.side_effect = [None, OSError("rename failed"), None]
                result = await svc._migrate_repo_directory(1, "/source/path", "/target/path")
                assert result == "/target/path"


class TestGetRepoPathCoverage2:
    """Cover _get_repo_path lines 258, 264, 266."""

    # Line 258: DB path starts with legacy and base_path != legacy → migrate
    @pytest.mark.asyncio
    async def test_line258_db_legacy_path_migration(self):
        """DB local_path under legacy base with different base_path → migrate (line 258)."""
        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) as mock_migrate:
                    mock_migrate.return_value = "/tmp/new-base/my-key"
                    result = await svc._get_repo_path(1, "my-key")
                    assert result == "/tmp/new-base/my-key"
                    mock_migrate.assert_called_once()

    # Line 264: legacy ID path exists → migrate
    # The normalized key for dashboard_id=1 is "1" (via _normalize_repo_key)
    @pytest.mark.asyncio
    async def test_line264_legacy_id_path_migration(self):
        """Legacy ID path exists, target doesn't → migrate (line 264)."""
        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.query.return_value.filter.return_value.first.return_value = None
                legacy_path = "/tmp/legacy/1"
                # normalized key for dashboard_id=1 (no repo_key) is "1"
                target_path = "/tmp/new-base/1"
                with patch("os.path.exists", side_effect=lambda p: p == legacy_path), \
                     patch.object(svc, '_migrate_repo_directory', new_callable=AsyncMock) as mock_migrate:
                    mock_migrate.return_value = target_path
                    result = await svc._get_repo_path(1)
                    assert result == target_path
                    mock_migrate.assert_called_once_with(1, legacy_path, target_path)

    # Line 266: target path exists → _update_repo_local_path called
    @pytest.mark.asyncio
    async def test_line266_target_path_exists_updates_db(self):
        """Target path exists → updates DB (line 266)."""
        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_with(1, "/tmp/base/my-key")


class TestDeleteRepoCoverage2:
    """Cover delete_repo lines 320, 334."""

    # Line 320: repo path is a file → os.remove
    @pytest.mark.asyncio
    async def test_line320_delete_file_path(self):
        """Repo path exists and is a file → os.remove (line 320)."""
        import contextlib
        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.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
                    gb_mod.run_blocking = AsyncMock()
                    await svc.delete_repo(1)
                    # os.remove should have been called (line 320)
                finally:
                    gb_mod.SessionLocal = orig_sl

    # Line 334: removed_files=True but no DB record → early return
    @pytest.mark.asyncio
    async def test_line334_removed_no_db_record_returns(self):
        """Files removed, no DB record → early return (line 334)."""
        import contextlib
        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-dir"), \
                 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
                orig_blocking = gb_mod.run_blocking
                try:
                    mock_db = MagicMock()
                    gb_mod.SessionLocal = MagicMock(return_value=mock_db)
                    mock_db.query.return_value.filter.return_value.first.return_value = None
                    gb_mod.run_blocking = AsyncMock()
                    result = await svc.delete_repo(1)
                    assert result is None
                finally:
                    gb_mod.SessionLocal = orig_sl
                    gb_mod.run_blocking = orig_blocking
# #endregion Test.Git.Base.Coverage2
