# #region Test.Git.Gitea [C:3] [TYPE Module] [SEMANTICS test,git,gitea,api,http,pull-request]
# @BRIEF Tests for GitServiceGiteaMixin — connection testing, headers, API requests, user resolution, repo CRUD, PR creation with fallback.
# @RELATION BINDS_TO -> [GitServiceGiteaMixin]
# @TEST_EDGE: local_url -> returns True without API call
# @TEST_EDGE: invalid_protocol -> returns False
# @TEST_EDGE: empty_pat -> returns False
# @TEST_EDGE: api_error -> HTTPException with status code
# @TEST_EDGE: network_error -> HTTPException 503
# @TEST_EDGE: empty_pat_headers -> HTTPException 400
# @TEST_EDGE: create_repo_conflict -> fetches existing repo
# @TEST_EDGE: pr_404_fallback -> retries with derived URL
# @TEST_EDGE: pr_404_branch_missing -> HTTPException 400 with detail
# @TEST_EDGE: delete_missing_owner -> HTTPException 400

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

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

from fastapi import HTTPException

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


class TestableGitGitea(GitServiceGiteaMixin):
    """Concrete test class providing _http_client and URL mixin 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}"


# ── test_connection ──

class TestTestConnection:
    """test_connection — provider connectivity check."""

    # #region test_connection_local_url [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_connection_local_url(self):
        """Local/localhost URL → True without API call."""
        svc = TestableGitGitea()
        result = await svc.test_connection(GitProvider.GITEA, "http://gitea.local:3000", "pat")
        assert result is True
        svc._http_client.get.assert_not_called()
    # #endregion test_connection_local_url

    # #region test_connection_invalid_protocol [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_connection_invalid_protocol(self):
        """Non-HTTP URL → False."""
        svc = TestableGitGitea()
        result = await svc.test_connection(GitProvider.GITEA, "ftp://host.com", "pat")
        assert result is False
    # #endregion test_connection_invalid_protocol

    # #region test_connection_empty_pat [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_connection_empty_pat(self):
        """Empty PAT → False."""
        svc = TestableGitGitea()
        result = await svc.test_connection(GitProvider.GITEA, "https://gitea.com", "")
        assert result is False
    # #endregion test_connection_empty_pat

    # #region test_connection_gitea_success [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_connection_gitea_success(self):
        """Gitea 200 → True."""
        svc = TestableGitGitea()
        resp = MagicMock()
        resp.status_code = 200
        svc._http_client.get.return_value = resp
        result = await svc.test_connection(GitProvider.GITEA, "https://gitea.example.com", "valid-pat")
        assert result is True
        svc._http_client.get.assert_called_once()
        call_url = svc._http_client.get.call_args[0][0]
        assert "/api/v1/user" in call_url
    # #endregion test_connection_gitea_success

    # #region test_connection_github_success [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_connection_github_success(self):
        """GitHub 200 → True."""
        svc = TestableGitGitea()
        resp = MagicMock()
        resp.status_code = 200
        svc._http_client.get.return_value = resp
        result = await svc.test_connection(GitProvider.GITHUB, "https://github.com", "valid-pat")
        assert result is True
        call_url = svc._http_client.get.call_args[0][0]
        assert "api.github.com" in call_url
    # #endregion test_connection_github_success

    # #region test_connection_gitlab_success [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_connection_gitlab_success(self):
        """GitLab 200 → True."""
        svc = TestableGitGitea()
        resp = MagicMock()
        resp.status_code = 200
        svc._http_client.get.return_value = resp
        result = await svc.test_connection(GitProvider.GITLAB, "https://gitlab.com", "valid-pat")
        assert result is True
        call_url = svc._http_client.get.call_args[0][0]
        assert "/api/v4/user" in call_url
    # #endregion test_connection_gitlab_success

    # #region test_connection_failure_status [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_connection_failure_status(self):
        """Non-200 response → False."""
        svc = TestableGitGitea()
        resp = MagicMock()
        resp.status_code = 401
        svc._http_client.get.return_value = resp
        result = await svc.test_connection(GitProvider.GITEA, "https://gitea.example.com", "bad-pat")
        assert result is False
    # #endregion test_connection_failure_status

    # #region test_connection_network_error [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_connection_network_error(self):
        """Network exception → False."""
        svc = TestableGitGitea()
        svc._http_client.get.side_effect = Exception("connection refused")
        result = await svc.test_connection(GitProvider.GITEA, "https://gitea.example.com", "pat")
        assert result is False
    # #endregion test_connection_network_error

    # #region test_connection_unknown_provider [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_connection_unknown_provider(self):
        """Unknown provider → False."""
        svc = TestableGitGitea()
        result = await svc.test_connection("BITBUCKET", "https://bitbucket.org", "pat")
        assert result is False
    # #endregion test_connection_unknown_provider


# ── _gitea_headers ──

class TestGiteaHeaders:
    """_gitea_headers — authorization header construction."""

    # #region test_headers_valid_pat [C:2] [TYPE Function]
    def test_headers_valid_pat(self):
        """Valid PAT → Authorization token header."""
        svc = TestableGitGitea()
        headers = svc._gitea_headers("my-token")
        assert headers["Authorization"] == "token my-token"
        assert headers["Content-Type"] == "application/json"
    # #endregion test_headers_valid_pat

    # #region test_headers_empty_pat [C:2] [TYPE Function]
    def test_headers_empty_pat(self):
        """Empty PAT → HTTPException 400."""
        svc = TestableGitGitea()
        with pytest.raises(HTTPException, match="PAT is required"):
            svc._gitea_headers("")
    # #endregion test_headers_empty_pat


# ── _gitea_request ──

class TestGiteaRequest:
    """_gitea_request — HTTP request wrapper with error mapping."""

    # #region test_request_success [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_request_success(self):
        """200 response → JSON payload returned."""
        svc = TestableGitGitea()
        resp = MagicMock()
        resp.status_code = 200
        resp.json.return_value = {"login": "admin"}
        svc._http_client.request.return_value = resp
        result = await svc._gitea_request("GET", "https://gitea.com", "pat", "/user")
        assert result == {"login": "admin"}
    # #endregion test_request_success

    # #region test_request_204_no_content [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_request_204_no_content(self):
        """204 → returns None."""
        svc = TestableGitGitea()
        resp = MagicMock()
        resp.status_code = 204
        svc._http_client.request.return_value = resp
        result = await svc._gitea_request("DELETE", "https://gitea.com", "pat", "/repos/o/r")
        assert result is None
    # #endregion test_request_204_no_content

    # #region test_request_api_error [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_request_api_error(self):
        """4xx/5xx → HTTPException with detail."""
        svc = TestableGitGitea()
        resp = MagicMock()
        resp.status_code = 404
        resp.text = "Not Found"
        resp.json.return_value = {"message": "repository not found"}
        svc._http_client.request.return_value = resp
        with pytest.raises(HTTPException, match="repository not found"):
            await svc._gitea_request("GET", "https://gitea.com", "pat", "/repos/o/r")
    # #endregion test_request_api_error

    # #region test_request_network_error [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_request_network_error(self):
        """Network exception → HTTPException 503."""
        svc = TestableGitGitea()
        svc._http_client.request.side_effect = Exception("connection refused")
        with pytest.raises(HTTPException, match="unavailable"):
            await svc._gitea_request("GET", "https://gitea.com", "pat", "/user")
    # #endregion test_request_network_error


# ── get_gitea_current_user ──

class TestGetGiteaCurrentUser:
    """get_gitea_current_user — resolve authenticated username."""

    # #region test_current_user_login [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_current_user_login(self):
        """Returns 'login' field from API response."""
        svc = TestableGitGitea()
        with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value={"login": "admin"}):
            result = await svc.get_gitea_current_user("https://gitea.com", "pat")
        assert result == "admin"
    # #endregion test_current_user_login

    # #region test_current_user_username_fallback [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_current_user_username_fallback(self):
        """Falls back to 'username' field."""
        svc = TestableGitGitea()
        with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value={"username": "bob"}):
            result = await svc.get_gitea_current_user("https://gitea.com", "pat")
        assert result == "bob"
    # #endregion test_current_user_username_fallback

    # #region test_current_user_missing [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_current_user_missing(self):
        """No login/username → HTTPException 500."""
        svc = TestableGitGitea()
        with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value={}):
            with pytest.raises(HTTPException, match="Failed to resolve"):
                await svc.get_gitea_current_user("https://gitea.com", "pat")
    # #endregion test_current_user_missing


# ── list_gitea_repositories ──

class TestListGiteaRepositories:
    """list_gitea_repositories — list user repos."""

    # #region test_list_repos_success [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_list_repos_success(self):
        """Returns list from API."""
        svc = TestableGitGitea()
        repos = [{"name": "repo1"}, {"name": "repo2"}]
        with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value=repos):
            result = await svc.list_gitea_repositories("https://gitea.com", "pat")
        assert len(result) == 2
    # #endregion test_list_repos_success

    # #region test_list_repos_non_list [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_list_repos_non_list(self):
        """Non-list response → returns []."""
        svc = TestableGitGitea()
        with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value={"error": "bad"}):
            result = await svc.list_gitea_repositories("https://gitea.com", "pat")
        assert result == []
    # #endregion test_list_repos_non_list


# ── create_gitea_repository ──

class TestCreateGiteaRepository:
    """create_gitea_repository — create or fetch existing."""

    # #region test_create_repo_success [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_create_repo_success(self):
        """200 → returns created repo dict."""
        svc = TestableGitGitea()
        created = {"name": "new-repo", "clone_url": "https://gitea.com/user/new-repo.git"}
        with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value=created):
            result = await svc.create_gitea_repository("https://gitea.com", "pat", "new-repo")
        assert result["name"] == "new-repo"
    # #endregion test_create_repo_success

    # #region test_create_repo_conflict_fetches_existing [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_create_repo_conflict_fetches_existing(self):
        """409 → fetches existing repo by name."""
        svc = TestableGitGitea()
        existing = {"name": "existing-repo", "clone_url": "https://gitea.com/user/existing-repo.git"}
        call_count = 0
        async def mock_request(method, url, pat, endpoint, payload=None):
            nonlocal call_count
            call_count += 1
            if method == "POST":
                raise HTTPException(status_code=409, detail="conflict")
            if endpoint.startswith("/user"):
                return {"login": "user"}
            return existing
        with patch.object(svc, "_gitea_request", side_effect=mock_request):
            result = await svc.create_gitea_repository("https://gitea.com", "pat", "existing-repo")
        assert result["name"] == "existing-repo"
    # #endregion test_create_repo_conflict_fetches_existing

    # #region test_create_repo_unexpected_response [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_create_repo_unexpected_response(self):
        """Non-dict response → HTTPException 500."""
        svc = TestableGitGitea()
        with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value="not a dict"):
            with pytest.raises(HTTPException, match="Unexpected"):
                await svc.create_gitea_repository("https://gitea.com", "pat", "repo")
    # #endregion test_create_repo_unexpected_response


# ── delete_gitea_repository ──

class TestDeleteGiteaRepository:
    """delete_gitea_repository — delete remote repo."""

    # #region test_delete_success [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_delete_success(self):
        """Valid owner/repo → DELETE request made."""
        svc = TestableGitGitea()
        with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value=None) as mock_req:
            await svc.delete_gitea_repository("https://gitea.com", "pat", "owner", "repo")
        mock_req.assert_called_once()
        assert mock_req.call_args[0][0] == "DELETE"
    # #endregion test_delete_success

    # #region test_delete_missing_owner [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_delete_missing_owner(self):
        """Empty owner → HTTPException 400."""
        svc = TestableGitGitea()
        with pytest.raises(HTTPException, match="required"):
            await svc.delete_gitea_repository("https://gitea.com", "pat", "", "repo")
    # #endregion test_delete_missing_owner


# ── _gitea_branch_exists ──

class TestGiteaBranchExists:
    """_gitea_branch_exists — check branch existence."""

    # #region test_branch_exists_true [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_branch_exists_true(self):
        """200 → True."""
        svc = TestableGitGitea()
        with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value={"name": "dev"}):
            result = await svc._gitea_branch_exists("https://gitea.com", "pat", "owner", "repo", "dev")
        assert result is True
    # #endregion test_branch_exists_true

    # #region test_branch_exists_404 [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_branch_exists_404(self):
        """404 → False."""
        svc = TestableGitGitea()
        with patch.object(svc, "_gitea_request", side_effect=HTTPException(status_code=404)):
            result = await svc._gitea_branch_exists("https://gitea.com", "pat", "owner", "repo", "missing")
        assert result is False
    # #endregion test_branch_exists_404

    # #region test_branch_exists_empty_params [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_branch_exists_empty_params(self):
        """Empty owner/repo/branch → False."""
        svc = TestableGitGitea()
        assert await svc._gitea_branch_exists("https://gitea.com", "pat", "", "repo", "dev") is False
        assert await svc._gitea_branch_exists("https://gitea.com", "pat", "owner", "", "dev") is False
        assert await svc._gitea_branch_exists("https://gitea.com", "pat", "owner", "repo", "") is False
    # #endregion test_branch_exists_empty_params


# ── create_gitea_pull_request ──

class TestCreateGiteaPullRequest:
    """create_gitea_pull_request — PR creation with fallback."""

    # #region test_pr_success [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_pr_success(self):
        """Successful PR → normalized metadata."""
        svc = TestableGitGitea()
        pr_data = {"number": 42, "html_url": "https://gitea.com/o/r/pulls/42", "state": "open"}
        with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value=pr_data):
            result = await svc.create_gitea_pull_request(
                "https://gitea.com", "pat", "https://gitea.com/owner/repo.git",
                "feature", "main", "Add feature",
            )
        assert result["id"] == 42
        assert result["status"] == "open"
        assert "pulls/42" in result["url"]
    # #endregion test_pr_success

    # #region test_pr_404_non_retryable [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_pr_404_non_retryable(self):
        """404 with no fallback URL → raises with branch detail."""
        svc = TestableGitGitea()
        with patch.object(svc, "_gitea_request", side_effect=HTTPException(status_code=404, detail="not found")), \
             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", "pat", "https://gitea.com/owner/repo.git",
                    "feature", "main", "PR title",
                )
    # #endregion test_pr_404_non_retryable

    # #region test_pr_unexpected_response [C:2] [TYPE Function]
    @pytest.mark.asyncio
    async def test_pr_unexpected_response(self):
        """Non-dict response → HTTPException 500."""
        svc = TestableGitGitea()
        with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value="not a dict"):
            with pytest.raises(HTTPException, match="Unexpected"):
                await svc.create_gitea_pull_request(
                    "https://gitea.com", "pat", "https://gitea.com/owner/repo.git",
                    "feature", "main", "PR title",
                )
    # #endregion test_pr_unexpected_response
# #endregion Test.Git.Gitea
