# #region Test.Git.Sync.Edge [C:3] [TYPE Module] [SEMANTICS test, git, sync, push, pull, coverage]
# @BRIEF Edge case tests for GitServiceSyncMixin — push/pull paths.
# @RELATION BINDS_TO -> [GitServiceSyncMixin]

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

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

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


class TestPushChanges:
    @pytest.mark.asyncio
    async def test_no_branches(self):
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        repo = MagicMock()
        repo.heads = []
        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo):
            await mixin.push_changes(1)  # should not raise

    @pytest.mark.asyncio
    async def test_no_origin_remote(self):
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        repo = MagicMock()
        repo.heads = [MagicMock()]
        repo.remote.side_effect = ValueError("no origin")
        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo):
            with pytest.raises(HTTPException, match="origin"):
                await mixin.push_changes(1)

    @pytest.mark.asyncio
    async def test_no_tracking_branch_sets_upstream(self):
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        mixin._align_origin_host_with_config = MagicMock(return_value=None)
        repo = MagicMock()
        repo.heads = [MagicMock()]
        origin = MagicMock()
        repo.remote.return_value = origin
        current_branch = MagicMock()
        current_branch.name = "feature"
        current_branch.tracking_branch.return_value = None
        repo.active_branch = current_branch

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo):
            with patch("src.services.git._sync.SessionLocal") as mock_sl_cls:
                mock_db = MagicMock()
                mock_sl_cls.return_value = mock_db
                mock_db_repo = MagicMock()
                mock_db_repo.remote_url = "https://git.com/org/repo.git"
                mock_db_repo.config_id = 1
                mock_db.query.return_value.filter.return_value.first.return_value = mock_db_repo
                mock_db_config = MagicMock()
                mock_db_config.url = "https://git.com"
                mock_db.query.return_value.filter.return_value.first.return_value = mock_db_config

                await mixin.push_changes(1)
                repo.git.push.assert_called_with("--set-upstream", "origin", "feature:feature")

    @pytest.mark.asyncio
    async def test_tracking_branch_push(self):
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        mixin._align_origin_host_with_config = MagicMock(return_value=None)
        repo = MagicMock()
        repo.heads = [MagicMock()]
        origin = MagicMock()
        repo.remote.return_value = origin
        current_branch = MagicMock()
        current_branch.name = "main"
        current_branch.tracking_branch.return_value = "origin/main"
        repo.active_branch = current_branch

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo):
            with patch("src.services.git._sync.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  # no db_repo

                await mixin.push_changes(1)
                origin.push.assert_called_once()

    @pytest.mark.asyncio
    async def test_push_git_command_error_nonfastforward(self):
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        mixin._align_origin_host_with_config = MagicMock(return_value=None)
        repo = MagicMock()
        repo.heads = [MagicMock()]
        origin = MagicMock()
        repo.remote.return_value = origin
        current_branch = MagicMock()
        current_branch.name = "main"
        current_branch.tracking_branch.return_value = None
        repo.active_branch = current_branch
        repo.git.push.side_effect = GitCommandError("push", "non-fast-forward")

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo):
            with patch("src.services.git._sync.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 pytest.raises(HTTPException, match="conflict|rejected|409"):
                    await mixin.push_changes(1)

    @pytest.mark.asyncio
    async def test_push_generic_exception(self):
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        mixin._align_origin_host_with_config = MagicMock(return_value=None)
        repo = MagicMock()
        repo.heads = [MagicMock()]
        origin = MagicMock()
        repo.remote.return_value = origin
        current_branch = MagicMock()
        current_branch.name = "main"
        current_branch.tracking_branch.return_value = None
        repo.active_branch = current_branch
        repo.git.push.side_effect = GitCommandError("push", "some error")

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo):
            with patch("src.services.git._sync.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 pytest.raises(HTTPException, match="500"):
                    await mixin.push_changes(1)


class TestPullChanges:
    @pytest.mark.asyncio
    async def test_unfinished_merge_detected(self):
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        repo = MagicMock()
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        repo.active_branch.name = "main"

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
             patch("os.path.exists", return_value=True), \
             patch.object(mixin, '_build_unfinished_merge_payload', return_value={
                 "repository_path": "/tmp/repo",
                 "git_dir": "/tmp/repo/.git",
                 "current_branch": "main",
                 "merge_head": "abc123",
                 "merge_message_preview": "Merge branch",
                 "conflicts_count": 2,
             }, create=True):
            with pytest.raises(HTTPException, match="409"):
                await mixin.pull_changes(1)

    @pytest.mark.asyncio
    async def test_remote_branch_not_found(self):
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        repo = MagicMock()
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        current_branch = MagicMock()
        current_branch.name = "feature"
        repo.active_branch = current_branch
        origin = MagicMock()
        repo.remote.return_value = origin
        repo.refs = []

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
             patch("os.path.exists", return_value=False):
            with pytest.raises(HTTPException, match="409"):
                await mixin.pull_changes(1)

    @pytest.mark.asyncio
    async def test_conflict_during_pull(self):
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        repo = MagicMock()
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        current_branch = MagicMock()
        current_branch.name = "main"
        repo.active_branch = current_branch
        origin = MagicMock()
        repo.remote.return_value = origin
        # Simulate remote branch existence
        ref = MagicMock()
        ref.name = "origin/main"
        repo.refs = [ref]
        repo.git.pull.side_effect = GitCommandError("pull", "conflict")

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
             patch("os.path.exists", return_value=False):
            with pytest.raises(HTTPException, match="409"):
                await mixin.pull_changes(1)

    @pytest.mark.asyncio
    async def test_origin_not_found(self):
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        repo = MagicMock()
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        repo.active_branch.name = "main"
        repo.remote.side_effect = ValueError

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
             patch("os.path.exists", return_value=False):
            with pytest.raises(HTTPException, match="origin"):
                await mixin.pull_changes(1)

    @pytest.mark.asyncio
    async def test_generic_pull_error(self):
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        repo = MagicMock()
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        current_branch = MagicMock()
        current_branch.name = "main"
        repo.active_branch = current_branch
        origin = MagicMock()
        repo.remote.return_value = origin
        ref = MagicMock()
        ref.name = "origin/main"
        repo.refs = [ref]
        repo.git.pull.side_effect = Exception("unknown error")

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
             patch("os.path.exists", return_value=False):
            with pytest.raises(HTTPException, match="500"):
                await mixin.pull_changes(1)


class TestPushChangesEdge:
    """push_changes — diagnostic and origin.urls exception paths."""

    @pytest.mark.asyncio
    async def test_origin_urls_exception(self):
        """list(origin.urls) raises → caught, handled."""
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        mixin._align_origin_host_with_config = MagicMock(return_value=None)
        repo = MagicMock()
        repo.heads = [MagicMock()]
        origin = MagicMock()
        # list(origin.urls) raises exception
        type(origin).urls = PropertyMock(side_effect=Exception("urls error"))
        repo.remote.return_value = origin
        branch = MagicMock()
        branch.name = "main"
        branch.tracking_branch.return_value = None
        repo.active_branch = branch

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
             patch("src.services.git._sync.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
            await mixin.push_changes(1)
            repo.git.push.assert_called_with("--set-upstream", "origin", "main:main")

    @pytest.mark.asyncio
    async def test_tracking_branch_exception(self):
        """tracking_branch() raises → handled as None."""
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        mixin._align_origin_host_with_config = MagicMock(return_value=None)
        repo = MagicMock()
        repo.heads = [MagicMock()]
        origin = MagicMock()
        origin.urls = ["https://git.com/org/repo.git"]
        repo.remote.return_value = origin
        branch = MagicMock()
        branch.name = "main"
        branch.tracking_branch.side_effect = Exception("no tracking")
        repo.active_branch = branch

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
             patch("src.services.git._sync.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
            await mixin.push_changes(1)
            repo.git.push.assert_called_with("--set-upstream", "origin", "main:main")

    @pytest.mark.asyncio
    async def test_push_generic_git_command_error_no_match(self):
        """GitCommandError that is not non-fast-forward → HTTPException 500."""
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        mixin._align_origin_host_with_config = MagicMock(return_value=None)
        repo = MagicMock()
        repo.heads = [MagicMock()]
        origin = MagicMock()
        origin.urls = ["https://git.com/org/repo.git"]
        repo.remote.return_value = origin
        branch = MagicMock()
        branch.name = "main"
        branch.tracking_branch.return_value = None
        repo.active_branch = branch
        repo.git.push.side_effect = GitCommandError("push", "unknown git error")

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
             patch("src.services.git._sync.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 pytest.raises(HTTPException) as exc_info:
                await mixin.push_changes(1)
            assert exc_info.value.status_code == 500

    @pytest.mark.asyncio
    async def test_push_push_info_raising_exception(self):
        """origin.push raises generic Exception → HTTPException 500."""
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        mixin._align_origin_host_with_config = MagicMock(return_value=None)
        repo = MagicMock()
        repo.heads = [MagicMock()]
        origin = MagicMock()
        repo.remote.return_value = origin
        branch = MagicMock()
        branch.name = "main"
        branch.tracking_branch.return_value = None
        repo.active_branch = branch
        # Force the else branch (tracking_branch is None → repo.git.push)
        repo.git.push.return_value = None  # won't be called because tracking_branch is None
        repo.git.push.side_effect = Exception("unexpected failure")

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
             patch("src.services.git._sync.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 pytest.raises(HTTPException) as exc_info:
                await mixin.push_changes(1)
            assert exc_info.value.status_code == 500

    @pytest.mark.asyncio
    async def test_push_with_origin_push_generic_error(self):
        """origin.push() generic exception → HTTPException 500."""
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        mixin._align_origin_host_with_config = MagicMock(return_value=None)
        repo = MagicMock()
        repo.heads = [MagicMock()]
        origin = MagicMock()
        origin.urls = ["https://git.com/org/repo.git"]
        repo.remote.return_value = origin
        branch = MagicMock()
        branch.name = "main"
        branch.tracking_branch.return_value = MagicMock()  # has tracking → uses origin.push
        repo.active_branch = branch
        origin.push.side_effect = Exception("generic push error")

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
             patch("src.services.git._sync.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 pytest.raises(HTTPException) as exc_info:
                await mixin.push_changes(1)
            assert exc_info.value.status_code == 500


class TestPullChangesEdge:
    """pull_changes — remaining edge paths."""

    @pytest.mark.asyncio
    async def test_origin_urls_exception_on_pull(self):
        """list(origin.urls) raises → caught, pull continues."""
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        repo = MagicMock()
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        repo.active_branch.name = "main"
        origin = MagicMock()
        # list(origin.urls) raises
        type(origin).urls = PropertyMock(side_effect=Exception("urls error"))
        repo.remote.return_value = origin
        ref = MagicMock()
        ref.name = "origin/main"
        repo.refs = [ref]

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
             patch("os.path.exists", return_value=False):
            await mixin.pull_changes(1)
            origin.fetch.assert_called_once_with(prune=True)
            repo.git.pull.assert_called_with("--no-rebase", "origin", "main")

    @pytest.mark.asyncio
    async def test_git_command_error_non_conflict(self):
        """GitCommandError that is not conflict → HTTPException 500."""
        from src.services.git._sync import GitServiceSyncMixin
        mixin = GitServiceSyncMixin()
        mixin._locked = lambda x: contextlib.nullcontext()
        repo = MagicMock()
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        repo.active_branch.name = "main"
        origin = MagicMock()
        origin.urls = ["https://git.com/org/repo.git"]
        repo.remote.return_value = origin
        ref = MagicMock()
        ref.name = "origin/main"
        repo.refs = [ref]
        repo.git.pull.side_effect = GitCommandError("pull", "fatal: not a git repository")

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
             patch("os.path.exists", return_value=False):
            with pytest.raises(HTTPException) as exc_info:
                await mixin.pull_changes(1)
            assert exc_info.value.status_code == 500
# #endregion Test.Git.Sync.Edge
