"""Tests for LinkedInClient service.

Tests cover:
- OAuth authorization URL generation
- State parameter generation and validation
- Token exchange with mock responses
- Token refresh functionality
- Organizations API
- Notifications API with pagination
- Error handling for various API failures
"""

import time
from unittest.mock import MagicMock, patch

import pytest
import requests
from django.conf import settings

from integrations.exceptions import (
    LinkedInAPIError,
    LinkedInAuthError,
    LinkedInInvalidStateError,
    LinkedInRateLimitError,
    LinkedInTokenExpiredError,
)
from integrations.services.linkedin_client import (
    LinkedInClient,
    LinkedInNotification,
    LinkedInOrganization,
    LinkedInTokenResponse,
)
from integrations.tests.fixtures import (
    mock_oauth_error_response,
    mock_oauth_token_response,
    mock_token_refresh_response,
)
from integrations.tests.fixtures.linkedin_notification_fixtures import (
    mock_notifications_response,
)
from integrations.tests.fixtures.linkedin_org_fixtures import (
    mock_organization_details,
)


@pytest.mark.django_db
class TestLinkedInClientInit:
    """Tests for LinkedInClient initialization."""

    def test_init_with_defaults(self):
        """Test initialization with default settings."""
        client = LinkedInClient()

        assert client.access_token is None
        assert client.client_id == settings.LINKEDIN_CLIENT_ID
        assert client.client_secret == settings.LINKEDIN_CLIENT_SECRET
        assert client.redirect_uri == settings.LINKEDIN_REDIRECT_URI

    def test_init_with_custom_values(self):
        """Test initialization with custom values."""
        client = LinkedInClient(
            access_token="test_token",
            client_id="custom_client_id",
            client_secret="custom_secret",
        )

        assert client.access_token == "test_token"
        assert client.client_id == "custom_client_id"
        assert client.client_secret == "custom_secret"


@pytest.mark.django_db
class TestLinkedInClientStateManagement:
    """Tests for OAuth state generation and validation."""

    def test_generate_state_creates_signed_state(self):
        """Test that generate_state creates a signed state string."""
        client = LinkedInClient()
        state = client.generate_state(workspace_id="123-456-789")

        assert state is not None
        assert len(state) > 0
        # State should contain a signature (colon separator)
        assert ":" in state

    def test_generate_state_unique_per_call(self):
        """Test that each state is unique."""
        client = LinkedInClient()
        state1 = client.generate_state(workspace_id="123")
        state2 = client.generate_state(workspace_id="123")

        # Same workspace_id but different nonces
        assert state1 != state2

    def test_validate_state_extracts_workspace_id(self):
        """Test that validate_state extracts the workspace_id."""
        client = LinkedInClient()
        workspace_id = "test-workspace-uuid"
        state = client.generate_state(workspace_id=workspace_id)

        extracted = client.validate_state(state=state)
        assert extracted == workspace_id

    def test_validate_state_with_expired_state(self):
        """Test that expired state raises error."""
        client = LinkedInClient()
        state = client.generate_state(workspace_id="123")

        # Validate with very short max_age
        with pytest.raises(LinkedInInvalidStateError):
            client.validate_state(state=state, max_age=-1)

    def test_validate_state_with_invalid_state(self):
        """Test that invalid state raises error."""
        client = LinkedInClient()

        with pytest.raises(LinkedInInvalidStateError):
            client.validate_state(state="invalid-state-string")

    def test_validate_state_with_tampered_state(self):
        """Test that tampered state raises error."""
        client = LinkedInClient()
        state = client.generate_state(workspace_id="123")

        # Tamper with the state
        tampered = state + "tampered"

        with pytest.raises(LinkedInInvalidStateError):
            client.validate_state(state=tampered)


@pytest.mark.django_db
class TestLinkedInClientAuthorizationURL:
    """Tests for authorization URL generation."""

    def test_get_authorization_url_format(self):
        """Test that authorization URL has correct format."""
        from urllib.parse import quote

        client = LinkedInClient()
        state = client.generate_state(workspace_id="123")
        url = client.get_authorization_url(state=state)

        assert url.startswith(settings.LINKEDIN_AUTHORIZATION_URL)
        assert "response_type=code" in url
        assert f"client_id={settings.LINKEDIN_CLIENT_ID}" in url
        # State is URL-encoded in the URL
        assert f"state={quote(state, safe='')}" in url
        assert settings.LINKEDIN_OAUTH_SCOPE in url

    def test_get_authorization_url_contains_redirect_uri(self):
        """Test that authorization URL contains redirect URI."""
        client = LinkedInClient()
        state = "test-state"
        url = client.get_authorization_url(state=state)

        # URL-encoded redirect URI should be in the URL
        assert "redirect_uri=" in url


@pytest.mark.django_db
class TestLinkedInClientTokenExchange:
    """Tests for OAuth token exchange."""

    @patch("requests.Session.post")
    def test_exchange_code_for_token_success(self, mock_post):
        """Test successful token exchange."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = mock_oauth_token_response(
            access_token="test_access_token",
            refresh_token="test_refresh_token",
            expires_in=5184000,
        )
        mock_post.return_value = mock_response

        client = LinkedInClient()
        result = client.exchange_code_for_token(code="auth_code_123")

        assert isinstance(result, LinkedInTokenResponse)
        assert result.access_token == "test_access_token"
        assert result.refresh_token == "test_refresh_token"
        assert result.expires_in == 5184000

        # Verify the post was called with correct data
        mock_post.assert_called_once()
        call_kwargs = mock_post.call_args
        assert call_kwargs.kwargs.get("data", {}).get("code") == "auth_code_123"
        assert call_kwargs.kwargs.get("data", {}).get("grant_type") == "authorization_code"

    @patch("requests.Session.post")
    def test_exchange_code_for_token_invalid_grant(self, mock_post):
        """Test token exchange with invalid grant error."""
        mock_response = MagicMock()
        mock_response.status_code = 400
        mock_response.content = b'{"error": "invalid_grant"}'
        mock_response.json.return_value = mock_oauth_error_response(
            error="invalid_grant",
            error_description="The authorization code has expired",
        )
        mock_post.return_value = mock_response

        client = LinkedInClient()

        with pytest.raises(LinkedInAuthError) as exc_info:
            client.exchange_code_for_token(code="expired_code")

        assert "expired" in str(exc_info.value).lower()

    @patch("requests.Session.post")
    def test_exchange_code_for_token_invalid_client(self, mock_post):
        """Test token exchange with invalid client error."""
        mock_response = MagicMock()
        mock_response.status_code = 400
        mock_response.content = b'{"error": "invalid_client"}'
        mock_response.json.return_value = mock_oauth_error_response(
            error="invalid_client",
            error_description="Client authentication failed",
        )
        mock_post.return_value = mock_response

        client = LinkedInClient()

        with pytest.raises(LinkedInAuthError):
            client.exchange_code_for_token(code="some_code")

    @patch("requests.Session.post")
    def test_exchange_code_for_token_network_error(self, mock_post):
        """Test token exchange with network error."""
        mock_post.side_effect = requests.RequestException("Connection failed")

        client = LinkedInClient()

        with pytest.raises(LinkedInAuthError) as exc_info:
            client.exchange_code_for_token(code="code")

        assert "request failed" in str(exc_info.value).lower()


@pytest.mark.django_db
class TestLinkedInClientTokenRefresh:
    """Tests for token refresh functionality."""

    @patch("requests.Session.post")
    def test_refresh_access_token_success(self, mock_post):
        """Test successful token refresh."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = mock_token_refresh_response(
            access_token="new_access_token",
            refresh_token="new_refresh_token",
        )
        mock_post.return_value = mock_response

        client = LinkedInClient()
        result = client.refresh_access_token(refresh_token="old_refresh_token")

        assert isinstance(result, LinkedInTokenResponse)
        assert result.access_token == "new_access_token"
        assert result.refresh_token == "new_refresh_token"

    @patch("requests.Session.post")
    def test_refresh_access_token_expired_refresh(self, mock_post):
        """Test token refresh when refresh token is expired."""
        mock_response = MagicMock()
        mock_response.status_code = 400
        mock_response.content = b'{"error": "invalid_grant"}'
        mock_response.json.return_value = mock_oauth_error_response(
            error="invalid_grant",
            error_description="Refresh token has expired",
        )
        mock_post.return_value = mock_response

        client = LinkedInClient()

        with pytest.raises(LinkedInTokenExpiredError):
            client.refresh_access_token(refresh_token="expired_refresh_token")


@pytest.mark.django_db
class TestLinkedInClientOrganizations:
    """Tests for organizations API."""

    @patch("requests.Session.request")
    def test_get_organizations_success(self, mock_request):
        """Test successful organizations fetch."""
        # First call: organizationAcls
        acl_response = MagicMock()
        acl_response.status_code = 200
        acl_response.content = b'{"elements": []}'
        acl_response.json.return_value = {
            "elements": [
                {
                    "organization": "urn:li:organization:12345",
                    "role": "ADMINISTRATOR",
                    "state": "APPROVED",
                }
            ]
        }

        # Second call: organization details
        org_response = MagicMock()
        org_response.status_code = 200
        org_response.content = b'{"id": "12345"}'
        org_response.json.return_value = mock_organization_details("acme_corp")

        mock_request.side_effect = [acl_response, org_response]

        client = LinkedInClient(access_token="test_token")
        orgs = client.get_organizations()

        assert len(orgs) == 1
        assert isinstance(orgs[0], LinkedInOrganization)
        assert orgs[0].organization_id == "12345"

    @patch("requests.Session.request")
    def test_get_organizations_empty(self, mock_request):
        """Test organizations fetch with no results."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.content = b'{"elements": []}'
        mock_response.json.return_value = {"elements": []}
        mock_request.return_value = mock_response

        client = LinkedInClient(access_token="test_token")
        orgs = client.get_organizations()

        assert orgs == []

    def test_get_organizations_no_token(self):
        """Test organizations fetch without access token."""
        client = LinkedInClient()  # No access token

        with pytest.raises(LinkedInAuthError) as exc_info:
            client.get_organizations()

        assert "no access token" in str(exc_info.value).lower()


@pytest.mark.django_db
class TestLinkedInClientNotifications:
    """Tests for notifications API."""

    @patch("requests.Session.request")
    def test_get_notifications_success(self, mock_request):
        """Test successful notifications fetch."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.content = b'{"elements": []}'
        notification_data = mock_notifications_response(
            count=3,
            organization_urn="urn:li:organization:12345",
        )
        # Adapt to the API format expected by the client
        mock_response.json.return_value = {
            "elements": [
                {
                    "notificationId": n["notificationId"],
                    "organizationalEntity": n["organizationalEntity"],
                    "action": n["action"],
                    "sourcePost": n["sourcePost"],
                    "decoratedSourcePost": n["decoratedSourcePost"],
                    "lastModifiedAt": n["lastModifiedAt"],
                    "subscriber": n["subscriber"],
                }
                for n in notification_data["notifications"]
            ],
            "paging": notification_data.get("paging", {}),
        }
        mock_request.return_value = mock_response

        client = LinkedInClient(access_token="test_token")
        notifications, next_start = client.get_notifications(organization_urn="urn:li:organization:12345")

        assert len(notifications) == 3
        assert all(isinstance(n, LinkedInNotification) for n in notifications)

    @patch("requests.Session.request")
    def test_get_notifications_empty(self, mock_request):
        """Test notifications fetch with empty response."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.content = b'{"elements": []}'
        mock_response.json.return_value = {"elements": [], "paging": {"total": 0}}
        mock_request.return_value = mock_response

        client = LinkedInClient(access_token="test_token")
        notifications, next_start = client.get_notifications(organization_urn="urn:li:organization:12345")

        assert notifications == []
        assert next_start is None

    @patch("requests.Session.request")
    def test_get_notifications_pagination(self, mock_request):
        """Test notifications pagination."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.content = b'{"elements": []}'
        mock_response.json.return_value = {
            "elements": [
                {
                    "notificationId": i,
                    "organizationalEntity": "urn:li:organization:12345",
                    "action": "SHARE_MENTION",
                    "sourcePost": f"urn:li:activity:{i}",
                    "decoratedSourcePost": {
                        "entity": f"urn:li:share:{i}",
                        "owner": f"urn:li:person:user{i}",
                        "text": f"Test post {i}",
                        "landingPageUrl": f"https://linkedin.com/post/{i}",
                    },
                    "lastModifiedAt": int(time.time() * 1000),
                }
                for i in range(10)
            ],
            "paging": {"total": 25, "start": 0, "count": 10},
        }
        mock_request.return_value = mock_response

        client = LinkedInClient(access_token="test_token")
        notifications, next_start = client.get_notifications(
            organization_urn="urn:li:organization:12345",
            start=0,
            count=10,
        )

        assert len(notifications) == 10
        # There are more results (25 total), so next_start should be set
        # next_start = 0 + 10 = 10 (since 10 < 25)

    @patch("requests.Session.request")
    def test_get_notifications_with_changed_since(self, mock_request):
        """Test notifications fetch with changedSince parameter."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.content = b'{"elements": []}'
        mock_response.json.return_value = {"elements": [], "paging": {"total": 0}}
        mock_request.return_value = mock_response

        client = LinkedInClient(access_token="test_token")
        changed_since = int(time.time() * 1000) - 86400000  # 24 hours ago

        client.get_notifications(
            organization_urn="urn:li:organization:12345",
            changed_since=changed_since,
        )

        # Verify changedSince was passed as a parameter
        call_kwargs = mock_request.call_args
        params = call_kwargs.kwargs.get("params", {})
        assert params.get("changedSince") == changed_since


@pytest.mark.django_db
class TestLinkedInClientAPIErrors:
    """Tests for API error handling."""

    @patch("requests.Session.request")
    def test_rate_limit_error(self, mock_request):
        """Test handling of rate limit response."""
        mock_response = MagicMock()
        mock_response.status_code = 429
        mock_response.headers = {"Retry-After": "60"}
        mock_request.return_value = mock_response

        client = LinkedInClient(access_token="test_token")

        with pytest.raises(LinkedInRateLimitError) as exc_info:
            client.get_organizations()

        assert exc_info.value.retry_after == 60

    @patch("requests.Session.request")
    def test_token_expired_error(self, mock_request):
        """Test handling of 401 unauthorized response."""
        mock_response = MagicMock()
        mock_response.status_code = 401
        mock_request.return_value = mock_response

        client = LinkedInClient(access_token="test_token")

        with pytest.raises(LinkedInTokenExpiredError):
            client.get_organizations()

    @patch("requests.Session.request")
    def test_forbidden_error(self, mock_request):
        """Test handling of 403 forbidden response."""
        mock_response = MagicMock()
        mock_response.status_code = 403
        mock_request.return_value = mock_response

        client = LinkedInClient(access_token="test_token")

        with pytest.raises(LinkedInAuthError) as exc_info:
            client.get_organizations()

        assert "permission" in str(exc_info.value).lower()

    @patch("requests.Session.request")
    def test_generic_api_error(self, mock_request):
        """Test handling of generic API errors."""
        mock_response = MagicMock()
        mock_response.status_code = 500
        mock_response.content = b'{"message": "Internal server error"}'
        mock_response.json.return_value = {"message": "Internal server error"}
        mock_request.return_value = mock_response

        client = LinkedInClient(access_token="test_token")

        with pytest.raises(LinkedInAPIError) as exc_info:
            client.get_organizations()

        assert exc_info.value.status_code == 500

    @patch("requests.Session.request")
    def test_network_error(self, mock_request):
        """Test handling of network errors."""
        mock_request.side_effect = requests.RequestException("Connection timeout")

        client = LinkedInClient(access_token="test_token")

        with pytest.raises(LinkedInAPIError) as exc_info:
            client.get_organizations()

        assert "request failed" in str(exc_info.value).lower()


@pytest.mark.django_db
class TestLinkedInClientGetAllNotifications:
    """Tests for get_all_notifications with automatic pagination."""

    @patch("requests.Session.request")
    def test_get_all_notifications_single_page(self, mock_request):
        """Test get_all_notifications when all results fit in one page."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.content = b'{"elements": []}'
        mock_response.json.return_value = {
            "elements": [
                {
                    "notificationId": i,
                    "organizationalEntity": "urn:li:organization:12345",
                    "action": "SHARE_MENTION",
                    "sourcePost": f"urn:li:activity:{i}",
                    "decoratedSourcePost": {
                        "entity": f"urn:li:share:{i}",
                        "owner": f"urn:li:person:user{i}",
                        "text": f"Test post {i}",
                        "landingPageUrl": f"https://linkedin.com/post/{i}",
                    },
                    "lastModifiedAt": int(time.time() * 1000),
                }
                for i in range(5)
            ],
            "paging": {"total": 5, "start": 0, "count": 50},
        }
        mock_request.return_value = mock_response

        client = LinkedInClient(access_token="test_token")
        all_notifications = client.get_all_notifications(organization_urn="urn:li:organization:12345")

        assert len(all_notifications) == 5
        # Should only make one API call since all results fit in one page
        assert mock_request.call_count == 1

    @patch("requests.Session.request")
    def test_get_all_notifications_multiple_pages(self, mock_request):
        """Test get_all_notifications with multiple pages."""

        # Set up responses for multiple pages
        def create_page_response(start: int, count: int, total: int):
            mock_resp = MagicMock()
            mock_resp.status_code = 200
            mock_resp.content = b'{"elements": []}'
            mock_resp.json.return_value = {
                "elements": [
                    {
                        "notificationId": start + i,
                        "organizationalEntity": "urn:li:organization:12345",
                        "action": "SHARE_MENTION",
                        "sourcePost": f"urn:li:activity:{start + i}",
                        "decoratedSourcePost": {
                            "entity": f"urn:li:share:{start + i}",
                            "owner": f"urn:li:person:user{start + i}",
                            "text": f"Test post {start + i}",
                            "landingPageUrl": f"https://linkedin.com/post/{start + i}",
                        },
                        "lastModifiedAt": int(time.time() * 1000),
                    }
                    for i in range(min(count, total - start))
                ],
                "paging": {"total": total, "start": start, "count": count},
            }
            return mock_resp

        # Simulate 2 pages of 50 results each, total 75 results
        mock_request.side_effect = [
            create_page_response(0, 50, 75),
            create_page_response(50, 50, 75),
        ]

        client = LinkedInClient(access_token="test_token")
        all_notifications = client.get_all_notifications(organization_urn="urn:li:organization:12345")

        assert len(all_notifications) == 75
        # Should make 2 API calls for pagination
        assert mock_request.call_count == 2
