# #region Test.Git.Merge.Coverage [C:3] [TYPE Module] [SEMANTICS test, git, merge, coverage, edge]
# @BRIEF Additional edge coverage for GitServiceMergeMixin — error-specific paths in _build_unfinished_merge_payload, resolve_merge_conflicts root, promote_direct_merge fallbacks.
# @RELATION BINDS_TO -> [GitServiceMergeMixin]

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

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

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


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


def _make_ref(name: str):
    """Create a simple mock object with .name returning the given string."""
    r = MagicMock()
    r.name = name
    return r


class TestBuildUnfinishedMergePayload:
    """Cover edge-error handlers in _build_unfinished_merge_payload."""

    def test_merge_head_read_error(self, mixin):
        """merge_head_path.read_text fails → merge_head = '<unreadable>'."""
        repo = MagicMock()
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        repo.active_branch.name = "main"
        repo.index.unmerged_blobs.return_value = {}
        with patch("os.path.exists", return_value=True), \
             patch("pathlib.Path.read_text", side_effect=[OSError("perm denied"), "merge msg"]):
            result = mixin._build_unfinished_merge_payload(repo)
        assert result["merge_head"] == "<unreadable>"

    def test_merge_msg_read_error(self, mixin):
        """MERGE_MSG read fails → merge_message_preview = '<unreadable>'."""
        repo = MagicMock()
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        repo.active_branch.name = "main"
        repo.index.unmerged_blobs.return_value = {}
        with patch("os.path.exists", return_value=True), \
             patch("pathlib.Path.read_text", side_effect=["abc123", OSError("no msg")]):
            result = mixin._build_unfinished_merge_payload(repo)
        assert result["merge_message_preview"] == "<unreadable>"

    def test_merge_msg_file_missing(self, mixin):
        """MERGE_MSG file does not exist → merge_message_preview = empty."""
        repo = MagicMock()
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        repo.active_branch.name = "main"
        repo.index.unmerged_blobs.return_value = {}
        def exists_side(path):
            return "MERGE_MSG" not in path
        with patch("os.path.exists", side_effect=exists_side), \
             patch("pathlib.Path.read_text", return_value="abc123"):
            result = mixin._build_unfinished_merge_payload(repo)
        assert result["merge_message_preview"] == ""

    def test_active_branch_error(self, mixin):
        """active_branch.name raises → current_branch = 'detached_or_unknown'."""
        repo = MagicMock()
        repo.git_dir = "/tmp/repo/.git"
        repo.working_tree_dir = "/tmp/repo"
        type(repo.active_branch).name = PropertyMock(side_effect=Exception("detached"))
        repo.index.unmerged_blobs.return_value = {}
        with patch("os.path.exists", return_value=True), \
             patch("pathlib.Path.read_text", return_value="abc123"):
            result = mixin._build_unfinished_merge_payload(repo)
        assert result["current_branch"] == "detached_or_unknown"


class TestResolveMergeConflicts:
    """Cover missing root error path."""

    def test_empty_working_tree_dir_raises(self, mixin):
        """Empty string working_tree_dir → repo_root becomes CWD, but abspath returns ''."""
        repo = MagicMock()
        repo.working_tree_dir = ""
        with patch.object(mixin, 'get_repo', return_value=repo, create=True), \
             patch("os.path.abspath", return_value=""):
            with pytest.raises(HTTPException, match="unavailable"):
                mixin.resolve_merge_conflicts(1, [{"file_path": "f.txt", "resolution": "mine"}])


class TestPromoteDirectMerge:
    """Cover promote error-recovery paths."""

    def test_original_branch_exception(self, mixin):
        """active_branch.name raises → original_branch = None."""
        repo = MagicMock()
        repo.heads = [_make_ref("dev"), _make_ref("main")]
        repo.refs = [_make_ref("origin/dev"), _make_ref("origin/main")]
        origin = MagicMock()
        repo.remote.return_value = origin
        type(repo.active_branch).name = PropertyMock(side_effect=[Exception("no branch"), "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_source_branch_from_origin_ref(self, mixin):
        """Source from origin/ref → checkout -b from origin."""
        repo = MagicMock()
        repo.heads = [_make_ref("main")]
        repo.refs = [_make_ref("origin/dev"), _make_ref("origin/main")]
        origin = MagicMock()
        repo.remote.return_value = origin
        type(repo.active_branch).name = PropertyMock(return_value="dev")
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            result = mixin.promote_direct_merge(1, "dev", "main")
        assert result["status"] == "merged"
        repo.git.checkout.assert_any_call("-b", "dev", "origin/dev")

    def test_target_branch_from_origin_ref(self, mixin):
        """Target from origin/ref → checkout -b from origin."""
        repo = MagicMock()
        repo.heads = [_make_ref("dev")]
        repo.refs = [_make_ref("origin/dev"), _make_ref("origin/staging")]
        origin = MagicMock()
        repo.remote.return_value = origin
        type(repo.active_branch).name = PropertyMock(return_value="dev")
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            result = mixin.promote_direct_merge(1, "dev", "staging")
        assert result["status"] == "merged"

    def test_pull_target_failure_logged(self, mixin):
        """origin.pull(target) fails → logged, merge proceeds."""
        repo = MagicMock()
        repo.heads = [_make_ref("dev"), _make_ref("main")]
        repo.refs = [_make_ref("origin/main")]
        origin = MagicMock()
        repo.remote.return_value = origin
        type(repo.active_branch).name = PropertyMock(return_value="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_raises_409(self, mixin):
        """CONFLICT → HTTP 409."""
        repo = MagicMock()
        repo.heads = [_make_ref("dev"), _make_ref("main")]
        repo.refs = [_make_ref("origin/main")]
        origin = MagicMock()
        repo.remote.return_value = origin
        repo.git.merge.side_effect = Exception("CONFLICT in file.txt")
        type(repo.active_branch).name = PropertyMock(return_value="dev")
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            with pytest.raises(HTTPException, match="Merge conflict"):
                mixin.promote_direct_merge(1, "dev", "main")

    def test_merge_generic_error_raises_500(self, mixin):
        """Generic error → HTTP 500."""
        repo = MagicMock()
        repo.heads = [_make_ref("dev"), _make_ref("main")]
        repo.refs = [_make_ref("origin/main")]
        origin = MagicMock()
        repo.remote.return_value = origin
        repo.git.merge.side_effect = Exception("some other error")
        type(repo.active_branch).name = PropertyMock(return_value="dev")
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            with pytest.raises(HTTPException, match="Direct promote failed"):
                mixin.promote_direct_merge(1, "dev", "main")

    def test_restore_original_branch_failure(self, mixin):
        """Restore fails in finally → caught."""
        repo = MagicMock()
        repo.heads = [_make_ref("dev"), _make_ref("main")]
        repo.refs = [_make_ref("origin/main")]
        origin = MagicMock()
        repo.remote.return_value = origin
        repo.git.checkout.side_effect = [None, None, GeneratorExit("checkout fail")]
        type(repo.active_branch).name = PropertyMock(return_value="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_target_branch_not_found(self, mixin):
        """Target not found → HTTP 404."""
        repo = MagicMock()
        repo.heads = [_make_ref("dev")]
        repo.refs = [_make_ref("origin/dev")]
        origin = MagicMock()
        repo.remote.return_value = origin
        type(repo.active_branch).name = PropertyMock(return_value="dev")
        with patch.object(mixin, 'get_repo', return_value=repo, create=True):
            with pytest.raises(HTTPException, match="not found"):
                mixin.promote_direct_merge(1, "dev", "nonexistent")
# #endregion Test.Git.Merge.Coverage
