"""Tests for LinkedIn OAuth callback views.

Tests cover:
- OAuth callback exchanges code for tokens
- OAuth callback handles invalid state
- OAuth callback handles OAuth errors from LinkedIn
- OAuth callback stores tokens temporarily for org selection
"""

from unittest.mock import MagicMock, patch

import pytest
from django.core.cache import cache
from django.test import Client, override_settings
from django.urls import reverse

from accounts.tests.factories import WorkspaceFactory
from integrations.services.linkedin_client import LinkedInClient


@pytest.fixture
def client():
    """Create a Django test client."""
    return Client()


@pytest.fixture
def workspace():
    """Create a test workspace."""
    return WorkspaceFactory()


@pytest.fixture
def valid_state(workspace):
    """Generate a valid OAuth state for the workspace."""
    linkedin_client = LinkedInClient()
    return linkedin_client.generate_state(workspace_id=str(workspace.pk))


@pytest.mark.django_db
class TestLinkedInOAuthCallback:
    """Tests for the LinkedIn OAuth callback view."""

    def setup_method(self):
        """Clear cache before each test."""
        cache.clear()

    @override_settings(CORS_ALLOWED_ORIGINS=["http://localhost:3000"])
    @patch("integrations.views.LinkedInClient")
    def test_callback_exchanges_code_for_tokens(self, mock_client_class, client, workspace, valid_state):
        """Test successful OAuth callback exchanges code and stores tokens."""
        # Setup mock client
        mock_client = MagicMock()
        mock_client_class.return_value = mock_client

        # Mock token response with all required fields
        mock_token_response = MagicMock()
        mock_token_response.access_token = "test_access_token"
        mock_token_response.refresh_token = "test_refresh_token"
        mock_token_response.expires_in = 5184000
        mock_token_response.refresh_token_expires_in = 31536000
        mock_client.exchange_code_for_token.return_value = mock_token_response
        mock_client.validate_state.return_value = str(workspace.pk)

        url = reverse("integrations:linkedin_callback")
        response = client.get(url, {"code": "auth_code_123", "state": valid_state})

        # Should redirect to frontend
        assert response.status_code == 302
        assert "linkedin=authorized" in response.url

        # Verify token exchange was called
        mock_client.exchange_code_for_token.assert_called_once_with(code="auth_code_123")

    @override_settings(CORS_ALLOWED_ORIGINS=["http://localhost:3000"])
    @patch("integrations.views.LinkedInClient")
    def test_callback_handles_invalid_state(self, mock_client_class, client):
        """Test callback handles invalid state parameter."""
        mock_client = MagicMock()
        mock_client_class.return_value = mock_client

        from integrations.exceptions import LinkedInInvalidStateError

        mock_client.validate_state.side_effect = LinkedInInvalidStateError("Invalid state")

        url = reverse("integrations:linkedin_callback")
        response = client.get(url, {"code": "auth_code_123", "state": "invalid_state"})

        # Should redirect with error
        assert response.status_code == 302
        assert "error=invalid_state" in response.url

    @override_settings(CORS_ALLOWED_ORIGINS=["http://localhost:3000"])
    @patch("integrations.views.LinkedInClient")
    def test_callback_handles_expired_state(self, mock_client_class, client):
        """Test callback handles expired state parameter."""
        mock_client = MagicMock()
        mock_client_class.return_value = mock_client

        from integrations.exceptions import LinkedInInvalidStateError

        mock_client.validate_state.side_effect = LinkedInInvalidStateError("State expired")

        url = reverse("integrations:linkedin_callback")
        response = client.get(url, {"code": "auth_code", "state": "expired_state"})

        assert response.status_code == 302
        assert "error=invalid_state" in response.url

    @override_settings(CORS_ALLOWED_ORIGINS=["http://localhost:3000"])
    @patch("integrations.views.LinkedInClient")
    def test_callback_handles_oauth_error(self, mock_client_class, client, workspace, valid_state):
        """Test callback handles OAuth error from LinkedIn."""
        mock_client = MagicMock()
        mock_client_class.return_value = mock_client
        mock_client.validate_state.return_value = str(workspace.pk)

        from integrations.exceptions import LinkedInAuthError

        mock_client.exchange_code_for_token.side_effect = LinkedInAuthError(
            "Authorization code expired",
            error_code="invalid_grant",
        )

        url = reverse("integrations:linkedin_callback")
        response = client.get(url, {"code": "expired_code", "state": valid_state})

        assert response.status_code == 302
        assert "error=token_exchange_failed" in response.url

    @override_settings(CORS_ALLOWED_ORIGINS=["http://localhost:3000"])
    def test_callback_handles_missing_code(self, client, workspace, valid_state):
        """Test callback handles missing authorization code."""
        url = reverse("integrations:linkedin_callback")
        response = client.get(url, {"state": valid_state})

        assert response.status_code == 302
        assert "error=missing_code" in response.url

    @override_settings(CORS_ALLOWED_ORIGINS=["http://localhost:3000"])
    def test_callback_handles_missing_state(self, client):
        """Test callback handles missing state parameter."""
        url = reverse("integrations:linkedin_callback")
        response = client.get(url, {"code": "auth_code_123"})

        assert response.status_code == 302
        assert "error=missing_state" in response.url

    @override_settings(CORS_ALLOWED_ORIGINS=["http://localhost:3000"])
    @patch("integrations.views.LinkedInClient")
    def test_callback_handles_linkedin_error_response(self, mock_client_class, client):
        """Test callback handles error response from LinkedIn."""
        # LinkedIn may redirect with error instead of code
        url = reverse("integrations:linkedin_callback")
        response = client.get(
            url,
            {
                "error": "access_denied",
                "error_description": "The user denied the authorization request",
                "state": "some_state",
            },
        )

        assert response.status_code == 302
        assert "error=access_denied" in response.url

    @override_settings(CORS_ALLOWED_ORIGINS=["http://localhost:3000"])
    @patch("integrations.views.LinkedInClient")
    def test_callback_stores_tokens_in_cache(self, mock_client_class, client, workspace, valid_state):
        """Test callback stores tokens in cache for later org selection."""
        mock_client = MagicMock()
        mock_client_class.return_value = mock_client

        mock_token_response = MagicMock()
        mock_token_response.access_token = "cached_access_token"
        mock_token_response.refresh_token = "cached_refresh_token"
        mock_token_response.expires_in = 5184000
        mock_token_response.refresh_token_expires_in = 31536000
        mock_client.exchange_code_for_token.return_value = mock_token_response
        mock_client.validate_state.return_value = str(workspace.pk)

        url = reverse("integrations:linkedin_callback")
        client.get(url, {"code": "auth_code", "state": valid_state})

        # Check that tokens are stored in cache
        cache_key = f"linkedin_oauth_{workspace.pk}"
        cached_data = cache.get(cache_key)

        assert cached_data is not None
        assert cached_data["access_token"] == "cached_access_token"
        assert cached_data["refresh_token"] == "cached_refresh_token"

    @override_settings(CORS_ALLOWED_ORIGINS=["http://localhost:3000"])
    @patch("integrations.views.LinkedInClient")
    def test_callback_workspace_not_found(self, mock_client_class, client):
        """Test callback handles workspace not found."""
        mock_client = MagicMock()
        mock_client_class.return_value = mock_client

        # Generate state for a non-existent workspace (using integer ID)
        linkedin_client = LinkedInClient()
        state = linkedin_client.generate_state(workspace_id="999999")

        mock_client.validate_state.return_value = "999999"

        url = reverse("integrations:linkedin_callback")
        response = client.get(url, {"code": "auth_code", "state": state})

        assert response.status_code == 302
        assert "error=workspace_not_found" in response.url


@pytest.mark.django_db
class TestLinkedInOAuthCallbackEdgeCases:
    """Edge case tests for LinkedIn OAuth callback."""

    @override_settings(CORS_ALLOWED_ORIGINS=["http://localhost:3000"])
    @patch("integrations.views.LinkedInClient")
    def test_callback_network_error_during_exchange(self, mock_client_class, client, workspace, valid_state):
        """Test callback handles network errors during token exchange."""

        mock_client = MagicMock()
        mock_client_class.return_value = mock_client
        mock_client.validate_state.return_value = str(workspace.pk)

        from integrations.exceptions import LinkedInAuthError

        mock_client.exchange_code_for_token.side_effect = LinkedInAuthError("Network error occurred")

        url = reverse("integrations:linkedin_callback")
        response = client.get(url, {"code": "auth_code", "state": valid_state})

        assert response.status_code == 302
        assert "error=" in response.url

    @override_settings(CORS_ALLOWED_ORIGINS=[])
    @patch("integrations.views.LinkedInClient")
    def test_callback_uses_default_redirect_url(self, mock_client_class, client, workspace, valid_state):
        """Test callback uses default redirect URL when CORS_ALLOWED_ORIGINS is empty."""
        mock_client = MagicMock()
        mock_client_class.return_value = mock_client

        mock_token_response = MagicMock()
        mock_token_response.access_token = "test_token"
        mock_token_response.refresh_token = "test_refresh"
        mock_token_response.expires_in = 5184000
        mock_token_response.refresh_token_expires_in = 31536000
        mock_client.exchange_code_for_token.return_value = mock_token_response
        mock_client.validate_state.return_value = str(workspace.pk)

        url = reverse("integrations:linkedin_callback")
        response = client.get(url, {"code": "auth_code", "state": valid_state})

        # Should redirect to default localhost
        assert response.status_code == 302
        assert "localhost:3000" in response.url
