# #region Test.Git.Gitea.Coverage [C:3] [TYPE Module] [SEMANTICS test, git, gitea, coverage, branch, pr, error]
# @BRIEF Additional edge coverage for GitServiceGiteaMixin — error JSON parse, description params, non-dict existing, branch re-raise, PR detail, fallback failure.
# @RELATION BINDS_TO -> [GitServiceGiteaMixin]

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

import pytest
from fastapi import HTTPException

from src.models.git import GitProvider
from src.services.git._gitea import GitServiceGiteaMixin


class TestableGitGitea(GitServiceGiteaMixin):
    """Concrete wrapper that inherits mixin methods and provides URL stubs."""
    def __init__(self):
        self._http_client = AsyncMock()

    def _normalize_git_server_url(self, raw_url):
        normalized = (raw_url or "").strip()
        if not normalized:
            raise HTTPException(status_code=400, detail="Git server URL is required")
        return normalized.rstrip("/")

    def _parse_remote_repo_identity(self, remote_url):
        from urllib.parse import urlparse
        normalized = str(remote_url or "").strip()
        if not normalized:
            raise HTTPException(status_code=400, detail="empty")
        if normalized.startswith("git@"):
            path = normalized.split(":", 1)[1] if ":" in normalized else ""
        else:
            parsed = urlparse(normalized)
            path = parsed.path or ""
        path = path.strip("/")
        if path.endswith(".git"):
            path = path[:-4]
        parts = [s for s in path.split("/") if s]
        if len(parts) < 2:
            raise HTTPException(status_code=400, detail="Cannot parse")
        owner, repo = parts[0], parts[-1]
        namespace = "/".join(parts[:-1])
        return {"owner": owner, "repo": repo, "namespace": namespace, "full_name": f"{namespace}/{repo}"}

    def _derive_server_url_from_remote(self, remote_url):
        from urllib.parse import urlparse
        normalized = str(remote_url or "").strip()
        if not normalized or normalized.startswith("git@"):
            return None
        parsed = urlparse(normalized)
        if parsed.scheme not in {"http", "https"}:
            return None
        if not parsed.hostname:
            return None
        netloc = parsed.hostname
        if parsed.port:
            netloc = f"{netloc}:{parsed.port}"
        return f"{parsed.scheme}://{netloc}"


@pytest.fixture
def svc():
    return TestableGitGitea()


class TestGiteaRequestCoverage:
    """Cover JSON parse exception branch in _gitea_request."""

    @pytest.mark.asyncio
    async def test_error_response_no_json(self, svc):
        """4xx response, JSON parse fails → uses raw text for detail."""
        resp = MagicMock()
        resp.status_code = 403
        resp.text = "forbidden"
        resp.json.side_effect = ValueError("not json")
        svc._http_client.request.return_value = resp
        with pytest.raises(HTTPException, match="forbidden"):
            await svc._gitea_request("GET", "https://gitea.com", "pat", "/user")


class TestCreateGiteaRepositoryCoverage:
    """Cover description param and non-dict existing repo response."""

    @pytest.mark.asyncio
    async def test_create_with_description(self, svc):
        """description provided → sent in payload."""
        created = {"name": "my-repo"}
        with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value=created) as mock_req:
            result = await svc.create_gitea_repository(
                "https://gitea.com", "pat", "my-repo",
                description="Test description",
            )
        assert result["name"] == "my-repo"
        call_payload = mock_req.call_args[1]["payload"]
        assert call_payload["description"] == "Test description"

    @pytest.mark.asyncio
    async def test_description_default_empty(self, svc):
        """No description → not in payload."""
        created = {"name": "repo"}
        with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value=created) as mock_req:
            await svc.create_gitea_repository("https://gitea.com", "pat", "repo")
        call_payload = mock_req.call_args[1]["payload"]
        assert "description" not in call_payload

    @pytest.mark.asyncio
    async def test_conflict_existing_not_dict_raises(self, svc):
        """409 conflict, existing GET returns non-dict → raises original 409."""
        call_log = []
        async def mock_request(method, url, pat, endpoint, payload=None):
            call_log.append((method, endpoint))
            if method == "POST":
                raise HTTPException(status_code=409, detail="conflict")
            if method == "GET" and endpoint == "/user":
                return {"login": "owner"}
            return ["not_a_dict"]
        with patch.object(svc, "_gitea_request", side_effect=mock_request):
            with pytest.raises(HTTPException, match="409"):
                await svc.create_gitea_repository("https://gitea.com", "pat", "existing-repo")


class TestGiteaBranchExistsCoverage:
    """Cover non-404 re-raise in _gitea_branch_exists."""

    @pytest.mark.asyncio
    async def test_non_404_raises(self, svc):
        """Non-404 HTTPException → re-raised."""
        with patch.object(svc, "_gitea_request", side_effect=HTTPException(status_code=403, detail="forbidden")):
            with pytest.raises(HTTPException, match="forbidden"):
                await svc._gitea_branch_exists("https://gitea.com", "pat", "owner", "repo", "dev")


class TestBuildGiteaPR404Detail:
    """_build_gitea_pr_404_detail — all branch-combination paths."""

    @pytest.mark.asyncio
    async def test_source_branch_missing(self, svc):
        """Source branch not found → specific detail."""
        with patch.object(svc, "_gitea_branch_exists", new_callable=AsyncMock) as mock_exists:
            async def exists_side(server_url, pat, owner, repo, branch):
                return branch == "main"
            mock_exists.side_effect = exists_side
            result = await svc._build_gitea_pr_404_detail(
                "https://gitea.com", "pat", "owner", "repo", "feature", "main",
            )
        assert "source branch" in (result or "")

    @pytest.mark.asyncio
    async def test_target_branch_missing(self, svc):
        """Target branch not found → specific detail."""
        with patch.object(svc, "_gitea_branch_exists", new_callable=AsyncMock) as mock_exists:
            async def exists_side(server_url, pat, owner, repo, branch):
                return branch == "feature"
            mock_exists.side_effect = exists_side
            result = await svc._build_gitea_pr_404_detail(
                "https://gitea.com", "pat", "owner", "repo", "feature", "main",
            )
        assert "target branch" in (result or "")

    @pytest.mark.asyncio
    async def test_both_branches_exist(self, svc):
        """Both branches exist → returns None."""
        with patch.object(svc, "_gitea_branch_exists", new_callable=AsyncMock, return_value=True):
            result = await svc._build_gitea_pr_404_detail(
                "https://gitea.com", "pat", "owner", "repo", "feature", "main",
            )
        assert result is None


class TestCreateGiteaPRCoverage:
    """Cover fallback retry still 404 path."""

    @pytest.mark.asyncio
    async def test_fallback_also_404_with_detail(self, svc):
        """Fallback URL also returns 404, branch detail found → HTTP 400."""
        call_count = [0]
        async def mock_request(method, url, pat, endpoint, payload=None):
            call_count[0] += 1
            if method == "POST":
                raise HTTPException(status_code=404, detail="not found")
            raise HTTPException(status_code=999, detail="unexpected")
        with patch.object(svc, "_gitea_request", side_effect=mock_request), \
             patch.object(svc, "_build_gitea_pr_404_detail", new_callable=AsyncMock, return_value="source branch 'feature' not found"):
            with pytest.raises(HTTPException) as exc_info:
                await svc.create_gitea_pull_request(
                    "https://gitea.com/owner", "pat",
                    "https://gitea.com/owner/repo.git",
                    "feature", "main", "PR title",
                )
            assert exc_info.value.status_code == 400
            assert "source" in exc_info.value.detail

    @pytest.mark.asyncio
    async def test_fallback_also_404_no_detail(self, svc):
        """Fallback also 404, branch detail None → raises original."""
        async def mock_request(method, url, pat, endpoint, payload=None):
            raise HTTPException(status_code=404, detail="not found")
        with patch.object(svc, "_gitea_request", side_effect=mock_request), \
             patch.object(svc, "_build_gitea_pr_404_detail", new_callable=AsyncMock, return_value=None):
            with pytest.raises(HTTPException, match="not found"):
                await svc.create_gitea_pull_request(
                    "https://gitea.com/owner", "pat",
                    "https://gitea.com/owner/repo.git",
                    "feature", "main", "PR title",
                )

    @pytest.mark.asyncio
    async def test_no_fallback_no_retry(self, svc):
        """No fallback URL → no retry on 404."""
        async def mock_request(method, url, pat, endpoint, payload=None):
            raise HTTPException(status_code=404, detail="not found")
        with patch.object(svc, "_gitea_request", side_effect=mock_request), \
             patch.object(svc, "_derive_server_url_from_remote", return_value=None), \
             patch.object(svc, "_build_gitea_pr_404_detail", new_callable=AsyncMock, return_value=None):
            with pytest.raises(HTTPException, match="not found"):
                await svc.create_gitea_pull_request(
                    "https://gitea.com/owner", "pat",
                    "https://gitea.com/owner/repo.git",
                    "feature", "main", "PR title",
                )
# #endregion Test.Git.Gitea.Coverage
