# #region Test.Git.Branch.Edge [C:3] [TYPE Module] [SEMANTICS test, git, branch, edge, coverage]
# @BRIEF Edge case tests for GitServiceBranchMixin — gitflow error paths, list/create/checkout edge cases.
# @RELATION BINDS_TO -> [GitServiceBranchMixin]

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

from src.services.git._branch import GitServiceBranchMixin


class TestableGitBranch(GitServiceBranchMixin):
    """Concrete test class providing _locked and get_repo stubs."""
    def __init__(self, mock_repo=None):
        self._mock_repo = mock_repo or MagicMock()

    async def get_repo(self, dashboard_id):
        return self._mock_repo

    def _locked(self, dashboard_id):
        @contextlib.contextmanager
        def _lock():
            yield
        return _lock()


# ── _ensure_gitflow_branches edge ──

class TestEnsureGitflowBranchesEdge:
    """Edge paths in _ensure_gitflow_branches."""

    def test_push_branch_failure(self):
        """Push branch to origin fails → HTTPException 500."""
        from src.services.git._branch import GitServiceBranchMixin
        repo = MagicMock()
        main_head = MagicMock()
        main_head.name = "main"
        main_head.commit = MagicMock()
        repo.heads = [main_head]
        repo.head.commit = MagicMock()
        origin = MagicMock()
        origin.refs = []
        origin.push.side_effect = Exception("push failed")
        repo.remote.return_value = origin
        svc = TestableGitBranch(repo)
        with pytest.raises(HTTPException, match="Failed to create default branch"):
            svc._ensure_gitflow_branches(repo, 1)

    def test_fetch_exception_swallowed(self):
        """origin.fetch() failure → caught, remote branches skipped."""
        from src.services.git._branch import GitServiceBranchMixin
        repo = MagicMock()
        main_head = MagicMock()
        main_head.name = "main"
        main_head.commit = MagicMock()
        repo.heads = [main_head]
        repo.head.commit = MagicMock()
        origin = MagicMock()
        origin.fetch.side_effect = Exception("fetch failed")
        repo.remote.return_value = origin
        svc = TestableGitBranch(repo)
        # Should not raise — fetch failure is caught and logged
        svc._ensure_gitflow_branches(repo, 1)

    def test_push_missing_branch_raises(self):
        """Exception pushing branch to origin → HTTPException 500."""
        from src.services.git._branch import GitServiceBranchMixin
        repo = MagicMock()
        main_head = MagicMock()
        main_head.name = "main"
        main_head.commit = MagicMock()
        dev_head = MagicMock()
        dev_head.name = "dev"
        dev_head.commit = MagicMock()
        repo.heads = [main_head, dev_head]
        repo.head.commit = MagicMock()
        origin = MagicMock()
        origin.refs = [MagicMock()]
        origin.refs[0].remote_head = "main"
        repo.remote.return_value = origin
        # Only dev is missing remote — push fails
        origin.push.side_effect = Exception("push error")
        svc = TestableGitBranch(repo)
        with pytest.raises(HTTPException, match="Failed to create default branch"):
            svc._ensure_gitflow_branches(repo, 1)

    def test_checkout_dev_failure_swallowed(self):
        """Checkout dev after gitflow fails → logged, not fatal."""
        from src.services.git._branch import GitServiceBranchMixin
        repo = MagicMock()
        main_head = MagicMock()
        main_head.name = "main"
        main_head.commit = MagicMock()
        preprod_head = MagicMock()
        preprod_head.name = "preprod"
        preprod_head.commit = MagicMock()
        repo.heads = [main_head, preprod_head]
        repo.head.commit = MagicMock()
        repo.active_branch.name = "main"
        origin = MagicMock()
        origin.refs = [MagicMock(remote_head="main")]
        repo.remote.return_value = origin
        repo.create_head.side_effect = None
        repo.git.checkout.side_effect = Exception("checkout failed")
        svc = TestableGitBranch(repo)
        svc._ensure_gitflow_branches(repo, 1)
        # Should not raise — checkout failure is logged


# ── list_branches edge ──

class TestListBranchesEdge:
    """list_branches — skip-exception and fallback paths."""

    @pytest.mark.asyncio
    async def test_skip_ref_on_exception(self):
        """Ref with no commit attribute → skipped, not failed."""
        from src.services.git._branch import GitServiceBranchMixin
        repo = MagicMock()
        bad_ref = MagicMock()
        bad_ref.name = "refs/heads/dev"
        del bad_ref.commit  # Simulate missing commit attribute
        repo.refs = [bad_ref]
        repo.active_branch.name = "dev"
        svc = TestableGitBranch(repo)
        result = await svc.list_branches(1)
        # Should still have dev entry from active_branch
        assert any(b["name"] == "dev" for b in result)

    @pytest.mark.asyncio
    async def test_active_branch_not_in_list_added(self):
        """Active branch not in refs list → appended."""
        from src.services.git._branch import GitServiceBranchMixin
        repo = MagicMock()
        local_ref = MagicMock()
        local_ref.name = "refs/heads/main"
        local_ref.commit.hexsha = "abc"
        local_ref.commit.committed_date = 1700000000
        local_ref.is_remote.return_value = False
        repo.refs = [local_ref]
        repo.active_branch.name = "dev"  # dev not in refs
        svc = TestableGitBranch(repo)
        result = await svc.list_branches(1)
        names = [b["name"] for b in result]
        assert "dev" in names
        assert "main" in names

    @pytest.mark.asyncio
    async def test_detached_head_fallback_uses_refs(self):
        """Detached HEAD but refs exist → no fallback needed."""
        from src.services.git._branch import GitServiceBranchMixin
        repo = MagicMock()
        local_ref = MagicMock()
        local_ref.name = "refs/heads/main"
        local_ref.commit.hexsha = "abc"
        local_ref.commit.committed_date = 1700000000
        local_ref.is_remote.return_value = False
        repo.refs = [local_ref]
        type(repo.active_branch).name = PropertyMock(side_effect=TypeError("detached"))
        svc = TestableGitBranch(repo)
        result = await svc.list_branches(1)
        # Fallback 'dev' should not be added because refs exist
        assert any(b["name"] == "main" for b in result)


# ── checkout_branch edge ──

class TestCheckoutBranchEdge:
    """checkout_branch — file parsing in stderr and generic paths."""

    @pytest.mark.asyncio
    async     def test_local_changes_returns_409_with_error_code(self):
        """Local changes → HTTPException 409 with GIT_CHECKOUT_LOCAL_CHANGES."""
        from src.services.git._branch import GitServiceBranchMixin
        repo = MagicMock()
        error = GitCommandError("checkout", "error")
        error.stderr = (
            "error: Your local changes to the following files would be overwritten by checkout:\n"
            "\tconfig.yaml\n"
        )
        repo.git.checkout.side_effect = error
        svc = TestableGitBranch(repo)
        with pytest.raises(HTTPException) as exc_info:
            await svc.checkout_branch(1, "other")
        assert exc_info.value.status_code == 409
        detail = exc_info.value.detail
        assert detail["error_code"] == "GIT_CHECKOUT_LOCAL_CHANGES"
        # Files list is empty because stderr parsing requires line.startswith('\t')

    @pytest.mark.asyncio
    async def test_local_changes_parses_files_from_stderr(self):
        """Stderr with tab-prefixed files → files list populated (line 200)."""
        from src.services.git._branch import GitServiceBranchMixin
        repo = MagicMock()
        error = GitCommandError("checkout", "error")
        error.stderr = (
            "error: Your local changes to the following files would be overwritten:\n"
            "\tdashboard/config.yaml\n"
            "\tdata/schema.sql\n"
        )
        repo.git.checkout.side_effect = error
        svc = TestableGitBranch(repo)
        with pytest.raises(HTTPException) as exc_info:
            await svc.checkout_branch(1, "other")
        detail = exc_info.value.detail
        assert "dashboard/config.yaml" in detail["files"]
        assert "data/schema.sql" in detail["files"]

    @pytest.mark.asyncio
    async def test_checkout_git_error_no_stderr(self):
        """GitCommandError with no stderr → generic 500."""
        from src.services.git._branch import GitServiceBranchMixin
        repo = MagicMock()
        error = GitCommandError("checkout", "fatal: bad revision")
        error.stderr = ""
        repo.git.checkout.side_effect = error
        svc = TestableGitBranch(repo)
        with pytest.raises(HTTPException) as exc_info:
            await svc.checkout_branch(1, "nonexistent")
        assert exc_info.value.status_code == 500


# ── create_branch edge ──

class TestCreateBranchEdge:
    """create_branch — exception handling."""

    @pytest.mark.asyncio
    async def test_create_head_raises(self):
        """create_head exception → re-raised."""
        from src.services.git._branch import GitServiceBranchMixin
        repo = MagicMock()
        repo.heads = [MagicMock()]
        repo.remotes = [MagicMock()]
        repo.commit.return_value = MagicMock()
        repo.create_head.side_effect = Exception("branch exists")
        svc = TestableGitBranch(repo)
        with pytest.raises(Exception, match="branch exists"):
            await svc.create_branch(1, "existing", "main")

    @pytest.mark.asyncio
    async def test_init_commit_already_exists(self):
        """Empty repo but README.md already exists → skips creation."""
        from src.services.git._branch import GitServiceBranchMixin
        repo = MagicMock()
        repo.heads = []
        repo.remotes = []
        repo.working_dir = "/tmp/repo"
        new_branch = MagicMock()
        repo.create_head.return_value = new_branch
        svc = TestableGitBranch(repo)
        with patch("os.path.exists", return_value=True), \
             patch("builtins.open", MagicMock()):
            result = await svc.create_branch(1, "feature", "main")
        repo.index.add.assert_called_with(["README.md"])
        repo.index.commit.assert_called_with("Initial commit")


# ── commit_changes edge ──

class TestCommitChangesEdge:
    """commit_changes — edge paths."""

    @pytest.mark.asyncio
    async def test_commit_with_empty_files_list(self):
        """Empty files list → stages all (not specific)."""
        from src.services.git._branch import GitServiceBranchMixin
        repo = MagicMock()
        repo.is_dirty.return_value = True
        svc = TestableGitBranch(repo)
        await svc.commit_changes(1, "msg", files=[])
        repo.git.add.assert_called_with(A=True)


# ── _ensure_gitflow_branches specific branches ──

class TestEnsureGitflowMissingMain:
    """_ensure_gitflow_branches — main branch missing from heads."""

    def test_main_not_in_heads_created_from_base(self):
        """main not in local_heads → created from repo.head.commit."""
        from src.services.git._branch import GitServiceBranchMixin
        repo = MagicMock()
        # No heads at all
        repo.heads = []
        repo.head.commit = MagicMock()
        repo.active_branch.name = "dev"
        origin = MagicMock()
        origin.refs = []
        repo.remote.return_value = origin
        svc = TestableGitBranch(repo)
        svc._ensure_gitflow_branches(repo, 1)
        # main should be created from repo.head.commit
        repo.create_head.assert_any_call("main", repo.head.commit)

    def test_active_branch_raises_no_checkout(self):
        """active_branch.name raises → current_branch=None, checkout dev attempted."""
        from src.services.git._branch import GitServiceBranchMixin
        repo = MagicMock()
        main_head = MagicMock()
        main_head.name = "main"
        main_head.commit = MagicMock()
        repo.heads = [main_head]
        repo.head.commit = MagicMock()
        type(repo.active_branch).name = PropertyMock(side_effect=Exception("detached"))
        origin = MagicMock()
        origin.refs = [MagicMock(remote_head="main"), MagicMock(remote_head="dev"), MagicMock(remote_head="preprod")]
        repo.remote.return_value = origin
        svc = TestableGitBranch(repo)
        svc._ensure_gitflow_branches(repo, 1)
        # With current_branch=None, None != "dev" is True, so checkout("dev") is called
        repo.git.checkout.assert_called_once_with("dev")
# #endregion Test.Git.Branch.Edge
