"""
Tests for GitHub provider implementation.
"""

from unittest.mock import MagicMock

import pytest

from exceptions import APIError, AuthenticationError
from models import MergeStrategy, PRState
from providers.github import GitHubProvider


class TestGitHubProviderBasics:
    """Tests for GitHubProvider basic operations."""

    def test_provider_name(self):
        """Test provider name is github."""
        provider = GitHubProvider(token="test-token")
        assert provider.name == "github"

    def test_parse_repo_url_https(self):
        """Test parsing HTTPS repo URL."""
        provider = GitHubProvider(token="test-token")

        owner, repo = provider.parse_repo_url("https://github.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 = GitHubProvider(token="test-token")

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

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

    def test_parse_repo_url_ssh(self):
        """Test parsing SSH repo URL."""
        provider = GitHubProvider(token="test-token")

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

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

    def test_parse_repo_url_invalid(self):
        """Test error on invalid URL."""
        provider = GitHubProvider(token="test-token")

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


@pytest.fixture
def mock_github_httpx_client():
    """Create a mock httpx client for GitHub provider tests."""
    from unittest.mock import AsyncMock

    mock_response = AsyncMock()
    mock_response.status_code = 200
    mock_response.json = MagicMock(return_value={})
    mock_response.text = ""

    mock_client = AsyncMock()
    mock_client.request = AsyncMock(return_value=mock_response)
    mock_client.get = AsyncMock(return_value=mock_response)
    mock_client.post = AsyncMock(return_value=mock_response)
    mock_client.patch = AsyncMock(return_value=mock_response)
    mock_client.put = AsyncMock(return_value=mock_response)
    mock_client.delete = AsyncMock(return_value=mock_response)

    return mock_client


@pytest.fixture
async def github_provider(test_settings, mock_github_httpx_client):
    """Create a GitHubProvider with mocked HTTP client."""
    provider = GitHubProvider(
        token=test_settings.github_token,
        settings=test_settings,
    )
    provider._client = mock_github_httpx_client

    yield provider

    await provider.close()


@pytest.fixture
def github_pr_data():
    """Sample PR data from GitHub API."""
    return {
        "number": 42,
        "title": "Test PR",
        "body": "This is a test pull request",
        "state": "open",
        "head": {"ref": "feature-branch"},
        "base": {"ref": "main"},
        "user": {"login": "test-user"},
        "created_at": "2024-01-15T10:00:00Z",
        "updated_at": "2024-01-15T12:00:00Z",
        "merged_at": None,
        "closed_at": None,
        "html_url": "https://github.com/owner/repo/pull/42",
        "labels": [{"name": "enhancement"}],
        "assignees": [{"login": "assignee1"}],
        "requested_reviewers": [{"login": "reviewer1"}],
        "mergeable": True,
        "draft": False,
    }


class TestGitHubProviderConnection:
    """Tests for GitHub provider connection."""

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

        result = await github_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 = GitHubProvider(
            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, github_provider, mock_github_httpx_client
    ):
        """Test getting authenticated user."""
        mock_github_httpx_client.request.return_value.json = MagicMock(
            return_value={"login": "test-user"}
        )

        user = await github_provider.get_authenticated_user()

        assert user == "test-user"


class TestGitHubProviderRepoOperations:
    """Tests for GitHub repository operations."""

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

        result = await github_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, github_provider, mock_github_httpx_client):
        """Test getting default branch."""
        mock_github_httpx_client.request.return_value.json = MagicMock(
            return_value={"default_branch": "develop"}
        )

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

        assert branch == "develop"


class TestGitHubPROperations:
    """Tests for GitHub PR operations."""

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

        result = await github_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://github.com/owner/repo/pull/42"

    @pytest.mark.asyncio
    async def test_create_pr_with_draft(
        self, github_provider, mock_github_httpx_client
    ):
        """Test creating a draft PR."""
        mock_github_httpx_client.request.return_value.json = MagicMock(
            return_value={
                "number": 43,
                "html_url": "https://github.com/owner/repo/pull/43",
            }
        )

        result = await github_provider.create_pr(
            owner="owner",
            repo="repo",
            title="Draft PR",
            body="Draft body",
            source_branch="feature",
            target_branch="main",
            draft=True,
        )

        assert result.success is True
        assert result.pr_number == 43

    @pytest.mark.asyncio
    async def test_create_pr_with_options(
        self, github_provider, mock_github_httpx_client
    ):
        """Test creating PR with labels, assignees, reviewers."""
        mock_responses = [
            {
                "number": 44,
                "html_url": "https://github.com/owner/repo/pull/44",
            },  # Create PR
            [{"name": "enhancement"}],  # POST add labels
            {},  # POST add assignees
            {},  # POST request reviewers
        ]
        mock_github_httpx_client.request.return_value.json = MagicMock(
            side_effect=mock_responses
        )

        result = await github_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, github_provider, mock_github_httpx_client, github_pr_data
    ):
        """Test getting a pull request."""
        mock_github_httpx_client.request.return_value.json = MagicMock(
            return_value=github_pr_data
        )

        result = await github_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, github_provider, mock_github_httpx_client):
        """Test getting non-existent PR."""
        mock_github_httpx_client.request.return_value.status_code = 404
        mock_github_httpx_client.request.return_value.json = MagicMock(
            return_value=None
        )

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

        assert result.success is False

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

        result = await github_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, github_provider, mock_github_httpx_client, github_pr_data
    ):
        """Test listing PRs with state filter."""
        mock_github_httpx_client.request.return_value.json = MagicMock(
            return_value=[github_pr_data]
        )

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

        assert result.success is True

    @pytest.mark.asyncio
    async def test_merge_pr(
        self, github_provider, mock_github_httpx_client, github_pr_data
    ):
        """Test merging a pull request."""
        # Merge returns sha, then get_pr returns the PR data, then delete branch
        mock_responses = [
            {"sha": "merge-commit-sha", "merged": True},  # PUT merge
            github_pr_data,  # GET PR for branch info
            None,  # DELETE branch
        ]
        mock_github_httpx_client.request.return_value.json = MagicMock(
            side_effect=mock_responses
        )

        result = await github_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_merge_pr_rebase(
        self, github_provider, mock_github_httpx_client, github_pr_data
    ):
        """Test merging with rebase strategy."""
        mock_responses = [
            {"sha": "rebase-commit-sha", "merged": True},  # PUT merge
            github_pr_data,  # GET PR for branch info
            None,  # DELETE branch
        ]
        mock_github_httpx_client.request.return_value.json = MagicMock(
            side_effect=mock_responses
        )

        result = await github_provider.merge_pr(
            "owner",
            "repo",
            42,
            merge_strategy=MergeStrategy.REBASE,
        )

        assert result.success is True

    @pytest.mark.asyncio
    async def test_update_pr(
        self, github_provider, mock_github_httpx_client, github_pr_data
    ):
        """Test updating a pull request."""
        mock_github_httpx_client.request.return_value.json = MagicMock(
            return_value=github_pr_data
        )

        result = await github_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, github_provider, mock_github_httpx_client, github_pr_data
    ):
        """Test closing a pull request."""
        github_pr_data["state"] = "closed"
        mock_github_httpx_client.request.return_value.json = MagicMock(
            return_value=github_pr_data
        )

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

        assert result.success is True


class TestGitHubBranchOperations:
    """Tests for GitHub branch operations."""

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

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

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

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

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

        assert result is True


class TestGitHubCommentOperations:
    """Tests for GitHub comment operations."""

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

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

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

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

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

        assert len(result) == 2


class TestGitHubLabelOperations:
    """Tests for GitHub label operations."""

    @pytest.mark.asyncio
    async def test_add_labels(self, github_provider, mock_github_httpx_client):
        """Test adding labels to a PR."""
        mock_github_httpx_client.request.return_value.json = MagicMock(
            return_value=[{"name": "bug"}, {"name": "urgent"}]
        )

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

        assert "bug" in result
        assert "urgent" in result

    @pytest.mark.asyncio
    async def test_remove_label(self, github_provider, mock_github_httpx_client):
        """Test removing a label from a PR."""
        mock_responses = [
            None,  # DELETE label
            {"labels": []},  # GET issue
        ]
        mock_github_httpx_client.request.return_value.json = MagicMock(
            side_effect=mock_responses
        )

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

        assert isinstance(result, list)


class TestGitHubReviewerOperations:
    """Tests for GitHub reviewer operations."""

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

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

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


class TestGitHubErrorHandling:
    """Tests for error handling in GitHub provider."""

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

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

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

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

    @pytest.mark.asyncio
    async def test_rate_limit_error(self, github_provider, mock_github_httpx_client):
        """Test handling rate limit errors."""
        mock_github_httpx_client.request.return_value.status_code = 403
        mock_github_httpx_client.request.return_value.text = "API rate limit exceeded"

        with pytest.raises(APIError, match="rate limit"):
            await github_provider._request("GET", "/user")

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

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


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

    def test_parse_pr_open(self, github_provider, github_pr_data):
        """Test parsing open PR."""
        pr_info = github_provider._parse_pr(github_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, github_provider, github_pr_data):
        """Test parsing merged PR."""
        github_pr_data["merged_at"] = "2024-01-16T10:00:00Z"

        pr_info = github_provider._parse_pr(github_pr_data)

        assert pr_info.state == PRState.MERGED

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

        pr_info = github_provider._parse_pr(github_pr_data)

        assert pr_info.state == PRState.CLOSED

    def test_parse_pr_draft(self, github_provider, github_pr_data):
        """Test parsing draft PR."""
        github_pr_data["draft"] = True

        pr_info = github_provider._parse_pr(github_pr_data)

        assert pr_info.draft is True

    def test_parse_datetime_iso(self, github_provider):
        """Test parsing ISO datetime strings."""
        dt = github_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, github_provider):
        """Test parsing None datetime returns now."""
        dt = github_provider._parse_datetime(None)

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

    def test_parse_pr_with_null_body(self, github_provider, github_pr_data):
        """Test parsing PR with null body."""
        github_pr_data["body"] = None

        pr_info = github_provider._parse_pr(github_pr_data)

        assert pr_info.body == ""
