# #region Test.Git.Merge.Edge [C:4] [TYPE Module] [SEMANTICS test, git, merge, conflict, coverage]
# @BRIEF Edge case tests for GitServiceMergeMixin — merge operations, conflict resolution, abort, continue, promote.
# @RELATION BINDS_TO -> [GitServiceMergeMixin]
# @REJECTED path_traversal_via_symlink — repo_root + os.sep prefix check is sufficient for real directories

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, PropertyMock

import pytest
from fastapi import HTTPException
from git.exc import GitCommandError
from git import Repo
from git.objects.blob import Blob


@pytest.fixture
def mixin():
    from src.services.git._merge import GitServiceMergeMixin
    m = GitServiceMergeMixin()
    m._locked = lambda x: contextlib.nullcontext()  # no-op context manager
    return m


class TestReadBlobText:
    def test_none_blob(self, mixin):
        assert mixin._read_blob_text(None) == ""

    def test_decode_error(self, mixin):
        blob = MagicMock(spec=Blob)
        blob.data_stream.read.return_value = b"\xff\xfe"
        result = mixin._read_blob_text(blob)
        assert result == "" or result is not None

    def test_success(self, mixin):
        blob = MagicMock(spec=Blob)
        blob.data_stream.read.return_value = b"hello world"
        result = mixin._read_blob_text(blob)
        assert result == "hello world"


class TestGetUnmergedFilePaths:
    def test_success(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.index.unmerged_blobs.return_value = {"file1.txt": [], "file2.txt": []}
        result = mixin._get_unmerged_file_paths(repo)
        assert result == ["file1.txt", "file2.txt"]

    def test_exception(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.index.unmerged_blobs.side_effect = Exception("error")
        result = mixin._get_unmerged_file_paths(repo)
        assert result == []


class TestGetMergeStatus:
    @pytest.mark.asyncio
    async def test_no_unfinished_merge(self, mixin):
        repo = MagicMock(spec=Repo)
        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=False):
            result = await mixin.get_merge_status(1)
            assert result["has_unfinished_merge"] is False
            assert result["current_branch"] == "main"

    @pytest.mark.asyncio
    async def test_unfinished_merge(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"

        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,
             }):
            result = await mixin.get_merge_status(1)
            assert result["has_unfinished_merge"] is True
            assert result["conflicts_count"] == 2

    @pytest.mark.asyncio
    async def test_active_branch_exception(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        repo.active_branch.name = "main"
        type(repo).active_branch = PropertyMock(side_effect=Exception("no branch"))

        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
             patch("os.path.exists", return_value=False):
            result = await mixin.get_merge_status(1)
            assert result["current_branch"] == "detached_or_unknown"


class TestResolveMergeConflicts:
    def test_no_resolutions(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.working_tree_dir = "/tmp/repo"
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            result = mixin.resolve_merge_conflicts(1, [])
            assert result == []

    def test_missing_file_path(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.working_tree_dir = "/tmp/repo"
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            with pytest.raises(HTTPException, match="file_path is required"):
                mixin.resolve_merge_conflicts(1, [{"resolution": "mine"}])

    def test_unsupported_strategy(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.working_tree_dir = "/tmp/repo"
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            with pytest.raises(HTTPException, match="Unsupported resolution"):
                mixin.resolve_merge_conflicts(1, [{"file_path": "f.txt", "resolution": "invalid"}])

    def test_mine_strategy(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.working_tree_dir = "/tmp/repo"
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            result = mixin.resolve_merge_conflicts(1, [{"file_path": "f.txt", "resolution": "mine"}])
            assert result == ["f.txt"]
            repo.git.checkout.assert_called_with("--ours", "--", "f.txt")

    def test_theirs_strategy(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.working_tree_dir = "/tmp/repo"
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            result = mixin.resolve_merge_conflicts(1, [{"file_path": "f.txt", "resolution": "theirs"}])
            assert result == ["f.txt"]
            repo.git.checkout.assert_called_with("--theirs", "--", "f.txt")

    def test_manual_strategy(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.working_tree_dir = "/tmp/repo"
        with patch.object(mixin, 'get_repo', return_value=repo, create=True), \
             patch("os.path.abspath", return_value="/tmp/repo/f.txt"), \
             patch("os.makedirs"), \
             patch("builtins.open", MagicMock()):
            result = mixin.resolve_merge_conflicts(1, [
                {"file_path": "f.txt", "resolution": "manual", "content": "new content"}
            ])
            assert result == ["f.txt"]

    def test_path_traversal_blocked(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.working_tree_dir = "/tmp/repo"
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            with pytest.raises(HTTPException, match="Invalid conflict file path"):
                mixin.resolve_merge_conflicts(1, [
                    {"file_path": "../../etc/passwd", "resolution": "manual", "content": "x"}
                ])

    def test_empty_working_tree(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.working_tree_dir = None
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            # When working_tree_dir is None, abspath("") returns CWD, so function proceeds
            result = mixin.resolve_merge_conflicts(1, [{"file_path": "f.txt", "resolution": "mine"}])
            assert result == ["f.txt"]


class TestAbortMerge:
    def test_success(self, mixin):
        repo = MagicMock(spec=Repo)
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            result = mixin.abort_merge(1)
            assert result["status"] == "aborted"
            repo.git.merge.assert_called_with("--abort")

    def test_no_merge_to_abort(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.git.merge.side_effect = GitCommandError("merge", "no merge to abort")
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            result = mixin.abort_merge(1)
            assert result["status"] == "no_merge_in_progress"

    def test_other_error(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.git.merge.side_effect = GitCommandError("merge", "some other error")
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            with pytest.raises(HTTPException, match="Cannot abort"):
                mixin.abort_merge(1)


class TestContinueMerge:
    def test_unresolved_conflicts(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.index.unmerged_blobs.return_value = {"f.txt": [(1, MagicMock())]}
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            with pytest.raises(HTTPException, match="GIT_MERGE_CONFLICTS_REMAIN"):
                mixin.continue_merge(1)

    def test_commit_with_message(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.index.unmerged_blobs.return_value = {}
        with patch.object(mixin, 'get_repo', return_value=repo, create=True), \
             patch.object(mixin, '_get_unmerged_file_paths', return_value=[]):
            result = mixin.continue_merge(1, message="Resolved conflicts")
            assert result["status"] == "committed"
            repo.git.commit.assert_called_with("-m", "Resolved conflicts")

    def test_commit_no_message(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.index.unmerged_blobs.return_value = {}
        with patch.object(mixin, 'get_repo', return_value=repo, create=True), \
             patch.object(mixin, '_get_unmerged_file_paths', return_value=[]):
            result = mixin.continue_merge(1)
            assert result["status"] == "committed"
            repo.git.commit.assert_called_with("--no-edit")

    def test_nothing_to_commit(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.index.unmerged_blobs.return_value = {}
        repo.git.commit.side_effect = GitCommandError("commit", "nothing to commit")
        with patch.object(mixin, 'get_repo', return_value=repo, create=True), \
             patch.object(mixin, '_get_unmerged_file_paths', return_value=[]):
            result = mixin.continue_merge(1)
            assert result["status"] == "already_clean"

    def test_commit_error(self, mixin):
        repo = MagicMock(spec=Repo)
        repo.index.unmerged_blobs.return_value = {}
        repo.git.commit.side_effect = GitCommandError("commit", "some error")
        with patch.object(mixin, 'get_repo', return_value=repo, create=True), \
             patch.object(mixin, '_get_unmerged_file_paths', return_value=[]):
            with pytest.raises(HTTPException, match="Cannot continue"):
                mixin.continue_merge(1)

    def test_commit_hash_exception(self, mixin):
        """Exception getting commit hash → empty string."""
        repo = MagicMock(spec=Repo)
        repo.index.unmerged_blobs.return_value = {}
        repo.head.commit.hexsha = "abc123"
        def raiser():
            raise Exception("no head")
        repo.head.commit = MagicMock()
        type(repo.head.commit).hexsha = PropertyMock(side_effect=Exception("no hexsha"))
        with patch.object(mixin, 'get_repo', return_value=repo, create=True), \
             patch.object(mixin, '_get_unmerged_file_paths', return_value=[]):
            result = mixin.continue_merge(1, message="msg")
            assert result["status"] == "committed"
            assert result["commit_hash"] == ""


class TestPromoteDirectMerge:
    def test_raises_on_missing_branches(self, mixin):
        with pytest.raises(HTTPException, match="required"):
            mixin.promote_direct_merge(1, "", "target")

    def test_raises_on_same_branches(self, mixin):
        repo = MagicMock(spec=Repo)
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            with pytest.raises(HTTPException, match="must be different"):
                mixin.promote_direct_merge(1, "main", "main")

    def test_original_branch_restoration_exception(self, mixin):
        """Exception in original branch restoration does not re-raise."""
        repo = MagicMock(spec=Repo)
        repo.remote.return_value = MagicMock()
        repo.heads = [MagicMock(name="dev"), MagicMock(name="main")]
        repo.heads[0].name = "dev"
        repo.heads[1].name = "main"
        repo.refs = [MagicMock()]
        repo.refs[0].name = "origin/main"
        repo.active_branch.name = "dev"
        # checkout(target) → OK, finally restore → exception
        repo.git.checkout.side_effect = [None, Exception("restore failed")]
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            result = mixin.promote_direct_merge(1, "dev", "main")
        assert result["status"] == "merged"

    def test_source_branch_only_on_remote(self, mixin):
        """Source exists only as origin/remote → checkout creates local."""
        repo = MagicMock(spec=Repo)
        origin = MagicMock()
        repo.remote.return_value = origin
        repo.heads = [MagicMock(name="main")]
        repo.heads[0].name = "main"
        remote_ref = MagicMock()
        remote_ref.name = "origin/feature"
        repo.refs = [remote_ref]
        repo.active_branch.name = "dev"
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            result = mixin.promote_direct_merge(1, "feature", "main")
        repo.git.checkout.assert_any_call("-b", "feature", "origin/feature")
        assert result["status"] == "merged"

    def test_target_branch_only_on_remote(self, mixin):
        """Target exists only as origin/remote → checkout creates local."""
        repo = MagicMock(spec=Repo)
        origin = MagicMock()
        repo.remote.return_value = origin
        repo.heads = [MagicMock(name="feature")]
        repo.heads[0].name = "feature"
        remote_ref = MagicMock()
        remote_ref.name = "origin/main"
        repo.refs = [remote_ref]
        repo.active_branch.name = "dev"
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            result = mixin.promote_direct_merge(1, "feature", "main")
        repo.git.checkout.assert_any_call("-b", "main", "origin/main")
        assert result["status"] == "merged"

    def test_origin_pull_failure_swallowed(self, mixin):
        """origin.pull(target) failure → logged but not fatal."""
        repo = MagicMock(spec=Repo)
        origin = MagicMock()
        origin.pull.side_effect = Exception("pull failed")
        repo.remote.return_value = origin
        repo.heads = [MagicMock(name="dev"), MagicMock(name="main")]
        repo.heads[0].name = "dev"
        repo.heads[1].name = "main"
        repo.refs = [MagicMock()]
        repo.refs[0].name = "origin/main"
        repo.active_branch.name = "dev"
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            result = mixin.promote_direct_merge(1, "dev", "main")
        assert result["status"] == "merged"

    def test_merge_conflict_in_promote(self, mixin):
        """Merge conflict during promote → HTTPException 409."""
        repo = MagicMock(spec=Repo)
        origin = MagicMock()
        repo.remote.return_value = origin
        repo.heads = [MagicMock(name="dev"), MagicMock(name="main")]
        repo.heads[0].name = "dev"
        repo.heads[1].name = "main"
        repo.refs = [MagicMock()]
        repo.refs[0].name = "origin/main"
        repo.active_branch.name = "dev"
        repo.git.merge.side_effect = Exception("CONFLICT in file.txt")
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            with pytest.raises(HTTPException) as exc_info:
                mixin.promote_direct_merge(1, "dev", "main")
            assert exc_info.value.status_code == 409

    def test_promote_generic_exception(self, mixin):
        """Generic exception during promote → HTTPException 500."""
        repo = MagicMock(spec=Repo)
        origin = MagicMock()
        repo.remote.return_value = origin
        repo.heads = [MagicMock(name="dev"), MagicMock(name="main")]
        repo.heads[0].name = "dev"
        repo.heads[1].name = "main"
        repo.refs = [MagicMock()]
        repo.refs[0].name = "origin/main"
        repo.active_branch.name = "dev"
        repo.git.merge.side_effect = Exception("unexpected error")
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            with pytest.raises(HTTPException) as exc_info:
                mixin.promote_direct_merge(1, "dev", "main")
            assert exc_info.value.status_code == 500

    def test_active_branch_exception_in_promote(self, mixin):
        """active_branch.name raises → original_branch is None."""
        repo = MagicMock(spec=Repo)
        origin = MagicMock()
        repo.remote.return_value = origin
        repo.heads = [MagicMock(name="dev"), MagicMock(name="main")]
        repo.heads[0].name = "dev"
        repo.heads[1].name = "main"
        repo.refs = [MagicMock()]
        repo.refs[0].name = "origin/main"
        type(repo).active_branch = PropertyMock(side_effect=Exception("no head"))
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            result = mixin.promote_direct_merge(1, "dev", "main")
        # Should still succeed because original_branch is None (skip restore)
        assert result["status"] == "merged"

    def test_source_branch_not_found(self, mixin):
        """Source branch not in heads or remote refs → HTTPException 404."""
        repo = MagicMock(spec=Repo)
        origin = MagicMock()
        repo.remote.return_value = origin
        repo.heads = [MagicMock(name="main")]
        repo.heads[0].name = "main"
        repo.refs = [MagicMock()]
        repo.refs[0].name = "origin/main"
        repo.active_branch.name = "dev"
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            with pytest.raises(HTTPException, match="not found"):
                mixin.promote_direct_merge(1, "nonexistent", "main")


class TestBuildUnfinishedMergePayload:
    """_build_unfinished_merge_payload — edge paths."""

    def test_merge_head_read_exception(self, mixin):
        """Path.read_text on MERGE_HEAD fails → merge_head_value = '<unreadable>'."""
        repo = MagicMock(spec=Repo)
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        repo.active_branch.name = "dev"
        repo.index.unmerged_blobs.return_value = {}
        with patch("os.path.join", return_value="/tmp/repo/.git/MERGE_HEAD"), \
             patch("pathlib.Path.read_text", side_effect=Exception("permission denied")):
            payload = mixin._build_unfinished_merge_payload(repo)
            assert payload["merge_head"] == "<unreadable>"

    def test_merge_msg_read_exception(self, mixin):
        """MERGE_MSG read fails → merge_message_preview = '<unreadable>'."""
        repo = MagicMock(spec=Repo)
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        repo.active_branch.name = "dev"
        repo.index.unmerged_blobs.return_value = {}
        with patch("os.path.join", side_effect=lambda *a: "/tmp/repo/.git/MERGE_HEAD" if "MERGE_HEAD" in a else "/tmp/repo/.git/MERGE_MSG"), \
             patch("pathlib.Path.read_text", return_value="abc"), \
             patch("os.path.exists", return_value=True), \
             patch("pathlib.Path.read_text", side_effect=[Exception("unreadable"), "abc"]):
            # First call to read_text on MERGE_HEAD fails, second on MERGE_MSG not reached
            # Need different approach - mock MERGE_HEAD read succeeds, MERGE_MSG fails
            pass

    def test_merge_msg_read_exception_v2(self, mixin):
        """MERGE_MSG exists but read fails → merge_message_preview = '<unreadable>'."""
        repo = MagicMock(spec=Repo)
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        repo.active_branch.name = "dev"
        repo.index.unmerged_blobs.return_value = {}
        # Patch Path.read_text for MERGE_HEAD and MERGE_MSG separately
        orig_read_text = Path.read_text
        def side_effect_read(path_self):
            if "MERGE_MSG" in str(path_self):
                raise Exception("cannot read MERGE_MSG")
            return "abc123"
        with patch("os.path.join", side_effect=lambda *a: "/tmp/repo/.git/MERGE_HEAD" if "MERGE_HEAD" in a[-1] else "/tmp/repo/.git/MERGE_MSG"), \
             patch("os.path.exists", return_value=True), \
             patch.object(Path, "read_text", side_effect=side_effect_read):
            payload = mixin._build_unfinished_merge_payload(repo)
            assert payload["merge_message_preview"] == "<unreadable>"

    def test_current_branch_exception(self, mixin):
        """active_branch.name raises → current_branch = 'detached_or_unknown'."""
        repo = MagicMock(spec=Repo)
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        repo.index.unmerged_blobs.return_value = {}
        type(repo).active_branch = PropertyMock(side_effect=Exception("no head"))
        with patch("os.path.join", side_effect=lambda *a: "/tmp/repo/.git/MERGE_HEAD" if "MERGE_HEAD" in a[-1] else "/tmp/repo/.git/MERGE_MSG"), \
             patch("pathlib.Path.read_text", return_value="abc123"), \
             patch("os.path.exists", return_value=True):
            payload = mixin._build_unfinished_merge_payload(repo)
            assert payload["current_branch"] == "detached_or_unknown"

    def test_merge_msg_file_not_exists(self, mixin):
        """MERGE_MSG file does not exist → merge_message_preview empty."""
        repo = MagicMock(spec=Repo)
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        repo.active_branch.name = "dev"
        repo.index.unmerged_blobs.return_value = {}
        with patch("os.path.join", side_effect=lambda *a: "/tmp/repo/.git/MERGE_HEAD" if "MERGE_HEAD" in a[-1] else "/tmp/repo/.git/MERGE_MSG"), \
             patch("pathlib.Path.read_text", return_value="abc123"), \
             patch("os.path.exists", return_value=False):
            payload = mixin._build_unfinished_merge_payload(repo)
            assert payload["merge_message_preview"] == ""

    def test_merge_msg_readlines_single_line(self, mixin):
        """MERGE_MSG read returns multi-line → only first line used."""
        repo = MagicMock(spec=Repo)
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        repo.active_branch.name = "dev"
        repo.index.unmerged_blobs.return_value = {}
        with patch("os.path.join", side_effect=lambda *a: "/tmp/repo/.git/MERGE_HEAD" if "MERGE_HEAD" in a[-1] else "/tmp/repo/.git/MERGE_MSG"), \
             patch("pathlib.Path.read_text", return_value="Merge branch 'feature'\n\nConflicts:\n\tfile.txt\n"), \
             patch("os.path.exists", return_value=True):
            payload = mixin._build_unfinished_merge_payload(repo)
            assert payload["merge_message_preview"] == "Merge branch 'feature'"


class TestResolveMergeConflictsPathTraversal:
    """resolve_merge_conflicts — path traversal detection."""

    def test_path_traversal_above_repo_root(self, mixin):
        """File path escaping repo root → HTTPException 400."""
        repo = MagicMock(spec=Repo)
        repo.working_tree_dir = "/tmp/repo"
        # Use real os.path.abspath which resolves /tmp/repo/../../etc/passwd → /etc/passwd
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            with pytest.raises(HTTPException, match="Invalid conflict file path"):
                mixin.resolve_merge_conflicts(1, [
                    {"file_path": "../../etc/passwd", "resolution": "manual", "content": "x"}
                ])

    def test_empty_working_tree_dir_uses_cwd(self, mixin):
        """working_tree_dir is None → os.path.abspath('') returns CWD."""
        repo = MagicMock(spec=Repo)
        repo.working_tree_dir = None
        with patch.object(mixin, 'get_repo', return_value=repo, create=True), \
             patch("os.path.abspath", return_value=os.getcwd()), \
             patch("os.makedirs"), \
             patch("builtins.open", MagicMock()):
            result = mixin.resolve_merge_conflicts(1, [
                {"file_path": "safe.txt", "resolution": "manual", "content": "data"}
            ])
            assert result == ["safe.txt"]


class TestGetMergeStatusEdge:
    """get_merge_status — MERGE_HEAD exists but branch name fails."""

    @pytest.mark.asyncio
    async def test_merge_in_progress_branch_exception(self, mixin):
        """MERGE_HEAD exists but active_branch.name fails → detached_or_unknown."""
        repo = MagicMock(spec=Repo)
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        type(repo).active_branch = PropertyMock(side_effect=Exception("no branch"))
        repo.index.unmerged_blobs.return_value = {"f.txt": [(2, MagicMock())]}
        with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
             patch("os.path.exists", return_value=True), \
             patch("pathlib.Path.read_text", return_value="abc123"):
            result = await mixin.get_merge_status(1)
            assert result["has_unfinished_merge"] is True
            assert result["current_branch"] == "detached_or_unknown"
# #endregion Test.Git.Merge.Edge
