"""
Unit tests for OAuth API views.

Tests all views, permissions, business logic, and edge cases using mocks.
No database access required - all dependencies are mocked.
"""
import secrets
from datetime import datetime, timedelta
from unittest.mock import Mock, patch, MagicMock
from urllib.parse import urlencode

import pytest
from django.conf import settings
from django.http import HttpResponse
from django.test import RequestFactory
from django.utils import timezone
from rest_framework import status
from rest_framework.exceptions import PermissionDenied
from rest_framework.test import APIRequestFactory
from rest_framework.request import Request

from smoothschedule.identity.core.oauth_views import (
    OAuthProvidersView,
    OAuthStatusView,
    GoogleOAuthInitiateView,
    GoogleOAuthCallbackView,
    MicrosoftOAuthInitiateView,
    MicrosoftOAuthCallbackView,
    OAuthCredentialListView,
    OAuthCredentialDeleteView,
    get_oauth_redirect_uri,
)


def create_mock_request_with_data(factory_method, url, data):
    """
    Create a mock request with request.data property.
    Simpler than wrapping in DRF Request which requires parsers setup.
    """
    request = factory_method(url)
    request.data = data
    return request


# ==============================================================================
# Tests for get_oauth_redirect_uri helper function
# ==============================================================================

class TestGetOAuthRedirectUri:
    """Test the OAuth redirect URI builder function."""

    def test_debug_mode_uses_lvh_me(self):
        """In DEBUG mode, should use lvh.me domain."""
        factory = RequestFactory()
        request = factory.get('/')

        with patch.object(settings, 'DEBUG', True):
            uri = get_oauth_redirect_uri(request, 'google')

        assert uri == 'http://platform.lvh.me:8000/api/oauth/google/callback/'

    def test_production_mode_uses_request_host_https(self):
        """In production, should use request host with HTTPS."""
        factory = RequestFactory()
        request = factory.get('/', HTTP_HOST='platform.smoothschedule.com', secure=True)

        # Mock the get_host method
        request.get_host = Mock(return_value='platform.smoothschedule.com')
        request.is_secure = Mock(return_value=True)

        with patch.object(settings, 'DEBUG', False):
            uri = get_oauth_redirect_uri(request, 'microsoft')

        assert uri == 'https://platform.smoothschedule.com/api/oauth/microsoft/callback/'

    def test_production_mode_uses_request_host_http(self):
        """In production with insecure request, should use HTTP."""
        factory = RequestFactory()
        request = factory.get('/', HTTP_HOST='localhost:8000')
        request.is_secure = Mock(return_value=False)

        with patch.object(settings, 'DEBUG', False):
            uri = get_oauth_redirect_uri(request, 'google')

        assert uri == 'http://localhost:8000/api/oauth/google/callback/'

    def test_different_providers(self):
        """Should work for different provider names."""
        factory = RequestFactory()
        request = factory.get('/')

        with patch.object(settings, 'DEBUG', True):
            google_uri = get_oauth_redirect_uri(request, 'google')
            microsoft_uri = get_oauth_redirect_uri(request, 'microsoft')

        assert 'google' in google_uri
        assert 'microsoft' in microsoft_uri


# ==============================================================================
# Tests for OAuthProvidersView
# ==============================================================================

class TestOAuthProvidersView:
    """Test the public OAuth providers listing endpoint."""

    def test_allows_unauthenticated_access(self):
        """Should allow access without authentication (AllowAny)."""
        view = OAuthProvidersView()
        assert hasattr(view, 'permission_classes')
        # Permission check handled by DRF, just verify it's set

    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.MicrosoftOAuthService')
    def test_returns_empty_list_when_no_providers_configured(
        self, mock_microsoft_cls, mock_google_cls
    ):
        """Should return empty list when no OAuth providers are configured."""
        # Mock services as not configured
        mock_google_service = Mock()
        mock_google_service.is_configured.return_value = False
        mock_google_cls.return_value = mock_google_service

        mock_microsoft_service = Mock()
        mock_microsoft_service.is_configured.return_value = False
        mock_microsoft_cls.return_value = mock_microsoft_service

        factory = APIRequestFactory()
        request = factory.get('/api/auth/oauth/providers/')

        view = OAuthProvidersView.as_view()
        response = view(request)

        assert response.status_code == 200
        assert response.data == {'providers': []}

    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.MicrosoftOAuthService')
    def test_returns_google_when_configured(
        self, mock_microsoft_cls, mock_google_cls
    ):
        """Should return Google in providers list when configured."""
        mock_google_service = Mock()
        mock_google_service.is_configured.return_value = True
        mock_google_cls.return_value = mock_google_service

        mock_microsoft_service = Mock()
        mock_microsoft_service.is_configured.return_value = False
        mock_microsoft_cls.return_value = mock_microsoft_service

        factory = APIRequestFactory()
        request = factory.get('/api/auth/oauth/providers/')

        view = OAuthProvidersView.as_view()
        response = view(request)

        assert response.status_code == 200
        assert len(response.data['providers']) == 1
        assert response.data['providers'][0]['name'] == 'google'
        assert response.data['providers'][0]['display_name'] == 'Google'
        assert response.data['providers'][0]['icon'] == 'google'

    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.MicrosoftOAuthService')
    def test_returns_microsoft_when_configured(
        self, mock_microsoft_cls, mock_google_cls
    ):
        """Should return Microsoft in providers list when configured."""
        mock_google_service = Mock()
        mock_google_service.is_configured.return_value = False
        mock_google_cls.return_value = mock_google_service

        mock_microsoft_service = Mock()
        mock_microsoft_service.is_configured.return_value = True
        mock_microsoft_cls.return_value = mock_microsoft_service

        factory = APIRequestFactory()
        request = factory.get('/api/auth/oauth/providers/')

        view = OAuthProvidersView.as_view()
        response = view(request)

        assert response.status_code == 200
        assert len(response.data['providers']) == 1
        assert response.data['providers'][0]['name'] == 'microsoft'
        assert response.data['providers'][0]['display_name'] == 'Microsoft'
        assert response.data['providers'][0]['icon'] == 'microsoft'

    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.MicrosoftOAuthService')
    def test_returns_both_providers_when_configured(
        self, mock_microsoft_cls, mock_google_cls
    ):
        """Should return both providers when both are configured."""
        mock_google_service = Mock()
        mock_google_service.is_configured.return_value = True
        mock_google_cls.return_value = mock_google_service

        mock_microsoft_service = Mock()
        mock_microsoft_service.is_configured.return_value = True
        mock_microsoft_cls.return_value = mock_microsoft_service

        factory = APIRequestFactory()
        request = factory.get('/api/auth/oauth/providers/')

        view = OAuthProvidersView.as_view()
        response = view(request)

        assert response.status_code == 200
        assert len(response.data['providers']) == 2
        provider_names = [p['name'] for p in response.data['providers']]
        assert 'google' in provider_names
        assert 'microsoft' in provider_names


# ==============================================================================
# Tests for OAuthStatusView
# ==============================================================================

class TestOAuthStatusView:
    """Test the OAuth status endpoint (platform admin only)."""

    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.MicrosoftOAuthService')
    def test_returns_status_for_both_providers(
        self, mock_microsoft_cls, mock_google_cls
    ):
        """Should return configuration status for both providers."""
        mock_google_service = Mock()
        mock_google_service.is_configured.return_value = True
        mock_google_cls.return_value = mock_google_service

        mock_microsoft_service = Mock()
        mock_microsoft_service.is_configured.return_value = False
        mock_microsoft_cls.return_value = mock_microsoft_service

        factory = APIRequestFactory()
        request = factory.get('/api/oauth/status/')
        request.user = Mock(is_authenticated=True)

        # Call the view method directly to bypass permission checks
        view = OAuthStatusView()
        response = view.get(request)

        assert response.status_code == 200
        assert response.data['google']['configured'] is True
        assert response.data['microsoft']['configured'] is False

    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.MicrosoftOAuthService')
    def test_returns_false_when_not_configured(
        self, mock_microsoft_cls, mock_google_cls
    ):
        """Should return false for both when not configured."""
        mock_google_service = Mock()
        mock_google_service.is_configured.return_value = False
        mock_google_cls.return_value = mock_google_service

        mock_microsoft_service = Mock()
        mock_microsoft_service.is_configured.return_value = False
        mock_microsoft_cls.return_value = mock_microsoft_service

        factory = APIRequestFactory()
        request = factory.get('/api/oauth/status/')
        request.user = Mock(is_authenticated=True)

        # Call the view method directly to bypass permission checks
        view = OAuthStatusView()
        response = view.get(request)

        assert response.status_code == 200
        assert response.data['google']['configured'] is False
        assert response.data['microsoft']['configured'] is False


# ==============================================================================
# Tests for GoogleOAuthInitiateView
# ==============================================================================

class TestGoogleOAuthInitiateView:
    """Test the Google OAuth initiation endpoint."""

    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.get_oauth_redirect_uri')
    def test_initiates_oauth_for_email_purpose(
        self, mock_get_redirect_uri, mock_service_cls
    ):
        """Should generate authorization URL for email purpose."""
        mock_service = Mock()
        mock_service.is_configured.return_value = True
        mock_service.get_authorization_url.return_value = 'https://accounts.google.com/o/oauth2/auth?...'
        mock_service_cls.return_value = mock_service

        mock_get_redirect_uri.return_value = 'http://platform.lvh.me:8000/api/oauth/google/callback/'

        factory = APIRequestFactory()
        request = create_mock_request_with_data(
            factory.post,
            '/api/oauth/google/initiate/',
            {'purpose': 'email'}
        )
        request.user = Mock(is_authenticated=True)
        request.session = {}

        # Call the view method directly to bypass permission checks
        view = GoogleOAuthInitiateView()
        response = view.post(request)

        assert response.status_code == 200
        assert response.data['success'] is True
        assert 'authorization_url' in response.data
        assert 'oauth_state' in request.session
        assert request.session['oauth_purpose'] == 'email'
        assert request.session['oauth_provider'] == 'google'

    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    def test_returns_error_when_not_configured(self, mock_service_cls):
        """Should return error when Google OAuth is not configured."""
        mock_service = Mock()
        mock_service.is_configured.return_value = False
        mock_service_cls.return_value = mock_service

        factory = APIRequestFactory()
        request = create_mock_request_with_data(
            factory.post,
            '/api/oauth/google/initiate/',
            {'purpose': 'email'}
        )
        request.user = Mock(is_authenticated=True)
        request.session = {}

        # Call the view method directly to bypass permission checks
        view = GoogleOAuthInitiateView()
        response = view.post(request)

        assert response.status_code == 400
        assert response.data['success'] is False
        assert 'GOOGLE_OAUTH_CLIENT_ID' in response.data['error']

    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.HasFeaturePermission')
    def test_checks_calendar_permission_for_calendar_purpose(
        self, mock_feature_perm_cls, mock_service_cls
    ):
        """Should check calendar sync permission when purpose is calendar."""
        mock_service = Mock()
        mock_service.is_configured.return_value = True
        mock_service_cls.return_value = mock_service

        # Mock permission class to deny
        mock_perm_instance = Mock()
        mock_perm_instance.has_permission.return_value = False
        mock_perm_cls = Mock(return_value=mock_perm_instance)
        mock_feature_perm_cls.return_value = mock_perm_cls

        factory = APIRequestFactory()
        request = create_mock_request_with_data(
            factory.post,
            '/api/oauth/google/initiate/',
            {'purpose': 'calendar'}
        )
        request.user = Mock(is_authenticated=True)
        request.session = {}

        # Call the view method directly to bypass permission checks
        view = GoogleOAuthInitiateView()
        response = view.post(request)

        assert response.status_code == 403
        assert response.data['success'] is False
        assert 'Calendar Sync' in response.data['error']
        mock_feature_perm_cls.assert_called_once_with('can_use_calendar_sync')

    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.HasFeaturePermission')
    @patch('smoothschedule.identity.core.oauth_views.get_oauth_redirect_uri')
    def test_allows_calendar_purpose_with_permission(
        self, mock_get_redirect_uri, mock_feature_perm_cls, mock_service_cls
    ):
        """Should allow calendar purpose when tenant has permission."""
        mock_service = Mock()
        mock_service.is_configured.return_value = True
        mock_service.get_authorization_url.return_value = 'https://accounts.google.com/auth...'
        mock_service_cls.return_value = mock_service

        mock_get_redirect_uri.return_value = 'http://platform.lvh.me:8000/api/oauth/google/callback/'

        # Mock permission class to allow
        mock_perm_instance = Mock()
        mock_perm_instance.has_permission.return_value = True
        mock_perm_cls = Mock(return_value=mock_perm_instance)
        mock_feature_perm_cls.return_value = mock_perm_cls

        factory = APIRequestFactory()
        request = create_mock_request_with_data(
            factory.post,
            '/api/oauth/google/initiate/',
            {'purpose': 'calendar'}
        )
        request.user = Mock(is_authenticated=True)
        request.session = {}

        # Call the view method directly to bypass permission checks
        view = GoogleOAuthInitiateView()
        response = view.post(request)

        assert response.status_code == 200
        assert response.data['success'] is True
        assert request.session['oauth_purpose'] == 'calendar'

    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.get_oauth_redirect_uri')
    def test_defaults_to_email_purpose(
        self, mock_get_redirect_uri, mock_service_cls
    ):
        """Should default to email purpose when not specified."""
        mock_service = Mock()
        mock_service.is_configured.return_value = True
        mock_service.get_authorization_url.return_value = 'https://accounts.google.com/auth...'
        mock_service_cls.return_value = mock_service

        mock_get_redirect_uri.return_value = 'http://platform.lvh.me:8000/api/oauth/google/callback/'

        factory = APIRequestFactory()
        request = create_mock_request_with_data(
            factory.post,
            '/api/oauth/google/initiate/',
            {}  # No purpose
        )
        request.user = Mock(is_authenticated=True)
        request.session = {}

        # Call the view method directly to bypass permission checks
        view = GoogleOAuthInitiateView()
        response = view.post(request)

        assert response.status_code == 200
        assert request.session['oauth_purpose'] == 'email'

    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.get_oauth_redirect_uri')
    def test_stores_state_in_session(
        self, mock_get_redirect_uri, mock_service_cls
    ):
        """Should store CSRF state token in session."""
        mock_service = Mock()
        mock_service.is_configured.return_value = True
        mock_service.get_authorization_url.return_value = 'https://accounts.google.com/auth...'
        mock_service_cls.return_value = mock_service

        mock_get_redirect_uri.return_value = 'http://platform.lvh.me:8000/api/oauth/google/callback/'

        factory = APIRequestFactory()
        request = create_mock_request_with_data(
            factory.post,
            '/api/oauth/google/initiate/',
            {'purpose': 'email'}
        )
        request.user = Mock(is_authenticated=True)
        request.session = {}

        # Call the view method directly to bypass permission checks
        view = GoogleOAuthInitiateView()
        response = view.post(request)

        assert 'oauth_state' in request.session
        # State should be URL-safe string
        state = request.session['oauth_state']
        assert isinstance(state, str)
        assert len(state) > 10  # Should be reasonably long

    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.get_oauth_redirect_uri')
    def test_handles_service_exception(
        self, mock_get_redirect_uri, mock_service_cls
    ):
        """Should return 500 error when service raises exception."""
        mock_service = Mock()
        mock_service.is_configured.return_value = True
        mock_service.get_authorization_url.side_effect = Exception('API error')
        mock_service_cls.return_value = mock_service

        mock_get_redirect_uri.return_value = 'http://platform.lvh.me:8000/api/oauth/google/callback/'

        factory = APIRequestFactory()
        request = create_mock_request_with_data(
            factory.post,
            '/api/oauth/google/initiate/',
            {'purpose': 'email'}
        )
        request.user = Mock(is_authenticated=True)
        request.session = {}

        # Call the view method directly to bypass permission checks
        view = GoogleOAuthInitiateView()
        response = view.post(request)

        assert response.status_code == 500
        assert response.data['success'] is False
        assert 'API error' in response.data['error']


# ==============================================================================
# Tests for GoogleOAuthCallbackView
# ==============================================================================

class TestGoogleOAuthCallbackView:
    """Test the Google OAuth callback endpoint."""

    @patch('smoothschedule.identity.core.oauth_views.OAuthCredential')
    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.get_oauth_redirect_uri')
    @patch.object(settings, 'FRONTEND_URL', 'http://platform.lvh.me:5173')
    def test_successful_callback_creates_credential(
        self, mock_get_redirect_uri, mock_service_cls, mock_credential_cls
    ):
        """Should exchange code for tokens and create credential."""
        mock_service = Mock()
        mock_service.exchange_code_for_tokens.return_value = {
            'access_token': 'test_access_token',
            'refresh_token': 'test_refresh_token',
            'expires_in': 3600,
            'email': 'test@example.com',
            'scopes': ['https://mail.google.com/'],
        }
        mock_service_cls.return_value = mock_service

        mock_get_redirect_uri.return_value = 'http://platform.lvh.me:8000/api/oauth/google/callback/'

        mock_credential = Mock(id=1, email='test@example.com')
        mock_credential_cls.objects.update_or_create.return_value = (mock_credential, True)

        factory = RequestFactory()
        request = factory.get('/api/oauth/google/callback/', {
            'code': 'test_code',
            'state': 'test_state'
        })
        request.user = Mock(is_authenticated=True, id=1)
        request.session = {
            'oauth_state': 'test_state',
            'oauth_purpose': 'email',
            'oauth_provider': 'google'
        }

        view = GoogleOAuthCallbackView.as_view()
        response = view(request)

        # Should redirect to success URL
        assert response.status_code == 302
        assert 'oauth=success' in response.url
        assert 'provider=google' in response.url
        assert 'email=test@example.com' in response.url

    @patch.object(settings, 'FRONTEND_URL', 'http://platform.lvh.me:5173')
    def test_redirects_to_error_when_oauth_error_present(self):
        """Should redirect to error URL when OAuth provider returns error."""
        factory = RequestFactory()
        request = factory.get('/api/oauth/google/callback/', {
            'error': 'access_denied'
        })
        request.user = Mock(is_authenticated=False)
        request.session = {}

        view = GoogleOAuthCallbackView.as_view()
        response = view(request)

        assert response.status_code == 302
        assert 'oauth=error' in response.url
        assert 'message=access_denied' in response.url

    @patch.object(settings, 'FRONTEND_URL', 'http://platform.lvh.me:5173')
    def test_validates_state_token(self):
        """Should validate CSRF state token."""
        factory = RequestFactory()
        request = factory.get('/api/oauth/google/callback/', {
            'code': 'test_code',
            'state': 'wrong_state'
        })
        request.user = Mock(is_authenticated=False)
        request.session = {'oauth_state': 'correct_state'}

        view = GoogleOAuthCallbackView.as_view()
        response = view(request)

        assert response.status_code == 302
        assert 'oauth=error' in response.url
        assert 'message=invalid_state' in response.url

    @patch.object(settings, 'FRONTEND_URL', 'http://platform.lvh.me:5173')
    def test_returns_error_when_state_missing(self):
        """Should return error when state is missing."""
        factory = RequestFactory()
        request = factory.get('/api/oauth/google/callback/', {
            'code': 'test_code'
            # Missing state
        })
        request.user = Mock(is_authenticated=False)
        request.session = {'oauth_state': 'test_state'}

        view = GoogleOAuthCallbackView.as_view()
        response = view(request)

        assert response.status_code == 302
        assert 'oauth=error' in response.url
        assert 'message=invalid_state' in response.url

    @patch.object(settings, 'FRONTEND_URL', 'http://platform.lvh.me:5173')
    def test_returns_error_when_code_missing(self):
        """Should return error when code is missing."""
        factory = RequestFactory()
        request = factory.get('/api/oauth/google/callback/', {
            'state': 'test_state'
            # Missing code
        })
        request.user = Mock(is_authenticated=False)
        request.session = {'oauth_state': 'test_state'}

        view = GoogleOAuthCallbackView.as_view()
        response = view(request)

        assert response.status_code == 302
        assert 'oauth=error' in response.url
        assert 'message=no_code' in response.url

    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.get_oauth_redirect_uri')
    @patch.object(settings, 'FRONTEND_URL', 'http://platform.lvh.me:5173')
    def test_returns_error_when_no_email_in_tokens(
        self, mock_get_redirect_uri, mock_service_cls
    ):
        """Should return error when email is missing from token response."""
        mock_service = Mock()
        mock_service.exchange_code_for_tokens.return_value = {
            'access_token': 'test_access_token',
            'refresh_token': 'test_refresh_token',
            'expires_in': 3600,
            'email': '',  # Empty email
            'scopes': ['https://mail.google.com/'],
        }
        mock_service_cls.return_value = mock_service

        mock_get_redirect_uri.return_value = 'http://platform.lvh.me:8000/api/oauth/google/callback/'

        factory = RequestFactory()
        request = factory.get('/api/oauth/google/callback/', {
            'code': 'test_code',
            'state': 'test_state'
        })
        request.user = Mock(is_authenticated=False)
        request.session = {'oauth_state': 'test_state'}

        view = GoogleOAuthCallbackView.as_view()
        response = view(request)

        assert response.status_code == 302
        assert 'oauth=error' in response.url
        assert 'message=no_email' in response.url

    @patch('smoothschedule.identity.core.oauth_views.OAuthCredential')
    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.get_oauth_redirect_uri')
    @patch.object(settings, 'FRONTEND_URL', 'http://platform.lvh.me:5173')
    def test_clears_session_after_success(
        self, mock_get_redirect_uri, mock_service_cls, mock_credential_cls
    ):
        """Should clear OAuth session data after successful callback."""
        mock_service = Mock()
        mock_service.exchange_code_for_tokens.return_value = {
            'access_token': 'test_access_token',
            'refresh_token': 'test_refresh_token',
            'expires_in': 3600,
            'email': 'test@example.com',
            'scopes': ['https://mail.google.com/'],
        }
        mock_service_cls.return_value = mock_service

        mock_get_redirect_uri.return_value = 'http://platform.lvh.me:8000/api/oauth/google/callback/'

        mock_credential = Mock(id=1, email='test@example.com')
        mock_credential_cls.objects.update_or_create.return_value = (mock_credential, True)

        factory = RequestFactory()
        request = factory.get('/api/oauth/google/callback/', {
            'code': 'test_code',
            'state': 'test_state'
        })
        request.user = Mock(is_authenticated=True)
        request.session = {
            'oauth_state': 'test_state',
            'oauth_purpose': 'email',
            'oauth_provider': 'google'
        }

        view = GoogleOAuthCallbackView.as_view()
        response = view(request)

        # Session should be cleared
        assert 'oauth_state' not in request.session
        assert 'oauth_purpose' not in request.session
        assert 'oauth_provider' not in request.session

    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.get_oauth_redirect_uri')
    @patch.object(settings, 'FRONTEND_URL', 'http://platform.lvh.me:5173')
    def test_handles_token_exchange_exception(
        self, mock_get_redirect_uri, mock_service_cls
    ):
        """Should handle exception during token exchange."""
        mock_service = Mock()
        mock_service.exchange_code_for_tokens.side_effect = Exception('Token exchange failed')
        mock_service_cls.return_value = mock_service

        mock_get_redirect_uri.return_value = 'http://platform.lvh.me:8000/api/oauth/google/callback/'

        factory = RequestFactory()
        request = factory.get('/api/oauth/google/callback/', {
            'code': 'test_code',
            'state': 'test_state'
        })
        request.user = Mock(is_authenticated=False)
        request.session = {'oauth_state': 'test_state'}

        view = GoogleOAuthCallbackView.as_view()
        response = view(request)

        assert response.status_code == 302
        assert 'oauth=error' in response.url
        assert 'message=' in response.url

    @patch('smoothschedule.identity.core.oauth_views.OAuthCredential')
    @patch('smoothschedule.identity.core.oauth_views.GoogleOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.get_oauth_redirect_uri')
    @patch.object(settings, 'FRONTEND_URL', 'http://platform.lvh.me:5173')
    def test_sets_token_expiry_when_provided(
        self, mock_get_redirect_uri, mock_service_cls, mock_credential_cls
    ):
        """Should set token expiry when expires_in is provided."""
        mock_service = Mock()
        mock_service.exchange_code_for_tokens.return_value = {
            'access_token': 'test_access_token',
            'refresh_token': 'test_refresh_token',
            'expires_in': 3600,
            'email': 'test@example.com',
            'scopes': ['https://mail.google.com/'],
        }
        mock_service_cls.return_value = mock_service

        mock_get_redirect_uri.return_value = 'http://platform.lvh.me:8000/api/oauth/google/callback/'

        mock_credential = Mock(id=1, email='test@example.com')
        mock_credential_cls.objects.update_or_create.return_value = (mock_credential, True)

        factory = RequestFactory()
        request = factory.get('/api/oauth/google/callback/', {
            'code': 'test_code',
            'state': 'test_state'
        })
        request.user = Mock(is_authenticated=True)
        request.session = {'oauth_state': 'test_state'}

        view = GoogleOAuthCallbackView.as_view()
        response = view(request)

        # Verify token_expiry was set
        assert mock_credential.save.called
        # Check that token_expiry was set to a datetime
        assert hasattr(mock_credential, 'token_expiry')

    def test_uses_default_frontend_url_when_not_configured(self):
        """Should use default frontend URL when FRONTEND_URL not in settings."""
        factory = RequestFactory()
        request = factory.get('/api/oauth/google/callback/', {
            'error': 'access_denied'
        })
        request.user = Mock(is_authenticated=False)
        request.session = {}

        # Save original FRONTEND_URL if it exists
        original_frontend_url = getattr(settings, 'FRONTEND_URL', None)
        had_frontend_url = hasattr(settings, 'FRONTEND_URL')

        try:
            # Temporarily remove FRONTEND_URL
            if hasattr(settings, 'FRONTEND_URL'):
                delattr(settings, 'FRONTEND_URL')

            view = GoogleOAuthCallbackView.as_view()
            response = view(request)

            assert response.status_code == 302
            # Should use default: http://platform.lvh.me:5173
            assert response.url.startswith('http://platform.lvh.me:5173')
        finally:
            # Restore original FRONTEND_URL
            if had_frontend_url:
                settings.FRONTEND_URL = original_frontend_url
            elif hasattr(settings, 'FRONTEND_URL'):
                delattr(settings, 'FRONTEND_URL')


# ==============================================================================
# Tests for MicrosoftOAuthInitiateView
# ==============================================================================

class TestMicrosoftOAuthInitiateView:
    """Test the Microsoft OAuth initiation endpoint."""

    @patch('smoothschedule.identity.core.oauth_views.MicrosoftOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.get_oauth_redirect_uri')
    def test_initiates_oauth_for_email_purpose(
        self, mock_get_redirect_uri, mock_service_cls
    ):
        """Should generate authorization URL for email purpose."""
        mock_service = Mock()
        mock_service.is_configured.return_value = True
        mock_service.get_authorization_url.return_value = 'https://login.microsoftonline.com/...'
        mock_service_cls.return_value = mock_service

        mock_get_redirect_uri.return_value = 'http://platform.lvh.me:8000/api/oauth/microsoft/callback/'

        factory = APIRequestFactory()
        request = create_mock_request_with_data(
            factory.post,
            '/api/oauth/microsoft/initiate/',
            {'purpose': 'email'}
        )
        request.user = Mock(is_authenticated=True)
        request.session = {}

        # Call the view method directly to bypass permission checks
        view = MicrosoftOAuthInitiateView()
        response = view.post(request)

        assert response.status_code == 200
        assert response.data['success'] is True
        assert 'authorization_url' in response.data
        assert request.session['oauth_provider'] == 'microsoft'

    @patch('smoothschedule.identity.core.oauth_views.MicrosoftOAuthService')
    def test_returns_error_when_not_configured(self, mock_service_cls):
        """Should return error when Microsoft OAuth is not configured."""
        mock_service = Mock()
        mock_service.is_configured.return_value = False
        mock_service_cls.return_value = mock_service

        factory = APIRequestFactory()
        request = create_mock_request_with_data(
            factory.post,
            '/api/oauth/microsoft/initiate/',
            {'purpose': 'email'}
        )
        request.user = Mock(is_authenticated=True)
        request.session = {}

        # Call the view method directly to bypass permission checks
        view = MicrosoftOAuthInitiateView()
        response = view.post(request)

        assert response.status_code == 400
        assert response.data['success'] is False
        assert 'MICROSOFT_OAUTH_CLIENT_ID' in response.data['error']

    @patch('smoothschedule.identity.core.oauth_views.MicrosoftOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.HasFeaturePermission')
    def test_checks_calendar_permission_for_calendar_purpose(
        self, mock_feature_perm_cls, mock_service_cls
    ):
        """Should check calendar sync permission when purpose is calendar."""
        mock_service = Mock()
        mock_service.is_configured.return_value = True
        mock_service_cls.return_value = mock_service

        # Mock permission to deny
        mock_perm_instance = Mock()
        mock_perm_instance.has_permission.return_value = False
        mock_perm_cls = Mock(return_value=mock_perm_instance)
        mock_feature_perm_cls.return_value = mock_perm_cls

        factory = APIRequestFactory()
        request = create_mock_request_with_data(
            factory.post,
            '/api/oauth/microsoft/initiate/',
            {'purpose': 'calendar'}
        )
        request.user = Mock(is_authenticated=True)
        request.session = {}

        # Call the view method directly to bypass permission checks
        view = MicrosoftOAuthInitiateView()
        response = view.post(request)

        assert response.status_code == 403
        assert response.data['success'] is False
        assert 'Calendar Sync' in response.data['error']

    @patch('smoothschedule.identity.core.oauth_views.MicrosoftOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.get_oauth_redirect_uri')
    def test_handles_service_exception(
        self, mock_get_redirect_uri, mock_service_cls
    ):
        """Should return 500 error when service raises exception."""
        mock_service = Mock()
        mock_service.is_configured.return_value = True
        mock_service.get_authorization_url.side_effect = Exception('MSAL error')
        mock_service_cls.return_value = mock_service

        mock_get_redirect_uri.return_value = 'http://platform.lvh.me:8000/api/oauth/microsoft/callback/'

        factory = APIRequestFactory()
        request = create_mock_request_with_data(
            factory.post,
            '/api/oauth/microsoft/initiate/',
            {'purpose': 'email'}
        )
        request.user = Mock(is_authenticated=True)
        request.session = {}

        # Call the view method directly to bypass permission checks
        view = MicrosoftOAuthInitiateView()
        response = view.post(request)

        assert response.status_code == 500
        assert response.data['success'] is False
        assert 'MSAL error' in response.data['error']


# ==============================================================================
# Tests for MicrosoftOAuthCallbackView
# ==============================================================================

class TestMicrosoftOAuthCallbackView:
    """Test the Microsoft OAuth callback endpoint."""

    @patch('smoothschedule.identity.core.oauth_views.OAuthCredential')
    @patch('smoothschedule.identity.core.oauth_views.MicrosoftOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.get_oauth_redirect_uri')
    @patch.object(settings, 'FRONTEND_URL', 'http://platform.lvh.me:5173')
    def test_successful_callback_creates_credential(
        self, mock_get_redirect_uri, mock_service_cls, mock_credential_cls
    ):
        """Should exchange code for tokens and create credential."""
        mock_service = Mock()
        mock_service.exchange_code_for_tokens.return_value = {
            'access_token': 'test_access_token',
            'refresh_token': 'test_refresh_token',
            'expires_in': 3600,
            'email': 'test@outlook.com',
            'scopes': ['IMAP.AccessAsUser.All'],
        }
        mock_service_cls.return_value = mock_service

        mock_get_redirect_uri.return_value = 'http://platform.lvh.me:8000/api/oauth/microsoft/callback/'

        mock_credential = Mock(id=1, email='test@outlook.com')
        mock_credential_cls.objects.update_or_create.return_value = (mock_credential, True)

        factory = RequestFactory()
        request = factory.get('/api/oauth/microsoft/callback/', {
            'code': 'test_code',
            'state': 'test_state'
        })
        request.user = Mock(is_authenticated=True)
        request.session = {'oauth_state': 'test_state'}

        view = MicrosoftOAuthCallbackView.as_view()
        response = view(request)

        assert response.status_code == 302
        assert 'oauth=success' in response.url
        assert 'provider=microsoft' in response.url
        assert 'email=test@outlook.com' in response.url

    @patch.object(settings, 'FRONTEND_URL', 'http://platform.lvh.me:5173')
    def test_handles_oauth_error_with_description(self):
        """Should handle OAuth error with error_description."""
        factory = RequestFactory()
        request = factory.get('/api/oauth/microsoft/callback/', {
            'error': 'invalid_grant',
            'error_description': 'The code has expired'
        })
        request.user = Mock(is_authenticated=False)
        request.session = {}

        view = MicrosoftOAuthCallbackView.as_view()
        response = view(request)

        assert response.status_code == 302
        assert 'oauth=error' in response.url
        assert 'message=invalid_grant' in response.url

    @patch.object(settings, 'FRONTEND_URL', 'http://platform.lvh.me:5173')
    def test_validates_state_token(self):
        """Should validate CSRF state token."""
        factory = RequestFactory()
        request = factory.get('/api/oauth/microsoft/callback/', {
            'code': 'test_code',
            'state': 'wrong_state'
        })
        request.user = Mock(is_authenticated=False)
        request.session = {'oauth_state': 'correct_state'}

        view = MicrosoftOAuthCallbackView.as_view()
        response = view(request)

        assert response.status_code == 302
        assert 'oauth=error' in response.url
        assert 'message=invalid_state' in response.url

    @patch('smoothschedule.identity.core.oauth_views.OAuthCredential')
    @patch('smoothschedule.identity.core.oauth_views.MicrosoftOAuthService')
    @patch('smoothschedule.identity.core.oauth_views.get_oauth_redirect_uri')
    @patch.object(settings, 'FRONTEND_URL', 'http://platform.lvh.me:5173')
    def test_clears_session_after_success(
        self, mock_get_redirect_uri, mock_service_cls, mock_credential_cls
    ):
        """Should clear OAuth session data after successful callback."""
        mock_service = Mock()
        mock_service.exchange_code_for_tokens.return_value = {
            'access_token': 'test_access_token',
            'refresh_token': 'test_refresh_token',
            'expires_in': 3600,
            'email': 'test@outlook.com',
            'scopes': ['IMAP.AccessAsUser.All'],
        }
        mock_service_cls.return_value = mock_service

        mock_get_redirect_uri.return_value = 'http://platform.lvh.me:8000/api/oauth/microsoft/callback/'

        mock_credential = Mock(id=1, email='test@outlook.com')
        mock_credential_cls.objects.update_or_create.return_value = (mock_credential, True)

        factory = RequestFactory()
        request = factory.get('/api/oauth/microsoft/callback/', {
            'code': 'test_code',
            'state': 'test_state'
        })
        request.user = Mock(is_authenticated=True)
        request.session = {
            'oauth_state': 'test_state',
            'oauth_purpose': 'calendar',
            'oauth_provider': 'microsoft'
        }

        view = MicrosoftOAuthCallbackView.as_view()
        response = view(request)

        assert 'oauth_state' not in request.session
        assert 'oauth_purpose' not in request.session
        assert 'oauth_provider' not in request.session


# ==============================================================================
# Tests for OAuthCredentialListView
# ==============================================================================

class TestOAuthCredentialListView:
    """Test the OAuth credentials listing endpoint."""

    @patch('smoothschedule.identity.core.oauth_views.OAuthCredential')
    def test_lists_platform_level_credentials(self, mock_credential_cls):
        """Should list platform-level email credentials."""
        mock_cred1 = Mock(
            id=1,
            provider='google',
            email='admin@example.com',
            purpose='email',
            is_valid=True,
            last_used_at=None,
            last_error='',
            created_at=timezone.now()
        )
        mock_cred1.is_expired.return_value = False

        mock_cred2 = Mock(
            id=2,
            provider='microsoft',
            email='support@example.com',
            purpose='email',
            is_valid=True,
            last_used_at=timezone.now(),
            last_error='',
            created_at=timezone.now()
        )
        mock_cred2.is_expired.return_value = False

        mock_queryset = Mock()
        mock_queryset.filter.return_value = mock_queryset
        mock_queryset.order_by.return_value = [mock_cred1, mock_cred2]
        mock_credential_cls.objects = mock_queryset

        factory = APIRequestFactory()
        request = factory.get('/api/oauth/credentials/')
        request.user = Mock(is_authenticated=True)

        view = OAuthCredentialListView()
        response = view.get(request)

        assert response.status_code == 200
        assert len(response.data) == 2
        assert response.data[0]['id'] == 1
        assert response.data[0]['provider'] == 'google'
        assert response.data[0]['email'] == 'admin@example.com'
        assert response.data[1]['id'] == 2

    @patch('smoothschedule.identity.core.oauth_views.OAuthCredential')
    def test_filters_by_tenant_none_and_purpose_email(self, mock_credential_cls):
        """Should filter by tenant=None and purpose=email."""
        mock_queryset = Mock()
        mock_queryset.filter.return_value = mock_queryset
        mock_queryset.order_by.return_value = []
        mock_credential_cls.objects = mock_queryset

        factory = APIRequestFactory()
        request = factory.get('/api/oauth/credentials/')
        request.user = Mock(is_authenticated=True)

        view = OAuthCredentialListView()
        response = view.get(request)

        # Verify filter was called with correct parameters
        mock_queryset.filter.assert_called_once_with(
            tenant=None,
            purpose='email'
        )

    @patch('smoothschedule.identity.core.oauth_views.OAuthCredential')
    def test_orders_by_created_at_descending(self, mock_credential_cls):
        """Should order results by created_at descending."""
        mock_queryset = Mock()
        mock_queryset.filter.return_value = mock_queryset
        mock_queryset.order_by.return_value = []
        mock_credential_cls.objects = mock_queryset

        factory = APIRequestFactory()
        request = factory.get('/api/oauth/credentials/')
        request.user = Mock(is_authenticated=True)

        view = OAuthCredentialListView()
        response = view.get(request)

        # Verify order_by was called
        mock_queryset.order_by.assert_called_once_with('-created_at')

    @patch('smoothschedule.identity.core.oauth_views.OAuthCredential')
    def test_includes_is_expired_in_response(self, mock_credential_cls):
        """Should call is_expired() and include in response."""
        mock_cred = Mock(
            id=1,
            provider='google',
            email='test@example.com',
            purpose='email',
            is_valid=True,
            last_used_at=None,
            last_error='',
            created_at=timezone.now()
        )
        mock_cred.is_expired.return_value = True

        mock_queryset = Mock()
        mock_queryset.filter.return_value = mock_queryset
        mock_queryset.order_by.return_value = [mock_cred]
        mock_credential_cls.objects = mock_queryset

        factory = APIRequestFactory()
        request = factory.get('/api/oauth/credentials/')
        request.user = Mock(is_authenticated=True)

        view = OAuthCredentialListView()
        response = view.get(request)

        assert response.data[0]['is_expired'] is True
        mock_cred.is_expired.assert_called_once()


# ==============================================================================
# Tests for OAuthCredentialDeleteView
# ==============================================================================

class TestOAuthCredentialDeleteView:
    """Test the OAuth credential deletion endpoint."""

    @patch('smoothschedule.identity.core.oauth_views.OAuthCredential')
    def test_deletes_credential_successfully(self, mock_credential_cls):
        """Should delete credential and return success message."""
        mock_credential = Mock(id=1, email='test@example.com')
        mock_credential_cls.objects.get.return_value = mock_credential

        factory = APIRequestFactory()
        request = factory.delete('/api/oauth/credentials/1/')
        request.user = Mock(is_authenticated=True)

        view = OAuthCredentialDeleteView()
        response = view.delete(request, credential_id=1)

        assert response.status_code == 200
        assert response.data['success'] is True
        assert 'test@example.com' in response.data['message']
        mock_credential.delete.assert_called_once()

    @patch('smoothschedule.identity.core.oauth_views.OAuthCredential')
    def test_filters_by_tenant_none(self, mock_credential_cls):
        """Should only allow deletion of platform-level credentials."""
        mock_credential = Mock(id=1, email='test@example.com')
        mock_credential_cls.objects.get.return_value = mock_credential

        factory = APIRequestFactory()
        request = factory.delete('/api/oauth/credentials/1/')
        request.user = Mock(is_authenticated=True)

        view = OAuthCredentialDeleteView()
        response = view.delete(request, credential_id=1)

        # Verify get was called with tenant=None
        mock_credential_cls.objects.get.assert_called_once_with(
            id=1,
            tenant=None
        )

    @patch('smoothschedule.identity.core.oauth_views.OAuthCredential')
    def test_returns_404_when_credential_not_found(self, mock_credential_cls):
        """Should return 404 when credential doesn't exist."""
        from smoothschedule.identity.core.models import OAuthCredential as RealOAuthCredential
        mock_credential_cls.DoesNotExist = RealOAuthCredential.DoesNotExist
        mock_credential_cls.objects.get.side_effect = RealOAuthCredential.DoesNotExist

        factory = APIRequestFactory()
        request = factory.delete('/api/oauth/credentials/999/')
        request.user = Mock(is_authenticated=True)

        view = OAuthCredentialDeleteView()
        response = view.delete(request, credential_id=999)

        assert response.status_code == 404
        assert response.data['success'] is False
        assert 'not found' in response.data['error']

    @patch('smoothschedule.identity.core.oauth_views.OAuthCredential')
    def test_uses_correct_credential_id_from_url(self, mock_credential_cls):
        """Should use credential_id from URL parameter."""
        mock_credential = Mock(id=42, email='test@example.com')
        mock_credential_cls.objects.get.return_value = mock_credential

        factory = APIRequestFactory()
        request = factory.delete('/api/oauth/credentials/42/')
        request.user = Mock(is_authenticated=True)

        view = OAuthCredentialDeleteView()
        response = view.delete(request, credential_id=42)

        mock_credential_cls.objects.get.assert_called_once_with(
            id=42,
            tenant=None
        )
