# #region Test.Git.Base [C:4] [TYPE Module] [SEMANTICS test,git,base,init,repo,lock,identity]
# @BRIEF Tests for GitServiceBase — initialization, path resolution, repo lifecycle (init/delete/get), identity configuration, concurrent locking, HTTP client.
# @RELATION BINDS_TO -> [GitServiceBase]

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

import pytest
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock

from fastapi import HTTPException
from git.exc import InvalidGitRepositoryError, NoSuchPathError
from git import Repo

from src.services.git._base import GitServiceBase


class TestableGitServiceBase:
    """Concrete test wrapper - passes through to GitServiceBase static methods."""
    pass


class TestGitServiceBase:
    """Test GitServiceBase using the actual class with mocks."""

    # #region test_close_idempotent [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_close_idempotent(self):
        """Multiple close calls only close once."""
        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._http_client = AsyncMock()
            await svc.close()
            svc._http_client.aclose.assert_called_once()
            await svc.close()
            svc._http_client.aclose.assert_called_once()
            assert svc._closed is True
    # #endregion test_close_idempotent

    # #region test_closed_service_raises [C:2] [TYPE Function]
    def test_closed_service_raises(self):
        """Closed service → RuntimeError on _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
    # #endregion test_closed_service_raises

    # #region test_get_lock_creates_new [C:2] [TYPE Function]
    def test_get_lock_creates_new(self):
        """New dashboard_id creates new lock."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            lock = svc._get_lock(42)
            assert lock is not None
            assert 42 in svc._lock_registry
            assert svc._get_lock(42) is lock
    # #endregion test_get_lock_creates_new

    # #region test_locked_acquire_release [C:2] [TYPE Function]
    def test_locked_acquire_release(self):
        """Lock is acquired on enter, released on exit."""
        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_lock = MagicMock()
            svc._lock_registry[1] = mock_lock
            with svc._locked(1):
                mock_lock.acquire.assert_called_once()
            mock_lock.release.assert_called_once()
    # #endregion test_locked_acquire_release

    # #region test_clone_with_auth_http [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_clone_with_auth_http(self):
        """HTTP URL with PAT → cloned with auth URL, then strips."""
        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()
            # Directly patch the imported name in the module dict to avoid
            # AsyncMock interaction issues when running in full suite.
            import src.services.git._base as gb_mod
            orig_blocking = gb_mod.run_blocking
            try:
                gb_mod.run_blocking = AsyncMock(return_value=mock_repo)
                result = await svc._clone_with_auth("https://gitea.com/org/repo.git", "/tmp/repo", "my-pat")
                assert result == mock_repo
            finally:
                gb_mod.run_blocking = orig_blocking
    # #endregion test_clone_with_auth_http

    # #region test_clone_with_auth_no_pat [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_clone_with_auth_no_pat(self):
        """No PAT → clones without auth modifications."""
        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()
            import src.services.git._base as gb_mod
            orig_blocking = gb_mod.run_blocking
            try:
                gb_mod.run_blocking = AsyncMock(return_value=mock_repo)
                result = await svc._clone_with_auth("https://gitea.com/org/repo.git", "/tmp/repo", "")
                assert result == mock_repo
            finally:
                gb_mod.run_blocking = orig_blocking
    # #endregion test_clone_with_auth_no_pat


# ── _ensure_base_path_exists ──

class TestEnsureBasePathExists:
    """_ensure_base_path_exists — directory creation."""

    # #region test_ensure_base_creates [C:2] [TYPE Function]
    def test_ensure_base_creates(self):
        """Non-existent path → creates directory."""
        with patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            svc.base_path = "/tmp/test"
            with patch.object(Path, "exists", return_value=False), \
                 patch.object(Path, "mkdir") as mock_mkdir:
                svc._ensure_base_path_exists()
                mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
    # #endregion test_ensure_base_creates

    # #region test_ensure_base_not_dir_raises [C:2] [TYPE Function]
    def test_ensure_base_not_dir_raises(self):
        """Exists but not dir → ValueError."""
        with patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            svc.base_path = "/tmp/test"
            with patch.object(Path, "exists", return_value=True), \
                 patch.object(Path, "is_dir", return_value=False):
                with pytest.raises(ValueError, match="not a directory"):
                    svc._ensure_base_path_exists()
    # #endregion test_ensure_base_not_dir_raises

    # #region test_ensure_base_permission_error [C:2] [TYPE Function]
    def test_ensure_base_permission_error(self):
        """PermissionError → ValueError."""
        with patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            svc.base_path = "/tmp/test"
            with patch.object(Path, "exists", return_value=False), \
                 patch.object(Path, "mkdir", side_effect=PermissionError("denied")):
                with pytest.raises(ValueError, match="Cannot create"):
                    svc._ensure_base_path_exists()
    # #endregion test_ensure_base_permission_error


# ── _normalize_repo_key ──

class TestNormalizeRepoKey:
    """_normalize_repo_key — sanitize to filesystem-safe name."""

    # #region test_normalize_basic [C:2] [TYPE Function]
    def test_normalize_basic(self):
        """Normal key returned unchanged."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            assert svc._normalize_repo_key("my-repo") == "my-repo"
    # #endregion test_normalize_basic

    # #region test_normalize_special_chars [C:2] [TYPE Function]
    def test_normalize_special_chars(self):
        """Special chars replaced with hyphens."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            assert svc._normalize_repo_key("My Repo!@#$") == "my-repo"
    # #endregion test_normalize_special_chars

    # #region test_normalize_empty [C:2] [TYPE Function]
    def test_normalize_empty(self):
        """Empty key → 'dashboard'."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            assert svc._normalize_repo_key("") == "dashboard"
            assert svc._normalize_repo_key(None) == "dashboard"
    # #endregion test_normalize_empty


# ── _update_repo_local_path ──

class TestUpdateRepoLocalPath:
    """_update_repo_local_path — persist local path in DB."""

    # #region test_update_path_existing [C:2] [TYPE Function]
    def test_update_path_existing(self):
        """Existing DB record → local_path updated."""
        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_session = MagicMock()
            mock_session.query.return_value.filter.return_value.first.return_value = MagicMock(local_path="")
            # Patch SessionLocal at the module where it's imported (_base.py),
            # using the actual import reference to ensure it works even in full suite
            import src.services.git._base as git_base_mod
            original_session_local = git_base_mod.SessionLocal
            try:
                git_base_mod.SessionLocal = MagicMock(return_value=mock_session)
                svc._update_repo_local_path(1, "/tmp/repo")
                assert mock_session.commit.called
            finally:
                git_base_mod.SessionLocal = original_session_local
    # #endregion test_update_path_existing

    # #region test_update_path_no_record [C:2] [TYPE Function]
    def test_update_path_no_record(self):
        """No DB record → no error."""
        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("src.services.git._base.SessionLocal") as mock_session_cls:
                mock_session = MagicMock()
                mock_session_cls.return_value = mock_session
                mock_session.query.return_value.filter.return_value.first.return_value = None
                svc._update_repo_local_path(1, "/tmp/repo")
    # #endregion test_update_path_no_record

    # #region test_update_path_db_error [C:2] [TYPE Function]
    def test_update_path_db_error(self):
        """DB error → caught, no exception."""
        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("src.services.git._base.SessionLocal", side_effect=Exception("db error")):
                svc._update_repo_local_path(1, "/tmp/repo")
    # #endregion test_update_path_db_error


# ── _migrate_repo_directory ──

class TestMigrateRepoDirectory:
    """_migrate_repo_directory — move repo to new path."""

    # #region test_migrate_same_path [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_migrate_same_path(self):
        """Source == target → returns source unchanged."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
            svc = GitServiceBase(base_path="/tmp/test")
            result = await svc._migrate_repo_directory(1, "/same/path", "/same/path")
            assert result == "/same/path"
    # #endregion test_migrate_same_path

    # #region test_migrate_target_exists [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_migrate_target_exists(self):
        """Target exists → returns 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"
    # #endregion test_migrate_target_exists


# ── _get_repo_path ──

class TestGetRepoPath:
    """_get_repo_path — resolve local filesystem path."""

    # #region test_get_repo_path_default [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_repo_path_default(self):
        """Default path → base_path + normalized key."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'):
            with patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/base"):
                svc = GitServiceBase(base_path="/tmp/base")
                svc.base_path = "/tmp/base"
                svc._uses_default_base_path = False
                result = await svc._get_repo_path(1, "my-key")
                assert result == "/tmp/base/my-key"
    # #endregion test_get_repo_path_default

    # #region test_get_repo_path_none_id [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_repo_path_none_id(self):
        """None dashboard_id → ValueError."""
        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 pytest.raises(ValueError, match="cannot be None"):
                await svc._get_repo_path(None)
    # #endregion test_get_repo_path_none_id


# ── init_repo ──

class TestInitRepo:
    """init_repo — initialize or clone repository."""

    # #region test_init_repo_clone [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_init_repo_clone(self):
        """New repo → cloned."""
        with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
             patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/base"):
            svc = GitServiceBase(base_path="/tmp/base")
            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=False), \
                 patch("src.services.git._base.run_blocking", new_callable=AsyncMock), \
                 patch.object(svc, "_clone_with_auth", new_callable=AsyncMock) as mock_clone, \
                 patch.object(svc, "_ensure_gitflow_branches", create=True) as mock_flow:
                mock_clone.return_value = MagicMock()
                result = await svc.init_repo(1, "https://remote.com/repo.git", "pat")
                mock_clone.assert_called_once()
                mock_flow.assert_called_once()
                assert result is not None
    # #endregion test_init_repo_clone


# ── get_repo ──

class TestGetRepo:
    """get_repo — open existing repository."""

    # #region test_get_repo_not_found [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_repo_not_found(self):
        """Repository doesn't exist → HTTPException 404."""
        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/nonexistent"), \
                 patch("os.path.exists", return_value=False):
                with pytest.raises(HTTPException) as exc_info:
                    await svc.get_repo(1)
                assert exc_info.value.status_code == 404
    # #endregion test_get_repo_not_found

    # #region test_get_repo_open_error [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_get_repo_open_error(self):
        """Repo path exists but cannot be opened → HTTPException 500."""
        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", side_effect=Exception("corrupt")):
                with pytest.raises(HTTPException) as exc_info:
                    await svc.get_repo(1)
                assert exc_info.value.status_code == 500
    # #endregion test_get_repo_open_error


# ── configure_identity ──

class TestConfigureIdentity:
    """configure_identity — set git user/email."""

    # #region test_configure_identity_success [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_configure_identity_success(self):
        """Valid username/email → config_writer called."""
        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()
            mock_config_writer = MagicMock()
            mock_repo.config_writer.return_value.__enter__.return_value = mock_config_writer
            with patch.object(svc, "get_repo", new_callable=AsyncMock, return_value=mock_repo):
                await svc.configure_identity(1, "Alice", "alice@example.com")
                mock_config_writer.set_value.assert_any_call("user", "name", "Alice")
                mock_config_writer.set_value.assert_any_call("user", "email", "alice@example.com")
    # #endregion test_configure_identity_success

    # #region test_configure_identity_empty [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_configure_identity_empty(self):
        """Empty username/email → no-op."""
        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", new_callable=AsyncMock) as mock_get:
                await svc.configure_identity(1, "", "")
                mock_get.assert_not_called()
                await svc.configure_identity(1, "user", "")
                mock_get.assert_not_called()
                await svc.configure_identity(1, "", "email@c.com")
                mock_get.assert_not_called()
    # #endregion test_configure_identity_empty

    # #region test_configure_identity_error [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_configure_identity_error(self):
        """Config writer error → HTTPException 500."""
        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()
            mock_repo.config_writer.side_effect = Exception("config error")
            with patch.object(svc, "get_repo", new_callable=AsyncMock, return_value=mock_repo):
                with pytest.raises(HTTPException, match="identity"):
                    await svc.configure_identity(1, "Alice", "alice@c.com")
    # #endregion test_configure_identity_error


# ── delete_repo ──

class TestDeleteRepo:
    """delete_repo — remove repository."""

    # #region test_delete_repo_not_found [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_delete_repo_not_found(self):
        """No local repo and no DB record → HTTPException 404."""
        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/nonexistent"), \
                 patch("os.path.exists", return_value=False), \
                 patch("src.services.git._base.SessionLocal") as mock_session_cls:
                mock_session = MagicMock()
                mock_session_cls.return_value = mock_session
                mock_session.query.return_value.filter.return_value.first.return_value = None
                with pytest.raises(HTTPException, match="not found"):
                    await svc.delete_repo(1)
    # #endregion test_delete_repo_not_found

    # #region test_delete_repo_success [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_delete_repo_success(self):
        """Local repo exists → deleted, DB record removed."""
        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_session = MagicMock()
            mock_db_repo = MagicMock()
            mock_session.query.return_value.filter.return_value.first.return_value = mock_db_repo
            import src.services.git._base as gb_mod
            orig_sl = gb_mod.SessionLocal
            orig_blocking = gb_mod.run_blocking
            try:
                gb_mod.SessionLocal = MagicMock(return_value=mock_session)
                gb_mod.run_blocking = AsyncMock()
                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):
                    await svc.delete_repo(1)
                    mock_session.delete.assert_called_with(mock_db_repo)
                    mock_session.commit.assert_called_once()
            finally:
                gb_mod.SessionLocal = orig_sl
                gb_mod.run_blocking = orig_blocking
    # #endregion test_delete_repo_success

    # #region test_delete_repo_db_error [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_delete_repo_db_error(self):
        """DB error → rollback, HTTPException 500."""
        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_session = MagicMock()
            mock_db_repo = MagicMock()
            mock_session.query.return_value.filter.return_value.first.return_value = mock_db_repo
            mock_session.delete.side_effect = Exception("db error")
            import src.services.git._base as git_base_mod
            original_sl = git_base_mod.SessionLocal
            try:
                git_base_mod.SessionLocal = MagicMock(return_value=mock_session)
                with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo"), \
                     patch("os.path.exists", return_value=False):
                    with pytest.raises(HTTPException) as exc_info:
                        await svc.delete_repo(1)
                    assert exc_info.value.status_code == 500
                    mock_session.rollback.assert_called_once()
            finally:
                git_base_mod.SessionLocal = original_sl
    # #endregion test_delete_repo_db_error
