"""Tests for integrations app services with edge case coverage."""

from datetime import timedelta
from unittest.mock import MagicMock, patch

import pytest
from django.utils import timezone

from integrations.services.github_client import (
    GitHubAPIError,
    GitHubClient,
    GitHubRateLimitError,
)
from integrations.services.github_sync import GitHubSyncService, SyncStats

from .factories import GitHubInstallationFactory, GitHubRepositoryFactory


@pytest.mark.django_db
class TestGitHubClientRateLimiting:
    """Edge case tests for API rate limiting (T102)."""

    def test_rate_limit_error_is_raised_with_reset_time(self):
        """Test that rate limit error includes reset time."""
        installation = GitHubInstallationFactory()
        client = GitHubClient(installation)

        # Create a mock response with rate limit headers
        mock_response = MagicMock()
        mock_response.status_code = 403
        mock_response.headers = {
            "X-RateLimit-Limit": "5000",
            "X-RateLimit-Remaining": "0",
            "X-RateLimit-Reset": str(int((timezone.now() + timedelta(hours=1)).timestamp())),
            "X-RateLimit-Used": "5000",
        }
        mock_response.json.return_value = {"message": "API rate limit exceeded"}

        with pytest.raises(GitHubRateLimitError) as exc_info:
            client._handle_response(mock_response)

        assert exc_info.value.reset_at is not None
        assert "rate limit exceeded" in str(exc_info.value).lower()

    def test_rate_limit_error_recorded_in_sync_stats(self):
        """Test that rate limit errors are recorded in sync stats."""
        repository = GitHubRepositoryFactory()
        sync_service = GitHubSyncService(repository)

        reset_time = timezone.now() + timedelta(hours=1)

        with patch.object(sync_service.client, "list_issues") as mock_list:
            mock_list.side_effect = GitHubRateLimitError(
                "Rate limit exceeded",
                reset_at=reset_time,
            )

            # sync_issues catches exceptions and records them
            sync_service.sync_issues()

            # Verify error was recorded in stats
            assert len(sync_service.stats.errors) >= 1
            assert "rate limit" in sync_service.stats.errors[0].lower()

    def test_rate_limit_info_extracted_from_response(self):
        """Test that rate limit info is correctly extracted from headers."""
        installation = GitHubInstallationFactory()
        client = GitHubClient(installation)

        reset_timestamp = int((timezone.now() + timedelta(minutes=30)).timestamp())

        mock_response = MagicMock()
        mock_response.headers = {
            "X-RateLimit-Limit": "5000",
            "X-RateLimit-Remaining": "4500",
            "X-RateLimit-Reset": str(reset_timestamp),
            "X-RateLimit-Used": "500",
        }

        rate_limit = client._extract_rate_limit(mock_response)

        assert rate_limit is not None
        assert rate_limit.limit == 5000
        assert rate_limit.remaining == 4500
        assert rate_limit.used == 500

    def test_rate_limit_with_missing_headers_returns_none(self):
        """Test handling of missing rate limit headers."""
        installation = GitHubInstallationFactory()
        client = GitHubClient(installation)

        mock_response = MagicMock()
        mock_response.headers = {}

        rate_limit = client._extract_rate_limit(mock_response)

        # Should not raise, returns info with zeros
        assert rate_limit is not None
        assert rate_limit.limit == 0


@pytest.mark.django_db
class TestGitHubSyncPartialFailures:
    """Edge case tests for partial sync failures (T103)."""

    def test_issues_sync_failure_records_error_and_continues(self):
        """Test that issues sync failure records error and continues to next content type."""
        repository = GitHubRepositoryFactory()
        sync_service = GitHubSyncService(repository)

        with patch.object(sync_service, "_update_status"):
            with patch.object(sync_service, "_update_cursors"):
                with patch.object(sync_service.client, "list_issues") as mock_issues:
                    with patch.object(sync_service.client, "list_pull_requests") as mock_prs:
                        with patch.object(sync_service.client, "list_discussions") as mock_disc:
                            mock_issues.side_effect = Exception("Issues API failed")
                            mock_prs.return_value = []
                            mock_disc.return_value = ([], None, False)

                            # sync_issues catches exceptions and records them
                            sync_service.sync_issues()

                            # Error should be recorded in stats
                            assert len(sync_service.stats.errors) >= 1
                            assert "Issues" in sync_service.stats.errors[0]

    def test_partial_sync_records_errors_in_stats(self):
        """Test that sync errors are recorded in stats."""
        repository = GitHubRepositoryFactory()
        sync_service = GitHubSyncService(repository)

        with patch.object(sync_service.client, "list_issues") as mock_issues:
            mock_issues.side_effect = Exception("Issues API failed")

            # sync_issues catches and records the error
            sync_service.sync_issues()

            # Error should be recorded
            assert len(sync_service.stats.errors) >= 1

    def test_sync_continues_after_individual_item_failure(self):
        """Test that sync continues if individual item processing fails."""
        repository = GitHubRepositoryFactory()
        sync_service = GitHubSyncService(repository)

        # Mock issue data - one good, one that causes error
        issues = [
            {
                "number": 1,
                "title": "Good Issue",
                "body": "test",
                "state": "open",
                "html_url": "https://github.com/test/test/issues/1",
                "updated_at": "2024-01-01T00:00:00Z",
                "created_at": "2024-01-01T00:00:00Z",
                "user": {"login": "test", "id": 1, "avatar_url": ""},
                "labels": [],
            },
        ]

        with patch.object(sync_service.client, "list_issues", return_value=issues):
            count = sync_service.sync_issues()
            assert count == 1

    def test_sync_stats_track_partial_progress(self):
        """Test that sync stats accurately track partial progress."""
        stats = SyncStats()

        stats.issues_synced = 5
        stats.prs_synced = 3
        stats.errors.append("Issue 6 failed")
        stats.errors.append("PR 4 failed")

        assert stats.issues_synced == 5
        assert stats.prs_synced == 3
        assert len(stats.errors) == 2


@pytest.mark.django_db
class TestGitHubAPIErrors:
    """Edge case tests for GitHub API errors (T104)."""

    def test_api_error_with_status_code(self):
        """Test API error includes status code and response body."""
        installation = GitHubInstallationFactory()
        client = GitHubClient(installation)

        mock_response = MagicMock()
        mock_response.status_code = 404
        mock_response.headers = {}
        mock_response.json.return_value = {"message": "Not Found"}

        with pytest.raises(GitHubAPIError) as exc_info:
            client._handle_response(mock_response)

        assert exc_info.value.status_code == 404
        assert exc_info.value.response_body == {"message": "Not Found"}

    def test_api_error_with_json_decode_failure(self):
        """Test API error when response isn't valid JSON."""
        installation = GitHubInstallationFactory()
        client = GitHubClient(installation)

        mock_response = MagicMock()
        mock_response.status_code = 500
        mock_response.headers = {}
        mock_response.json.side_effect = ValueError("No JSON")

        with pytest.raises(GitHubAPIError) as exc_info:
            client._handle_response(mock_response)

        assert exc_info.value.status_code == 500
        assert exc_info.value.response_body == {}

    def test_graphql_error_handling(self):
        """Test handling of GraphQL API errors."""
        installation = GitHubInstallationFactory()
        client = GitHubClient(installation)

        # Mock successful HTTP response but with GraphQL errors
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.headers = {}
        mock_response.json.return_value = {
            "data": None,
            "errors": [{"message": "Field not found"}],
        }

        # Need to mock _get_access_token and _session
        with patch.object(client, "_get_access_token", return_value="test-token"):
            with patch.object(client._session, "post", return_value=mock_response):
                with pytest.raises(GitHubAPIError) as exc_info:
                    client.graphql("{ viewer { login } }")

                assert "GraphQL errors" in str(exc_info.value)

    def test_authentication_error_handling(self):
        """Test handling of authentication errors."""
        installation = GitHubInstallationFactory()
        client = GitHubClient(installation)

        mock_response = MagicMock()
        mock_response.status_code = 401
        mock_response.headers = {}
        mock_response.json.return_value = {"message": "Bad credentials"}

        with pytest.raises(GitHubAPIError) as exc_info:
            client._handle_response(mock_response)

        assert exc_info.value.status_code == 401

    def test_server_error_handling(self):
        """Test handling of GitHub server errors (5xx)."""
        installation = GitHubInstallationFactory()
        client = GitHubClient(installation)

        mock_response = MagicMock()
        mock_response.status_code = 502
        mock_response.headers = {}
        mock_response.json.return_value = {"message": "Bad Gateway"}

        with pytest.raises(GitHubAPIError) as exc_info:
            client._handle_response(mock_response)

        assert exc_info.value.status_code == 502

    def test_204_no_content_returns_empty_dict(self):
        """Test that 204 No Content returns empty dict."""
        installation = GitHubInstallationFactory()
        client = GitHubClient(installation)

        mock_response = MagicMock()
        mock_response.status_code = 204
        mock_response.headers = {}

        result = client._handle_response(mock_response)
        assert result == {}

    def test_sync_records_api_error_in_stats(self):
        """Test that sync records API errors in stats."""
        repository = GitHubRepositoryFactory()
        sync_service = GitHubSyncService(repository)

        with patch.object(sync_service.client, "list_issues") as mock:
            mock.side_effect = GitHubAPIError("Server Error", status_code=500)

            # sync_issues catches and records errors
            sync_service.sync_issues()

            # Verify error was recorded in stats
            assert len(sync_service.stats.errors) >= 1
            assert "Server Error" in sync_service.stats.errors[0]

    def test_sync_error_message_truncated(self):
        """Test that very long error messages are truncated by _update_status."""
        repository = GitHubRepositoryFactory()
        sync_service = GitHubSyncService(repository)

        long_error = "x" * 1000

        # Directly test _update_status truncation
        sync_service._update_status("error", long_error)

        repository.refresh_from_db()
        # Error should be truncated to 500 chars
        assert len(repository.last_sync_error) <= 500
