"""
Tests for git provider implementations.
"""

from unittest.mock import MagicMock

import pytest

from exceptions import APIError, AuthenticationError
from models import MergeStrategy, PRState
from providers.gitea import GiteaProvider


class TestBaseProvider:
    """Tests for BaseProvider interface."""

    def test_parse_repo_url_https(self, mock_gitea_provider):
        """Test parsing HTTPS repo URL."""
        # The mock needs parse_repo_url to work
        provider = GiteaProvider(base_url="https://gitea.test.com", token="test-token")

        owner, repo = provider.parse_repo_url("https://gitea.test.com/owner/repo.git")

        assert owner == "owner"
        assert repo == "repo"

    def test_parse_repo_url_https_no_git(self):
        """Test parsing HTTPS URL without .git suffix."""
        provider = GiteaProvider(base_url="https://gitea.test.com", token="test-token")

        owner, repo = provider.parse_repo_url("https://gitea.test.com/owner/repo")

        assert owner == "owner"
        assert repo == "repo"

    def test_parse_repo_url_ssh(self):
        """Test parsing SSH repo URL."""
        provider = GiteaProvider(base_url="https://gitea.test.com", token="test-token")

        owner, repo = provider.parse_repo_url("git@gitea.test.com:owner/repo.git")

        assert owner == "owner"
        assert repo == "repo"

    def test_parse_repo_url_invalid(self):
        """Test error on invalid URL."""
        provider = GiteaProvider(base_url="https://gitea.test.com", token="test-token")

        with pytest.raises(ValueError, match="Unable to parse"):
            provider.parse_repo_url("invalid-url")


class TestGiteaProvider:
    """Tests for GiteaProvider."""

    @pytest.mark.asyncio
    async def test_is_connected(self, gitea_provider, mock_httpx_client):
        """Test connection check."""
        mock_httpx_client.request.return_value.json = MagicMock(
            return_value={"login": "test-user"}
        )

        result = await gitea_provider.is_connected()

        assert result is True

    @pytest.mark.asyncio
    async def test_is_connected_no_token(self, test_settings):
        """Test connection fails without token."""
        provider = GiteaProvider(
            base_url="https://gitea.test.com",
            token="",
            settings=test_settings,
        )

        result = await provider.is_connected()
        assert result is False

        await provider.close()

    @pytest.mark.asyncio
    async def test_get_authenticated_user(self, gitea_provider, mock_httpx_client):
        """Test getting authenticated user."""
        mock_httpx_client.request.return_value.json = MagicMock(
            return_value={"login": "test-user"}
        )

        user = await gitea_provider.get_authenticated_user()

        assert user == "test-user"

    @pytest.mark.asyncio
    async def test_get_repo_info(self, gitea_provider, mock_httpx_client):
        """Test getting repository info."""
        mock_httpx_client.request.return_value.json = MagicMock(
            return_value={
                "name": "repo",
                "full_name": "owner/repo",
                "default_branch": "main",
            }
        )

        result = await gitea_provider.get_repo_info("owner", "repo")

        assert result["name"] == "repo"
        assert result["default_branch"] == "main"

    @pytest.mark.asyncio
    async def test_get_default_branch(self, gitea_provider, mock_httpx_client):
        """Test getting default branch."""
        mock_httpx_client.request.return_value.json = MagicMock(
            return_value={"default_branch": "develop"}
        )

        branch = await gitea_provider.get_default_branch("owner", "repo")

        assert branch == "develop"


class TestGiteaPROperations:
    """Tests for Gitea PR operations."""

    @pytest.mark.asyncio
    async def test_create_pr(self, gitea_provider, mock_httpx_client):
        """Test creating a pull request."""
        mock_httpx_client.request.return_value.json = MagicMock(
            return_value={
                "number": 42,
                "html_url": "https://gitea.test.com/owner/repo/pull/42",
            }
        )

        result = await gitea_provider.create_pr(
            owner="owner",
            repo="repo",
            title="Test PR",
            body="Test body",
            source_branch="feature",
            target_branch="main",
        )

        assert result.success is True
        assert result.pr_number == 42
        assert result.pr_url == "https://gitea.test.com/owner/repo/pull/42"

    @pytest.mark.asyncio
    async def test_create_pr_with_options(self, gitea_provider, mock_httpx_client):
        """Test creating PR with labels, assignees, reviewers."""
        # Use side_effect for multiple API calls:
        # 1. POST create PR
        # 2. GET labels (for "enhancement") - in add_labels -> _get_or_create_label
        # 3. POST add labels to PR - in add_labels
        # 4. GET issue to return labels - in add_labels
        # 5. PATCH add assignees
        # 6. POST request reviewers
        mock_responses = [
            {
                "number": 43,
                "html_url": "https://gitea.test.com/owner/repo/pull/43",
            },  # Create PR
            [{"id": 1, "name": "enhancement"}],  # GET labels (found)
            {},  # POST add labels to PR
            {"labels": [{"name": "enhancement"}]},  # GET issue to return current labels
            {},  # PATCH add assignees
            {},  # POST request reviewers
        ]
        mock_httpx_client.request.return_value.json = MagicMock(
            side_effect=mock_responses
        )

        result = await gitea_provider.create_pr(
            owner="owner",
            repo="repo",
            title="Test PR",
            body="Test body",
            source_branch="feature",
            target_branch="main",
            labels=["enhancement"],
            assignees=["user1"],
            reviewers=["reviewer1"],
        )

        assert result.success is True

    @pytest.mark.asyncio
    async def test_get_pr(self, gitea_provider, mock_httpx_client, sample_pr_data):
        """Test getting a pull request."""
        mock_httpx_client.request.return_value.json = MagicMock(
            return_value=sample_pr_data
        )

        result = await gitea_provider.get_pr("owner", "repo", 42)

        assert result.success is True
        assert result.pr["number"] == 42
        assert result.pr["title"] == "Test PR"

    @pytest.mark.asyncio
    async def test_get_pr_not_found(self, gitea_provider, mock_httpx_client):
        """Test getting non-existent PR."""
        mock_httpx_client.request.return_value.status_code = 404
        mock_httpx_client.request.return_value.json = MagicMock(return_value=None)

        result = await gitea_provider.get_pr("owner", "repo", 999)

        assert result.success is False

    @pytest.mark.asyncio
    async def test_list_prs(self, gitea_provider, mock_httpx_client, sample_pr_data):
        """Test listing pull requests."""
        mock_httpx_client.request.return_value.json = MagicMock(
            return_value=[sample_pr_data, sample_pr_data]
        )

        result = await gitea_provider.list_prs("owner", "repo")

        assert result.success is True
        assert len(result.pull_requests) == 2

    @pytest.mark.asyncio
    async def test_list_prs_with_state_filter(
        self, gitea_provider, mock_httpx_client, sample_pr_data
    ):
        """Test listing PRs with state filter."""
        mock_httpx_client.request.return_value.json = MagicMock(
            return_value=[sample_pr_data]
        )

        result = await gitea_provider.list_prs("owner", "repo", state=PRState.OPEN)

        assert result.success is True

    @pytest.mark.asyncio
    async def test_merge_pr(self, gitea_provider, mock_httpx_client):
        """Test merging a pull request."""
        # First call returns merge result
        mock_httpx_client.request.return_value.json = MagicMock(
            return_value={"sha": "merge-commit-sha"}
        )

        result = await gitea_provider.merge_pr(
            "owner",
            "repo",
            42,
            merge_strategy=MergeStrategy.SQUASH,
        )

        assert result.success is True
        assert result.merge_commit_sha == "merge-commit-sha"

    @pytest.mark.asyncio
    async def test_update_pr(self, gitea_provider, mock_httpx_client, sample_pr_data):
        """Test updating a pull request."""
        mock_httpx_client.request.return_value.json = MagicMock(
            return_value=sample_pr_data
        )

        result = await gitea_provider.update_pr(
            "owner",
            "repo",
            42,
            title="Updated Title",
            body="Updated body",
        )

        assert result.success is True

    @pytest.mark.asyncio
    async def test_close_pr(self, gitea_provider, mock_httpx_client, sample_pr_data):
        """Test closing a pull request."""
        sample_pr_data["state"] = "closed"
        mock_httpx_client.request.return_value.json = MagicMock(
            return_value=sample_pr_data
        )

        result = await gitea_provider.close_pr("owner", "repo", 42)

        assert result.success is True


class TestGiteaBranchOperations:
    """Tests for Gitea branch operations."""

    @pytest.mark.asyncio
    async def test_get_branch(self, gitea_provider, mock_httpx_client):
        """Test getting branch info."""
        mock_httpx_client.request.return_value.json = MagicMock(
            return_value={
                "name": "main",
                "commit": {"sha": "abc123"},
            }
        )

        result = await gitea_provider.get_branch("owner", "repo", "main")

        assert result["name"] == "main"

    @pytest.mark.asyncio
    async def test_delete_remote_branch(self, gitea_provider, mock_httpx_client):
        """Test deleting a remote branch."""
        mock_httpx_client.request.return_value.status_code = 204

        result = await gitea_provider.delete_remote_branch(
            "owner", "repo", "old-branch"
        )

        assert result is True


class TestGiteaCommentOperations:
    """Tests for Gitea comment operations."""

    @pytest.mark.asyncio
    async def test_add_pr_comment(self, gitea_provider, mock_httpx_client):
        """Test adding a comment to a PR."""
        mock_httpx_client.request.return_value.json = MagicMock(
            return_value={"id": 1, "body": "Test comment"}
        )

        result = await gitea_provider.add_pr_comment(
            "owner", "repo", 42, "Test comment"
        )

        assert result["body"] == "Test comment"

    @pytest.mark.asyncio
    async def test_list_pr_comments(self, gitea_provider, mock_httpx_client):
        """Test listing PR comments."""
        mock_httpx_client.request.return_value.json = MagicMock(
            return_value=[
                {"id": 1, "body": "Comment 1"},
                {"id": 2, "body": "Comment 2"},
            ]
        )

        result = await gitea_provider.list_pr_comments("owner", "repo", 42)

        assert len(result) == 2


class TestGiteaLabelOperations:
    """Tests for Gitea label operations."""

    @pytest.mark.asyncio
    async def test_add_labels(self, gitea_provider, mock_httpx_client):
        """Test adding labels to a PR."""
        # Use side_effect to return different values for different calls
        # 1. GET labels (for "bug") - returns existing labels
        # 2. POST to create "bug" label
        # 3. GET labels (for "urgent")
        # 4. POST to create "urgent" label
        # 5. POST labels to PR
        # 6. GET issue to return final labels
        mock_responses = [
            [{"id": 1, "name": "existing"}],  # GET labels (bug not found)
            {"id": 2, "name": "bug"},  # POST create bug
            [
                {"id": 1, "name": "existing"},
                {"id": 2, "name": "bug"},
            ],  # GET labels (urgent not found)
            {"id": 3, "name": "urgent"},  # POST create urgent
            {},  # POST add labels to PR
            {"labels": [{"name": "bug"}, {"name": "urgent"}]},  # GET issue
        ]
        mock_httpx_client.request.return_value.json = MagicMock(
            side_effect=mock_responses
        )

        result = await gitea_provider.add_labels("owner", "repo", 42, ["bug", "urgent"])

        # Should return updated label list
        assert isinstance(result, list)

    @pytest.mark.asyncio
    async def test_remove_label(self, gitea_provider, mock_httpx_client):
        """Test removing a label from a PR."""
        # Use side_effect for multiple calls
        # 1. GET labels to find the label ID
        # 2. DELETE the label from the PR
        # 3. GET issue to return remaining labels
        mock_responses = [
            [{"id": 1, "name": "bug"}],  # GET labels
            {},  # DELETE label
            {"labels": []},  # GET issue
        ]
        mock_httpx_client.request.return_value.json = MagicMock(
            side_effect=mock_responses
        )

        result = await gitea_provider.remove_label("owner", "repo", 42, "bug")

        assert isinstance(result, list)


class TestGiteaReviewerOperations:
    """Tests for Gitea reviewer operations."""

    @pytest.mark.asyncio
    async def test_request_review(self, gitea_provider, mock_httpx_client):
        """Test requesting review from users."""
        mock_httpx_client.request.return_value.json = MagicMock(return_value={})

        result = await gitea_provider.request_review(
            "owner", "repo", 42, ["reviewer1", "reviewer2"]
        )

        assert result == ["reviewer1", "reviewer2"]


class TestGiteaErrorHandling:
    """Tests for error handling in Gitea provider."""

    @pytest.mark.asyncio
    async def test_authentication_error(self, gitea_provider, mock_httpx_client):
        """Test handling authentication errors."""
        mock_httpx_client.request.return_value.status_code = 401

        with pytest.raises(AuthenticationError):
            await gitea_provider._request("GET", "/user")

    @pytest.mark.asyncio
    async def test_permission_denied(self, gitea_provider, mock_httpx_client):
        """Test handling permission denied errors."""
        mock_httpx_client.request.return_value.status_code = 403

        with pytest.raises(AuthenticationError, match="Insufficient permissions"):
            await gitea_provider._request("GET", "/protected")

    @pytest.mark.asyncio
    async def test_api_error(self, gitea_provider, mock_httpx_client):
        """Test handling general API errors."""
        mock_httpx_client.request.return_value.status_code = 500
        mock_httpx_client.request.return_value.text = "Internal Server Error"
        mock_httpx_client.request.return_value.json = MagicMock(
            return_value={"message": "Server error"}
        )

        with pytest.raises(APIError):
            await gitea_provider._request("GET", "/error")


class TestGiteaPRParsing:
    """Tests for PR data parsing."""

    def test_parse_pr_open(self, gitea_provider, sample_pr_data):
        """Test parsing open PR."""
        pr_info = gitea_provider._parse_pr(sample_pr_data)

        assert pr_info.number == 42
        assert pr_info.state == PRState.OPEN
        assert pr_info.title == "Test PR"
        assert pr_info.source_branch == "feature-branch"
        assert pr_info.target_branch == "main"

    def test_parse_pr_merged(self, gitea_provider, sample_pr_data):
        """Test parsing merged PR."""
        sample_pr_data["merged"] = True
        sample_pr_data["merged_at"] = "2024-01-16T10:00:00Z"

        pr_info = gitea_provider._parse_pr(sample_pr_data)

        assert pr_info.state == PRState.MERGED

    def test_parse_pr_closed(self, gitea_provider, sample_pr_data):
        """Test parsing closed PR."""
        sample_pr_data["state"] = "closed"
        sample_pr_data["closed_at"] = "2024-01-16T10:00:00Z"

        pr_info = gitea_provider._parse_pr(sample_pr_data)

        assert pr_info.state == PRState.CLOSED

    def test_parse_datetime_iso(self, gitea_provider):
        """Test parsing ISO datetime strings."""
        dt = gitea_provider._parse_datetime("2024-01-15T10:30:00Z")

        assert dt.year == 2024
        assert dt.month == 1
        assert dt.day == 15

    def test_parse_datetime_none(self, gitea_provider):
        """Test parsing None datetime returns now."""
        dt = gitea_provider._parse_datetime(None)

        assert dt is not None
        assert dt.tzinfo is not None
