"""
Unit tests for MFA API Views

Tests all views, actions, permissions, and business logic using mocks.
Does NOT use @pytest.mark.django_db - uses APIRequestFactory with mocked authentication.
"""

from datetime import datetime, timedelta
from unittest.mock import Mock, patch, MagicMock, call
from django.utils import timezone

import pytest
from rest_framework import status
from rest_framework.test import APIRequestFactory

from smoothschedule.identity.users import mfa_api_views


# ============================================================================
# FIXTURES
# ============================================================================

@pytest.fixture
def mock_user():
    """Create a mock authenticated user."""
    user = Mock()
    user.id = 1
    user.email = 'test@example.com'
    user.username = 'testuser'
    user.first_name = 'Test'
    user.last_name = 'User'
    user.full_name = 'Test User'
    user.phone = '+14155551234'
    user.phone_verified = False
    user.role = 'TENANT_OWNER'
    user.tenant = Mock(id=1, schema_name='demo')
    user.mfa_enabled = False
    user.mfa_method = 'NONE'
    user.totp_secret = ''
    user.totp_verified = False
    user.mfa_backup_codes = []
    user.mfa_backup_codes_generated_at = None
    user.is_authenticated = True
    user.check_password = Mock(return_value=True)
    user.save = Mock()
    return user


@pytest.fixture
def mock_mfa_manager():
    """Create a mock MFA manager."""
    with patch('smoothschedule.identity.users.mfa_api_views.mfa_manager') as mock:
        yield mock


@pytest.fixture
def factory():
    """Create API request factory."""
    return APIRequestFactory()


@pytest.fixture(autouse=True)
def mock_jwt():
    """Mock rest_framework_simplejwt module for all tests."""
    import sys

    # Create mock RefreshToken
    mock_refresh_token = Mock()
    mock_refresh_token.access_token = 'mock_access_token'
    mock_refresh_token.__str__ = Mock(return_value='mock_refresh_token')

    mock_refresh_class = Mock()
    mock_refresh_class.for_user = Mock(return_value=mock_refresh_token)

    # Create fake modules
    mock_jwt_module = MagicMock()
    mock_jwt_tokens = MagicMock()
    mock_jwt_tokens.RefreshToken = mock_refresh_class
    mock_jwt_module.tokens = mock_jwt_tokens

    sys.modules['rest_framework_simplejwt'] = mock_jwt_module
    sys.modules['rest_framework_simplejwt.tokens'] = mock_jwt_tokens

    yield mock_refresh_class

    # Cleanup
    if 'rest_framework_simplejwt' in sys.modules:
        del sys.modules['rest_framework_simplejwt']
    if 'rest_framework_simplejwt.tokens' in sys.modules:
        del sys.modules['rest_framework_simplejwt.tokens']


# ============================================================================
# MFA STATUS TESTS
# ============================================================================

class TestMFAStatus:
    """Tests for mfa_status view."""

    def test_mfa_status_returns_user_settings(self, factory, mock_user, mock_mfa_manager):
        """Test that mfa_status returns correct user MFA settings."""
        # Arrange
        mock_mfa_manager.get_available_methods.return_value = ['SMS', 'TOTP']

        with patch('smoothschedule.identity.users.mfa_api_views.TrustedDevice') as mock_td:
            mock_td.objects.filter.return_value.count.return_value = 2

            request = factory.get('/api/mfa/status/')
            request.user = mock_user

            # Act
            response = mfa_api_views.mfa_status(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            assert response.data['mfa_enabled'] is False
            assert response.data['mfa_method'] == 'NONE'
            assert response.data['methods'] == ['SMS', 'TOTP']
            assert response.data['phone_last_4'] == '1234'
            assert response.data['phone_verified'] is False
            assert response.data['totp_verified'] is False
            assert response.data['backup_codes_count'] == 0
            assert response.data['trusted_devices_count'] == 2

    def test_mfa_status_with_short_phone(self, factory, mock_user, mock_mfa_manager):
        """Test mfa_status with phone number shorter than 4 digits."""
        # Arrange
        mock_user.phone = '123'
        mock_mfa_manager.get_available_methods.return_value = []

        with patch('smoothschedule.identity.users.mfa_api_views.TrustedDevice') as mock_td:
            mock_td.objects.filter.return_value.count.return_value = 0

            request = factory.get('/api/mfa/status/')
            request.user = mock_user

            # Act
            response = mfa_api_views.mfa_status(request)

            # Assert
            assert response.data['phone_last_4'] is None

    def test_mfa_status_with_no_phone(self, factory, mock_user, mock_mfa_manager):
        """Test mfa_status when user has no phone number."""
        # Arrange
        mock_user.phone = None
        mock_mfa_manager.get_available_methods.return_value = []

        with patch('smoothschedule.identity.users.mfa_api_views.TrustedDevice') as mock_td:
            mock_td.objects.filter.return_value.count.return_value = 0

            request = factory.get('/api/mfa/status/')
            request.user = mock_user

            # Act
            response = mfa_api_views.mfa_status(request)

            # Assert
            assert response.data['phone_last_4'] is None

    def test_mfa_status_with_backup_codes(self, factory, mock_user, mock_mfa_manager):
        """Test mfa_status includes backup codes count."""
        # Arrange
        mock_user.mfa_backup_codes = ['hash1', 'hash2', 'hash3']
        generated_at = timezone.now()
        mock_user.mfa_backup_codes_generated_at = generated_at
        mock_mfa_manager.get_available_methods.return_value = []

        with patch('smoothschedule.identity.users.mfa_api_views.TrustedDevice') as mock_td:
            mock_td.objects.filter.return_value.count.return_value = 0

            request = factory.get('/api/mfa/status/')
            request.user = mock_user

            # Act
            response = mfa_api_views.mfa_status(request)

            # Assert
            assert response.data['backup_codes_count'] == 3
            assert response.data['backup_codes_generated_at'] == generated_at


# ============================================================================
# SMS SETUP TESTS
# ============================================================================

class TestSendPhoneVerification:
    """Tests for send_phone_verification view."""

    def test_send_phone_verification_success(self, factory, mock_user, mock_mfa_manager):
        """Test successful phone verification code sending."""
        # Arrange
        mock_mfa_manager.send_sms_code.return_value = (True, 'Code sent')

        request = factory.post('/api/mfa/send-phone-verification/', {'phone': '+14155559999'})
        request.user = mock_user

        # Act
        response = mfa_api_views.send_phone_verification(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['success'] is True
        assert response.data['message'] == 'Verification code sent'
        mock_user.save.assert_called_once_with(update_fields=['phone', 'phone_verified'])
        assert mock_user.phone == '+14155559999'
        assert mock_user.phone_verified is False
        mock_mfa_manager.send_sms_code.assert_called_once_with(mock_user, purpose='PHONE_VERIFY')

    def test_send_phone_verification_missing_phone(self, factory, mock_user, mock_mfa_manager):
        """Test send_phone_verification returns error when phone is missing."""
        # Arrange
        request = factory.post('/api/mfa/send-phone-verification/', {})
        request.user = mock_user

        # Act
        response = mfa_api_views.send_phone_verification(request)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data
        assert response.data['error'] == 'Phone number is required'
        mock_mfa_manager.send_sms_code.assert_not_called()

    def test_send_phone_verification_sms_failure(self, factory, mock_user, mock_mfa_manager):
        """Test send_phone_verification handles SMS sending failure."""
        # Arrange
        mock_mfa_manager.send_sms_code.return_value = (False, 'SMS service error')

        request = factory.post('/api/mfa/send-phone-verification/', {'phone': '+14155559999'})
        request.user = mock_user

        # Act
        response = mfa_api_views.send_phone_verification(request)

        # Assert
        assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
        assert 'error' in response.data
        assert response.data['error'] == 'SMS service error'


class TestVerifyPhone:
    """Tests for verify_phone view."""

    def test_verify_phone_success(self, factory, mock_user):
        """Test successful phone verification."""
        # Arrange
        mock_verification = Mock()
        mock_verification.verify.return_value = True

        with patch('smoothschedule.identity.users.mfa_api_views.MFAVerificationCode') as mock_mvc:
            mock_mvc.objects.filter.return_value.order_by.return_value.first.return_value = mock_verification
            mock_mvc.Purpose.PHONE_VERIFY = 'PHONE_VERIFY'

            request = factory.post('/api/mfa/verify-phone/', {'code': '123456'})
            request.user = mock_user

            # Act
            response = mfa_api_views.verify_phone(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            assert response.data['success'] is True
            assert response.data['message'] == 'Phone number verified'
            assert mock_user.phone_verified is True
            mock_user.save.assert_called_once_with(update_fields=['phone_verified'])

    def test_verify_phone_invalid_code_format(self, factory, mock_user):
        """Test verify_phone rejects invalid code format."""
        # Arrange
        request = factory.post('/api/mfa/verify-phone/', {'code': '12345'})  # Only 5 digits
        request.user = mock_user

        # Act
        response = mfa_api_views.verify_phone(request)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data
        assert response.data['error'] == 'Invalid code format'

    def test_verify_phone_missing_code(self, factory, mock_user):
        """Test verify_phone handles missing code."""
        # Arrange
        request = factory.post('/api/mfa/verify-phone/', {})
        request.user = mock_user

        # Act
        response = mfa_api_views.verify_phone(request)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data

    def test_verify_phone_no_pending_verification(self, factory, mock_user):
        """Test verify_phone when no pending verification exists."""
        # Arrange
        with patch('smoothschedule.identity.users.mfa_api_views.MFAVerificationCode') as mock_mvc:
            mock_mvc.objects.filter.return_value.order_by.return_value.first.return_value = None
            mock_mvc.Purpose.PHONE_VERIFY = 'PHONE_VERIFY'

            request = factory.post('/api/mfa/verify-phone/', {'code': '123456'})
            request.user = mock_user

            # Act
            response = mfa_api_views.verify_phone(request)

            # Assert
            assert response.status_code == status.HTTP_400_BAD_REQUEST
            assert 'error' in response.data
            assert 'No pending verification' in response.data['error']

    def test_verify_phone_incorrect_code(self, factory, mock_user):
        """Test verify_phone handles incorrect code."""
        # Arrange
        mock_verification = Mock()
        mock_verification.verify.return_value = False
        mock_verification.attempts = 2

        with patch('smoothschedule.identity.users.mfa_api_views.MFAVerificationCode') as mock_mvc:
            mock_mvc.objects.filter.return_value.order_by.return_value.first.return_value = mock_verification
            mock_mvc.Purpose.PHONE_VERIFY = 'PHONE_VERIFY'

            request = factory.post('/api/mfa/verify-phone/', {'code': '123456'})
            request.user = mock_user

            # Act
            response = mfa_api_views.verify_phone(request)

            # Assert
            assert response.status_code == status.HTTP_400_BAD_REQUEST
            assert 'error' in response.data
            assert '3 attempts remaining' in response.data['error']

    def test_verify_phone_strips_whitespace(self, factory, mock_user):
        """Test verify_phone strips whitespace from code."""
        # Arrange
        mock_verification = Mock()
        mock_verification.verify.return_value = True

        with patch('smoothschedule.identity.users.mfa_api_views.MFAVerificationCode') as mock_mvc:
            mock_mvc.objects.filter.return_value.order_by.return_value.first.return_value = mock_verification
            mock_mvc.Purpose.PHONE_VERIFY = 'PHONE_VERIFY'

            request = factory.post('/api/mfa/verify-phone/', {'code': '  123456  '})
            request.user = mock_user

            # Act
            response = mfa_api_views.verify_phone(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            mock_verification.verify.assert_called_once_with('123456')


class TestEnableSMSMFA:
    """Tests for enable_sms_mfa view."""

    def test_enable_sms_mfa_first_time_success(self, factory, mock_user, mock_mfa_manager):
        """Test enabling SMS MFA for the first time generates backup codes."""
        # Arrange
        mock_user.phone_verified = True
        mock_mfa_manager.generate_backup_codes.return_value = ['code1', 'code2', 'code3']

        request = factory.post('/api/mfa/enable-sms/')
        request.user = mock_user

        # Act
        response = mfa_api_views.enable_sms_mfa(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['success'] is True
        assert response.data['message'] == 'SMS MFA enabled'
        assert response.data['mfa_method'] == 'SMS'
        assert 'backup_codes' in response.data
        assert response.data['backup_codes'] == ['code1', 'code2', 'code3']
        assert 'backup_codes_message' in response.data
        assert mock_user.mfa_enabled is True
        assert mock_user.mfa_method == 'SMS'
        mock_user.save.assert_called_once_with(update_fields=['mfa_enabled', 'mfa_method'])

    def test_enable_sms_mfa_phone_not_verified(self, factory, mock_user):
        """Test enable_sms_mfa rejects when phone not verified."""
        # Arrange
        mock_user.phone_verified = False

        request = factory.post('/api/mfa/enable-sms/')
        request.user = mock_user

        # Act
        response = mfa_api_views.enable_sms_mfa(request)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data
        assert 'Phone number must be verified first' in response.data['error']

    def test_enable_sms_mfa_already_has_totp(self, factory, mock_user, mock_mfa_manager):
        """Test enabling SMS when user already has TOTP sets method to BOTH."""
        # Arrange
        mock_user.phone_verified = True
        mock_user.mfa_enabled = True
        mock_user.mfa_method = 'TOTP'

        request = factory.post('/api/mfa/enable-sms/')
        request.user = mock_user

        # Act
        response = mfa_api_views.enable_sms_mfa(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert mock_user.mfa_method == 'BOTH'
        assert 'backup_codes' not in response.data  # Not first time

    def test_enable_sms_mfa_already_enabled_no_backup_codes(self, factory, mock_user):
        """Test enabling SMS when MFA already enabled doesn't generate new backup codes."""
        # Arrange
        mock_user.phone_verified = True
        mock_user.mfa_enabled = True
        mock_user.mfa_method = 'NONE'

        request = factory.post('/api/mfa/enable-sms/')
        request.user = mock_user

        # Act
        response = mfa_api_views.enable_sms_mfa(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert 'backup_codes' not in response.data


# ============================================================================
# TOTP SETUP TESTS
# ============================================================================

class TestSetupTOTP:
    """Tests for setup_totp view."""

    def test_setup_totp_success(self, factory, mock_user, mock_mfa_manager):
        """Test successful TOTP setup initialization."""
        # Arrange
        mock_mfa_manager.setup_totp.return_value = {
            'secret': 'JBSWY3DPEHPK3PXP',
            'qr_code': 'data:image/png;base64,iVBORw0KG...',
            'provisioning_uri': 'otpauth://totp/...'
        }

        request = factory.post('/api/mfa/setup-totp/')
        request.user = mock_user

        # Act
        response = mfa_api_views.setup_totp(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['success'] is True
        assert response.data['secret'] == 'JBSWY3DPEHPK3PXP'
        assert 'qr_code' in response.data
        assert 'provisioning_uri' in response.data
        assert 'message' in response.data
        mock_mfa_manager.setup_totp.assert_called_once_with(mock_user)


class TestVerifyTOTPSetup:
    """Tests for verify_totp_setup view."""

    def test_verify_totp_setup_first_time_success(self, factory, mock_user, mock_mfa_manager):
        """Test successful TOTP verification for first time generates backup codes."""
        # Arrange
        mock_user.mfa_enabled = False
        mock_user.mfa_method = 'NONE'
        mock_mfa_manager.verify_totp_setup.return_value = True
        mock_mfa_manager.generate_backup_codes.return_value = ['code1', 'code2']

        request = factory.post('/api/mfa/verify-totp-setup/', {'code': '123456'})
        request.user = mock_user

        # Act
        response = mfa_api_views.verify_totp_setup(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['success'] is True
        assert response.data['message'] == 'Authenticator app configured successfully'
        assert 'backup_codes' in response.data
        assert response.data['backup_codes'] == ['code1', 'code2']
        mock_mfa_manager.verify_totp_setup.assert_called_once_with(mock_user, '123456')

    def test_verify_totp_setup_invalid_code_format(self, factory, mock_user):
        """Test verify_totp_setup rejects invalid code format."""
        # Arrange
        request = factory.post('/api/mfa/verify-totp-setup/', {'code': '12345'})
        request.user = mock_user

        # Act
        response = mfa_api_views.verify_totp_setup(request)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data
        assert 'Invalid code format' in response.data['error']

    def test_verify_totp_setup_incorrect_code(self, factory, mock_user, mock_mfa_manager):
        """Test verify_totp_setup handles incorrect code."""
        # Arrange
        mock_mfa_manager.verify_totp_setup.return_value = False

        request = factory.post('/api/mfa/verify-totp-setup/', {'code': '123456'})
        request.user = mock_user

        # Act
        response = mfa_api_views.verify_totp_setup(request)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data
        assert 'Invalid code' in response.data['error']

    def test_verify_totp_setup_already_enabled_no_backup_codes(self, factory, mock_user, mock_mfa_manager):
        """Test verifying TOTP when MFA already enabled doesn't generate backup codes."""
        # Arrange
        mock_user.mfa_enabled = True
        mock_mfa_manager.verify_totp_setup.return_value = True

        request = factory.post('/api/mfa/verify-totp-setup/', {'code': '123456'})
        request.user = mock_user

        # Act
        response = mfa_api_views.verify_totp_setup(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert 'backup_codes' not in response.data

    def test_verify_totp_setup_strips_whitespace(self, factory, mock_user, mock_mfa_manager):
        """Test verify_totp_setup strips whitespace from code."""
        # Arrange
        mock_mfa_manager.verify_totp_setup.return_value = True

        request = factory.post('/api/mfa/verify-totp-setup/', {'code': '  123456  '})
        request.user = mock_user

        # Act
        response = mfa_api_views.verify_totp_setup(request)

        # Assert
        mock_mfa_manager.verify_totp_setup.assert_called_once_with(mock_user, '123456')


# ============================================================================
# BACKUP CODES TESTS
# ============================================================================

class TestGenerateBackupCodes:
    """Tests for generate_backup_codes view."""

    def test_generate_backup_codes_success(self, factory, mock_user, mock_mfa_manager):
        """Test successful backup codes generation."""
        # Arrange
        mock_user.mfa_enabled = True
        mock_mfa_manager.generate_backup_codes.return_value = ['code1', 'code2', 'code3']

        request = factory.post('/api/mfa/generate-backup-codes/')
        request.user = mock_user

        # Act
        response = mfa_api_views.generate_backup_codes(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['success'] is True
        assert response.data['backup_codes'] == ['code1', 'code2', 'code3']
        assert 'message' in response.data
        assert 'warning' in response.data
        mock_mfa_manager.generate_backup_codes.assert_called_once_with(mock_user)

    def test_generate_backup_codes_mfa_not_enabled(self, factory, mock_user):
        """Test generate_backup_codes rejects when MFA not enabled."""
        # Arrange
        mock_user.mfa_enabled = False

        request = factory.post('/api/mfa/generate-backup-codes/')
        request.user = mock_user

        # Act
        response = mfa_api_views.generate_backup_codes(request)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data
        assert 'MFA must be enabled' in response.data['error']


class TestBackupCodesStatus:
    """Tests for backup_codes_status view."""

    def test_backup_codes_status_with_codes(self, factory, mock_user):
        """Test backup_codes_status returns correct count."""
        # Arrange
        generated_at = timezone.now()
        mock_user.mfa_backup_codes = ['hash1', 'hash2', 'hash3']
        mock_user.mfa_backup_codes_generated_at = generated_at

        request = factory.get('/api/mfa/backup-codes-status/')
        request.user = mock_user

        # Act
        response = mfa_api_views.backup_codes_status(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['count'] == 3
        assert response.data['generated_at'] == generated_at

    def test_backup_codes_status_no_codes(self, factory, mock_user):
        """Test backup_codes_status when no codes exist."""
        # Arrange
        mock_user.mfa_backup_codes = None
        mock_user.mfa_backup_codes_generated_at = None

        request = factory.get('/api/mfa/backup-codes-status/')
        request.user = mock_user

        # Act
        response = mfa_api_views.backup_codes_status(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['count'] == 0
        assert response.data['generated_at'] is None


# ============================================================================
# DISABLE MFA TESTS
# ============================================================================

class TestDisableMFA:
    """Tests for disable_mfa view."""

    def test_disable_mfa_with_password(self, factory, mock_user, mock_mfa_manager):
        """Test disabling MFA with valid password."""
        # Arrange
        mock_user.check_password.return_value = True

        request = factory.post('/api/mfa/disable/', {'password': 'correct_password'})
        request.user = mock_user

        # Act
        response = mfa_api_views.disable_mfa(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['success'] is True
        assert 'message' in response.data
        mock_user.check_password.assert_called_once_with('correct_password')
        mock_mfa_manager.disable_mfa.assert_called_once_with(mock_user)

    def test_disable_mfa_with_totp_code(self, factory, mock_user, mock_mfa_manager):
        """Test disabling MFA with valid TOTP code."""
        # Arrange
        mock_mfa_manager.verify_totp.return_value = True

        request = factory.post('/api/mfa/disable/', {'mfa_code': '123456'})
        request.user = mock_user

        # Act
        response = mfa_api_views.disable_mfa(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['success'] is True
        mock_mfa_manager.verify_totp.assert_called_once_with(mock_user, '123456')
        mock_mfa_manager.disable_mfa.assert_called_once_with(mock_user)

    def test_disable_mfa_with_backup_code(self, factory, mock_user, mock_mfa_manager):
        """Test disabling MFA with valid backup code."""
        # Arrange
        mock_mfa_manager.verify_totp.return_value = False
        mock_mfa_manager.verify_backup_code.return_value = True

        request = factory.post('/api/mfa/disable/', {'mfa_code': 'BACKUP123'})
        request.user = mock_user

        # Act
        response = mfa_api_views.disable_mfa(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['success'] is True
        mock_mfa_manager.verify_backup_code.assert_called_once_with(mock_user, 'BACKUP123')
        mock_mfa_manager.disable_mfa.assert_called_once_with(mock_user)

    def test_disable_mfa_invalid_password(self, factory, mock_user, mock_mfa_manager):
        """Test disable_mfa rejects invalid password."""
        # Arrange
        mock_user.check_password.return_value = False

        request = factory.post('/api/mfa/disable/', {'password': 'wrong_password'})
        request.user = mock_user

        # Act
        response = mfa_api_views.disable_mfa(request)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data
        assert 'Invalid password or MFA code' in response.data['error']
        mock_mfa_manager.disable_mfa.assert_not_called()

    def test_disable_mfa_invalid_mfa_code(self, factory, mock_user, mock_mfa_manager):
        """Test disable_mfa rejects invalid MFA code."""
        # Arrange
        mock_mfa_manager.verify_totp.return_value = False
        mock_mfa_manager.verify_backup_code.return_value = False

        request = factory.post('/api/mfa/disable/', {'mfa_code': 'invalid'})
        request.user = mock_user

        # Act
        response = mfa_api_views.disable_mfa(request)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data
        mock_mfa_manager.disable_mfa.assert_not_called()

    def test_disable_mfa_no_credentials(self, factory, mock_user, mock_mfa_manager):
        """Test disable_mfa rejects when no credentials provided."""
        # Arrange
        request = factory.post('/api/mfa/disable/', {})
        request.user = mock_user

        # Act
        response = mfa_api_views.disable_mfa(request)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data
        mock_mfa_manager.disable_mfa.assert_not_called()


# ============================================================================
# MFA LOGIN CHALLENGE TESTS
# ============================================================================

class TestMFALoginSendCode:
    """Tests for mfa_login_send_code view."""

    def test_mfa_login_send_code_sms_success(self, factory, mock_mfa_manager):
        """Test successful SMS code sending for login."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 1
        mock_user.phone = '+14155551234'
        mock_user.phone_verified = True

        mock_mfa_manager.send_sms_code.return_value = (True, 'Code sent')

        with patch('smoothschedule.identity.users.models.User') as mock_user_model:
            mock_user_model.objects.get.return_value = mock_user

            request = factory.post('/api/mfa/login/send-code/', {
                'user_id': 1,
                'method': 'SMS'
            })

            # Act
            response = mfa_api_views.mfa_login_send_code(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            assert response.data['success'] is True
            assert response.data['method'] == 'SMS'
            assert '1234' in response.data['message']
            mock_mfa_manager.send_sms_code.assert_called_once_with(mock_user, purpose='LOGIN')

    def test_mfa_login_send_code_totp_success(self, factory):
        """Test TOTP method request (doesn't actually send anything)."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 1

        with patch('smoothschedule.identity.users.models.User') as mock_user_model:
            mock_user_model.objects.get.return_value = mock_user

            request = factory.post('/api/mfa/login/send-code/', {
                'user_id': 1,
                'method': 'TOTP'
            })

            # Act
            response = mfa_api_views.mfa_login_send_code(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            assert response.data['success'] is True
            assert response.data['method'] == 'TOTP'
            assert 'authenticator app' in response.data['message']

    def test_mfa_login_send_code_missing_user_id(self, factory):
        """Test mfa_login_send_code rejects missing user_id."""
        # Arrange
        request = factory.post('/api/mfa/login/send-code/', {'method': 'SMS'})

        # Act
        response = mfa_api_views.mfa_login_send_code(request)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data
        assert 'User ID is required' in response.data['error']

    def test_mfa_login_send_code_invalid_user(self, factory):
        """Test mfa_login_send_code handles invalid user."""
        # Arrange
        from smoothschedule.identity.users.models import User

        with patch('smoothschedule.identity.users.models.User') as mock_user_model:
            mock_user_model.DoesNotExist = User.DoesNotExist
            mock_user_model.objects.get.side_effect = User.DoesNotExist

            request = factory.post('/api/mfa/login/send-code/', {
                'user_id': 999,
                'method': 'SMS'
            })

            # Act
            response = mfa_api_views.mfa_login_send_code(request)

            # Assert
            assert response.status_code == status.HTTP_400_BAD_REQUEST
            assert 'error' in response.data
            assert 'Invalid user' in response.data['error']

    def test_mfa_login_send_code_sms_not_verified(self, factory):
        """Test mfa_login_send_code rejects SMS when phone not verified."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 1
        mock_user.phone_verified = False

        with patch('smoothschedule.identity.users.models.User') as mock_user_model:
            mock_user_model.objects.get.return_value = mock_user

            request = factory.post('/api/mfa/login/send-code/', {
                'user_id': 1,
                'method': 'SMS'
            })

            # Act
            response = mfa_api_views.mfa_login_send_code(request)

            # Assert
            assert response.status_code == status.HTTP_400_BAD_REQUEST
            assert 'error' in response.data
            assert 'SMS not available' in response.data['error']

    def test_mfa_login_send_code_sms_failure(self, factory, mock_mfa_manager):
        """Test mfa_login_send_code handles SMS sending failure."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 1
        mock_user.phone = '+14155551234'
        mock_user.phone_verified = True

        mock_mfa_manager.send_sms_code.return_value = (False, 'Twilio error')

        with patch('smoothschedule.identity.users.models.User') as mock_user_model:
            mock_user_model.objects.get.return_value = mock_user

            request = factory.post('/api/mfa/login/send-code/', {
                'user_id': 1,
                'method': 'SMS'
            })

            # Act
            response = mfa_api_views.mfa_login_send_code(request)

            # Assert
            assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
            assert 'error' in response.data

    def test_mfa_login_send_code_invalid_method(self, factory):
        """Test mfa_login_send_code rejects invalid method."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 1

        with patch('smoothschedule.identity.users.models.User') as mock_user_model:
            mock_user_model.objects.get.return_value = mock_user

            request = factory.post('/api/mfa/login/send-code/', {
                'user_id': 1,
                'method': 'INVALID'
            })

            # Act
            response = mfa_api_views.mfa_login_send_code(request)

            # Assert
            assert response.status_code == status.HTTP_400_BAD_REQUEST
            assert 'error' in response.data
            assert 'Invalid method' in response.data['error']

    def test_mfa_login_send_code_defaults_to_sms(self, factory, mock_mfa_manager):
        """Test mfa_login_send_code defaults to SMS method."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 1
        mock_user.phone = '+14155551234'
        mock_user.phone_verified = True

        mock_mfa_manager.send_sms_code.return_value = (True, 'Code sent')

        with patch('smoothschedule.identity.users.models.User') as mock_user_model:
            mock_user_model.objects.get.return_value = mock_user

            request = factory.post('/api/mfa/login/send-code/', {'user_id': 1})

            # Act
            response = mfa_api_views.mfa_login_send_code(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            assert response.data['method'] == 'SMS'


class TestMFALoginVerify:
    """Tests for mfa_login_verify view."""

    def test_mfa_login_verify_sms_success(self, factory, mock_mfa_manager):
        """Test successful MFA login verification with SMS."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 1
        mock_user.email = 'test@example.com'
        mock_user.username = 'testuser'
        mock_user.first_name = 'Test'
        mock_user.last_name = 'User'
        mock_user.full_name = 'Test User'
        mock_user.role = 'TENANT_OWNER'
        mock_user.mfa_enabled = True
        mock_user.tenant = None

        mock_verification = Mock()
        mock_verification.verify.return_value = True

        with patch('smoothschedule.identity.users.models.User') as mock_user_model, \
             patch('smoothschedule.identity.users.mfa_api_views.MFAVerificationCode') as mock_mvc:

            mock_user_model.objects.get.return_value = mock_user
            mock_mvc.objects.filter.return_value.order_by.return_value.first.return_value = mock_verification
            mock_mvc.Purpose.LOGIN = 'LOGIN'

            request = factory.post('/api/mfa/login/verify/', {
                'user_id': 1,
                'code': '123456',
                'method': 'SMS',
                'trust_device': False
            })

            # Act
            response = mfa_api_views.mfa_login_verify(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            assert response.data['success'] is True
            assert 'access' in response.data
            assert 'refresh' in response.data
            assert response.data['user']['id'] == 1
            assert response.data['user']['email'] == 'test@example.com'
            mock_verification.verify.assert_called_once_with('123456')

    def test_mfa_login_verify_totp_success(self, factory, mock_mfa_manager):
        """Test successful MFA login verification with TOTP."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 1
        mock_user.email = 'test@example.com'
        mock_user.username = 'testuser'
        mock_user.first_name = 'Test'
        mock_user.last_name = 'User'
        mock_user.full_name = 'Test User'
        mock_user.role = 'TENANT_OWNER'
        mock_user.mfa_enabled = True
        mock_user.tenant = None

        mock_mfa_manager.verify_totp.return_value = True

        with patch('smoothschedule.identity.users.models.User') as mock_user_model:

            mock_user_model.objects.get.return_value = mock_user

            request = factory.post('/api/mfa/login/verify/', {
                'user_id': 1,
                'code': '123456',
                'method': 'TOTP'
            })

            # Act
            response = mfa_api_views.mfa_login_verify(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            assert response.data['success'] is True
            mock_mfa_manager.verify_totp.assert_called_once_with(mock_user, '123456')

    def test_mfa_login_verify_backup_code_success(self, factory, mock_mfa_manager):
        """Test successful MFA login verification with backup code."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 1
        mock_user.email = 'test@example.com'
        mock_user.username = 'testuser'
        mock_user.first_name = 'Test'
        mock_user.last_name = 'User'
        mock_user.full_name = 'Test User'
        mock_user.role = 'TENANT_OWNER'
        mock_user.mfa_enabled = True
        mock_user.tenant = None

        mock_mfa_manager.verify_backup_code.return_value = True

        with patch('smoothschedule.identity.users.models.User') as mock_user_model:

            mock_user_model.objects.get.return_value = mock_user

            request = factory.post('/api/mfa/login/verify/', {
                'user_id': 1,
                'code': 'BACKUP123',
                'method': 'BACKUP'
            })

            # Act
            response = mfa_api_views.mfa_login_verify(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            assert response.data['success'] is True
            mock_mfa_manager.verify_backup_code.assert_called_once_with(mock_user, 'BACKUP123')

    def test_mfa_login_verify_with_trust_device(self, factory, mock_mfa_manager):
        """Test MFA login verification trusts device when requested."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 1
        mock_user.email = 'test@example.com'
        mock_user.username = 'testuser'
        mock_user.first_name = 'Test'
        mock_user.last_name = 'User'
        mock_user.full_name = 'Test User'
        mock_user.role = 'TENANT_OWNER'
        mock_user.mfa_enabled = True
        mock_user.tenant = None

        mock_mfa_manager.verify_totp.return_value = True

        with patch('smoothschedule.identity.users.models.User') as mock_user_model:

            mock_user_model.objects.get.return_value = mock_user

            request = factory.post('/api/mfa/login/verify/', {
                'user_id': 1,
                'code': '123456',
                'method': 'TOTP',
                'trust_device': True
            })

            # Act
            response = mfa_api_views.mfa_login_verify(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            # Check that trust_device was called with the user (request object may be wrapped)
            mock_mfa_manager.trust_device.assert_called_once()
            assert mock_mfa_manager.trust_device.call_args[0][0] == mock_user

    def test_mfa_login_verify_with_tenant_subdomain(self, factory, mock_mfa_manager):
        """Test MFA login verification includes tenant subdomain."""
        # Arrange
        mock_domain = Mock()
        mock_domain.domain = 'demo.smoothschedule.com'

        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_tenant.schema_name = 'demo'
        mock_tenant.domains.filter.return_value.first.return_value = mock_domain

        mock_user = Mock()
        mock_user.id = 1
        mock_user.email = 'test@example.com'
        mock_user.username = 'testuser'
        mock_user.first_name = 'Test'
        mock_user.last_name = 'User'
        mock_user.full_name = 'Test User'
        mock_user.role = 'TENANT_OWNER'
        mock_user.mfa_enabled = True
        mock_user.tenant = mock_tenant

        mock_mfa_manager.verify_totp.return_value = True

        with patch('smoothschedule.identity.users.models.User') as mock_user_model:

            mock_user_model.objects.get.return_value = mock_user

            request = factory.post('/api/mfa/login/verify/', {
                'user_id': 1,
                'code': '123456',
                'method': 'TOTP'
            })

            # Act
            response = mfa_api_views.mfa_login_verify(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            assert response.data['user']['business_subdomain'] == 'demo'

    def test_mfa_login_verify_missing_user_id(self, factory):
        """Test mfa_login_verify rejects missing user_id."""
        # Arrange
        request = factory.post('/api/mfa/login/verify/', {
            'code': '123456',
            'method': 'TOTP'
        })

        # Act
        response = mfa_api_views.mfa_login_verify(request)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data
        assert 'User ID and code are required' in response.data['error']

    def test_mfa_login_verify_missing_code(self, factory):
        """Test mfa_login_verify rejects missing code."""
        # Arrange
        request = factory.post('/api/mfa/login/verify/', {
            'user_id': 1,
            'method': 'TOTP'
        })

        # Act
        response = mfa_api_views.mfa_login_verify(request)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data

    def test_mfa_login_verify_invalid_user(self, factory, mock_mfa_manager):
        """Test mfa_login_verify handles invalid user."""
        # Arrange
        # The verify methods will return False, causing "Invalid verification code" error
        # This demonstrates the view handles bad input gracefully
        mock_mfa_manager.verify_totp.return_value = False

        with patch('smoothschedule.identity.users.models.User') as mock_user_model:
            mock_user_model.objects.get.return_value = Mock(id=999)

            request = factory.post('/api/mfa/login/verify/', {
                'user_id': 999,
                'code': '123456',
                'method': 'TOTP'
            })

            # Act
            response = mfa_api_views.mfa_login_verify(request)

            # Assert
            assert response.status_code == status.HTTP_400_BAD_REQUEST
            assert 'error' in response.data

    def test_mfa_login_verify_invalid_code(self, factory, mock_mfa_manager):
        """Test mfa_login_verify handles invalid verification code."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 1

        mock_mfa_manager.verify_totp.return_value = False

        with patch('smoothschedule.identity.users.models.User') as mock_user_model:
            mock_user_model.objects.get.return_value = mock_user

            request = factory.post('/api/mfa/login/verify/', {
                'user_id': 1,
                'code': '123456',
                'method': 'TOTP'
            })

            # Act
            response = mfa_api_views.mfa_login_verify(request)

            # Assert
            assert response.status_code == status.HTTP_400_BAD_REQUEST
            assert 'error' in response.data
            assert 'Invalid verification code' in response.data['error']

    def test_mfa_login_verify_defaults_to_totp(self, factory, mock_mfa_manager):
        """Test mfa_login_verify defaults to TOTP method."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 1
        mock_user.email = 'test@example.com'
        mock_user.username = 'testuser'
        mock_user.first_name = 'Test'
        mock_user.last_name = 'User'
        mock_user.full_name = 'Test User'
        mock_user.role = 'TENANT_OWNER'
        mock_user.mfa_enabled = True
        mock_user.tenant = None

        mock_mfa_manager.verify_totp.return_value = True

        with patch('smoothschedule.identity.users.models.User') as mock_user_model:

            mock_user_model.objects.get.return_value = mock_user

            request = factory.post('/api/mfa/login/verify/', {
                'user_id': 1,
                'code': '123456'
            })

            # Act
            response = mfa_api_views.mfa_login_verify(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            mock_mfa_manager.verify_totp.assert_called_once()

    def test_mfa_login_verify_strips_whitespace_from_code(self, factory, mock_mfa_manager):
        """Test mfa_login_verify strips whitespace from code."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 1
        mock_user.email = 'test@example.com'
        mock_user.username = 'testuser'
        mock_user.first_name = 'Test'
        mock_user.last_name = 'User'
        mock_user.full_name = 'Test User'
        mock_user.role = 'TENANT_OWNER'
        mock_user.mfa_enabled = True
        mock_user.tenant = None

        mock_mfa_manager.verify_totp.return_value = True

        with patch('smoothschedule.identity.users.models.User') as mock_user_model:

            mock_user_model.objects.get.return_value = mock_user

            request = factory.post('/api/mfa/login/verify/', {
                'user_id': 1,
                'code': '  123456  ',
                'method': 'TOTP'
            })

            # Act
            response = mfa_api_views.mfa_login_verify(request)

            # Assert
            mock_mfa_manager.verify_totp.assert_called_once_with(mock_user, '123456')


# ============================================================================
# TRUSTED DEVICES TESTS
# ============================================================================

class TestListTrustedDevices:
    """Tests for list_trusted_devices view."""

    def test_list_trusted_devices_success(self, factory, mock_user, mock_mfa_manager):
        """Test successful listing of trusted devices."""
        # Arrange
        device1 = Mock()
        device1.id = 1
        device1.name = 'Chrome on MacBook'
        device1.ip_address = '192.168.1.1'
        device1.created_at = timezone.now()
        device1.last_used_at = timezone.now()
        device1.expires_at = timezone.now() + timedelta(days=30)
        device1.device_hash = 'hash1'

        device2 = Mock()
        device2.id = 2
        device2.name = 'Firefox on Linux'
        device2.ip_address = '192.168.1.2'
        device2.created_at = timezone.now()
        device2.last_used_at = timezone.now()
        device2.expires_at = timezone.now() + timedelta(days=30)
        device2.device_hash = 'hash2'

        with patch('smoothschedule.identity.users.mfa_api_views.TrustedDevice') as mock_td:
            mock_td.objects.filter.return_value = [device1, device2]
            mock_mfa_manager.device_service.generate_device_hash.return_value = 'hash1'
            mock_mfa_manager._get_client_ip.return_value = '192.168.1.1'

            request = factory.get('/api/mfa/trusted-devices/')
            request.user = mock_user
            request.META = {'HTTP_USER_AGENT': 'Chrome/90.0'}

            # Act
            response = mfa_api_views.list_trusted_devices(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            assert len(response.data['devices']) == 2
            assert response.data['devices'][0]['id'] == 1
            assert response.data['devices'][0]['is_current'] is True
            assert response.data['devices'][1]['is_current'] is False

    def test_list_trusted_devices_empty(self, factory, mock_user):
        """Test listing trusted devices when none exist."""
        # Arrange
        with patch('smoothschedule.identity.users.mfa_api_views.TrustedDevice') as mock_td:
            mock_td.objects.filter.return_value = []

            request = factory.get('/api/mfa/trusted-devices/')
            request.user = mock_user
            request.META = {}

            # Act
            response = mfa_api_views.list_trusted_devices(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            assert len(response.data['devices']) == 0


class TestRevokeTrustedDevice:
    """Tests for revoke_trusted_device view."""

    def test_revoke_trusted_device_success(self, factory, mock_user):
        """Test successful device revocation."""
        # Arrange
        mock_device = Mock()
        mock_device.id = 1
        mock_device.delete = Mock()

        with patch('smoothschedule.identity.users.mfa_api_views.TrustedDevice') as mock_td:
            mock_td.objects.get.return_value = mock_device

            request = factory.delete('/api/mfa/trusted-devices/1/')
            request.user = mock_user

            # Act
            response = mfa_api_views.revoke_trusted_device(request, device_id=1)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            assert response.data['success'] is True
            assert 'message' in response.data
            mock_device.delete.assert_called_once()
            mock_td.objects.get.assert_called_once_with(id=1, user=mock_user)

    def test_revoke_trusted_device_not_found(self, factory, mock_user):
        """Test revoking non-existent device."""
        # Arrange
        from smoothschedule.identity.users.models import TrustedDevice

        with patch('smoothschedule.identity.users.mfa_api_views.TrustedDevice') as mock_td:
            mock_td.DoesNotExist = TrustedDevice.DoesNotExist
            mock_td.objects.get.side_effect = TrustedDevice.DoesNotExist

            request = factory.delete('/api/mfa/trusted-devices/999/')
            request.user = mock_user

            # Act
            response = mfa_api_views.revoke_trusted_device(request, device_id=999)

            # Assert
            assert response.status_code == status.HTTP_404_NOT_FOUND
            assert 'error' in response.data
            assert 'Device not found' in response.data['error']


class TestRevokeAllTrustedDevices:
    """Tests for revoke_all_trusted_devices view."""

    def test_revoke_all_trusted_devices_success(self, factory, mock_user):
        """Test successful revocation of all devices."""
        # Arrange
        with patch('smoothschedule.identity.users.mfa_api_views.TrustedDevice') as mock_td:
            mock_td.objects.filter.return_value.delete.return_value = (3, {})

            request = factory.delete('/api/mfa/trusted-devices/all/')
            request.user = mock_user

            # Act
            response = mfa_api_views.revoke_all_trusted_devices(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            assert response.data['success'] is True
            assert response.data['count'] == 3
            assert '3 device(s) revoked' in response.data['message']
            mock_td.objects.filter.assert_called_once_with(user=mock_user)

    def test_revoke_all_trusted_devices_none_exist(self, factory, mock_user):
        """Test revoking all devices when none exist."""
        # Arrange
        with patch('smoothschedule.identity.users.mfa_api_views.TrustedDevice') as mock_td:
            mock_td.objects.filter.return_value.delete.return_value = (0, {})

            request = factory.delete('/api/mfa/trusted-devices/all/')
            request.user = mock_user

            # Act
            response = mfa_api_views.revoke_all_trusted_devices(request)

            # Assert
            assert response.status_code == status.HTTP_200_OK
            assert response.data['count'] == 0
            assert '0 device(s) revoked' in response.data['message']
