"""
Comprehensive unit tests for users/api_views.py
Tests all views, actions, permissions, and business logic using mocks.
NO database access - uses APIRequestFactory with mocked authentication.
"""
from datetime import datetime, timedelta
from unittest.mock import Mock, patch, MagicMock, call
from django.utils import timezone
from django.conf import settings
from rest_framework import status
from rest_framework.test import APIRequestFactory
from rest_framework.authtoken.models import Token
import pytest

from smoothschedule.identity.users import api_views
from smoothschedule.identity.users.models import User, EmailVerificationToken, StaffInvitation


# ============================================================================
# Helper Functions Tests
# ============================================================================

class TestGetClientIp:
    """Test _get_client_ip helper function"""

    def test_extracts_from_x_forwarded_for_single_ip(self):
        request = Mock()
        request.META = {'HTTP_X_FORWARDED_FOR': '192.168.1.1'}

        result = api_views._get_client_ip(request)

        assert result == '192.168.1.1'

    def test_extracts_first_ip_from_x_forwarded_for_chain(self):
        request = Mock()
        request.META = {'HTTP_X_FORWARDED_FOR': '192.168.1.1, 10.0.0.1, 172.16.0.1'}

        result = api_views._get_client_ip(request)

        assert result == '192.168.1.1'

    def test_strips_whitespace_from_ip(self):
        request = Mock()
        request.META = {'HTTP_X_FORWARDED_FOR': '  192.168.1.1  '}

        result = api_views._get_client_ip(request)

        assert result == '192.168.1.1'

    def test_falls_back_to_remote_addr_when_no_x_forwarded_for(self):
        request = Mock()
        request.META = {'REMOTE_ADDR': '10.0.0.5'}

        result = api_views._get_client_ip(request)

        assert result == '10.0.0.5'

    def test_returns_empty_string_when_no_ip_available(self):
        request = Mock()
        request.META = {}

        result = api_views._get_client_ip(request)

        assert result == ''


class TestGetUserData:
    """Test _get_user_data helper function"""

    @patch('smoothschedule.identity.users.api_views.Resource')
    @patch('smoothschedule.identity.users.api_views.schema_context')
    def test_returns_complete_user_data_for_tenant_owner(self, mock_schema_context, mock_resource):
        # Arrange
        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_tenant.name = 'Test Business'
        mock_tenant.schema_name = 'testbiz'

        mock_domain = Mock()
        mock_domain.domain = 'testbiz.lvh.me'
        mock_tenant.domains.filter.return_value.first.return_value = mock_domain

        mock_user = Mock()
        mock_user.id = 100
        mock_user.username = 'owner@test.com'
        mock_user.email = 'owner@test.com'
        mock_user.full_name = 'John Owner'
        mock_user.role = User.Role.TENANT_OWNER
        mock_user.email_verified = True
        mock_user.is_staff = False
        mock_user.is_superuser = False
        mock_user.is_active = True
        mock_user.tenant = mock_tenant
        mock_user.tenant_id = 1
        mock_user.permissions = {'can_invite_staff': True}
        mock_user.can_invite_staff.return_value = True
        mock_user.can_access_tickets.return_value = True

        # Mock linked resource
        mock_linked_resource = Mock()
        mock_linked_resource.id = 50
        mock_linked_resource.user_can_edit_schedule = True
        mock_resource.objects.filter.return_value.first.return_value = mock_linked_resource

        # Act
        result = api_views._get_user_data(mock_user)

        # Assert
        assert result['id'] == 100
        assert result['username'] == 'owner@test.com'
        assert result['email'] == 'owner@test.com'
        assert result['name'] == 'John Owner'
        assert result['role'] == 'owner'
        assert result['email_verified'] is True
        assert result['business'] == 1
        assert result['business_name'] == 'Test Business'
        assert result['business_subdomain'] == 'testbiz'
        assert result['permissions'] == {'can_invite_staff': True}
        assert result['can_invite_staff'] is True
        assert result['can_access_tickets'] is True
        assert result['linked_resource_id'] == 50
        assert result['can_edit_schedule'] is True

    def test_handles_user_without_tenant(self):
        mock_user = Mock()
        mock_user.id = 1
        mock_user.username = 'platform@admin.com'
        mock_user.email = 'platform@admin.com'
        mock_user.full_name = 'Platform Admin'
        mock_user.role = User.Role.SUPERUSER
        mock_user.email_verified = True
        mock_user.is_staff = True
        mock_user.is_superuser = True
        mock_user.is_active = True
        mock_user.tenant = None
        mock_user.tenant_id = None
        mock_user.permissions = {}
        mock_user.can_invite_staff.return_value = False
        mock_user.can_access_tickets.return_value = True

        result = api_views._get_user_data(mock_user)

        assert result['business'] is None
        assert result['business_name'] is None
        assert result['business_subdomain'] is None
        assert result['linked_resource_id'] is None
        assert result['can_edit_schedule'] is False

    @patch('smoothschedule.identity.users.api_views.Resource')
    @patch('smoothschedule.identity.users.api_views.schema_context')
    def test_handles_resource_query_exception(self, mock_schema_context, mock_resource):
        mock_tenant = Mock()
        mock_tenant.schema_name = 'testbiz'

        # Mock domain properly to avoid dict subscript issues
        mock_domain = Mock()
        mock_domain.domain = 'testbiz.lvh.me'
        mock_tenant.domains.filter.return_value.first.return_value = mock_domain

        mock_user = Mock()
        mock_user.id = 1
        mock_user.role = User.Role.TENANT_STAFF
        mock_user.tenant = mock_tenant
        mock_user.tenant_id = 1
        mock_user.full_name = 'Staff Member'
        mock_user.username = 'staff@test.com'
        mock_user.email = 'staff@test.com'
        mock_user.email_verified = True
        mock_user.is_staff = False
        mock_user.is_superuser = False
        mock_user.is_active = True
        mock_user.permissions = {}
        mock_user.can_invite_staff.return_value = False
        mock_user.can_access_tickets.return_value = False

        # Simulate exception when querying resources
        mock_resource.objects.filter.side_effect = Exception('Database error')

        result = api_views._get_user_data(mock_user)

        # Should gracefully handle error
        assert result['linked_resource_id'] is None
        assert result['can_edit_schedule'] is False

    def test_maps_all_role_types_correctly(self):
        role_tests = [
            (User.Role.SUPERUSER, 'superuser'),
            (User.Role.PLATFORM_MANAGER, 'platform_manager'),
            (User.Role.PLATFORM_SALES, 'platform_sales'),
            (User.Role.PLATFORM_SUPPORT, 'platform_support'),
            (User.Role.TENANT_OWNER, 'owner'),
            (User.Role.TENANT_MANAGER, 'manager'),
            (User.Role.TENANT_STAFF, 'staff'),
            (User.Role.CUSTOMER, 'customer'),
        ]

        for db_role, expected_frontend_role in role_tests:
            mock_user = Mock()
            mock_user.role = db_role
            mock_user.tenant = None
            mock_user.permissions = {}
            mock_user.can_invite_staff.return_value = False
            mock_user.can_access_tickets.return_value = False

            result = api_views._get_user_data(mock_user)
            assert result['role'] == expected_frontend_role


# ============================================================================
# Login View Tests
# ============================================================================

class TestLoginView:
    """Test login_view function"""

    @patch('smoothschedule.identity.users.api_views.authenticate')
    @patch('smoothschedule.identity.users.api_views.User')
    @patch('smoothschedule.identity.users.api_views.Token')
    @patch('smoothschedule.identity.users.api_views.mfa_manager')
    @patch('smoothschedule.identity.users.api_views._get_user_data')
    def test_successful_login_without_mfa(self, mock_get_user_data, mock_mfa_manager,
                                         mock_token_model, mock_user_model, mock_authenticate):
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/auth/login/', {
            'email': 'user@example.com',
            'password': 'testpass123'
        })

        mock_user = Mock()
        mock_user.id = 1
        mock_user.username = 'user@example.com'
        mock_user.is_active = True

        mock_user_model.objects.get.return_value = mock_user
        mock_authenticate.return_value = mock_user
        mock_mfa_manager.requires_mfa.return_value = False

        mock_token = Mock()
        mock_token.key = 'test-token-key'
        mock_token_model.objects.get_or_create.return_value = (mock_token, True)

        mock_get_user_data.return_value = {'id': 1, 'email': 'user@example.com'}

        # Act
        response = api_views.login_view(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['access'] == 'test-token-key'
        assert response.data['refresh'] == 'test-token-key'
        assert 'user' in response.data
        mock_user.save.assert_called_once()

    def test_login_missing_email(self):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/login/', {
            'password': 'testpass123'
        })

        response = api_views.login_view(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'Email and password are required' in response.data['error']

    def test_login_missing_password(self):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/login/', {
            'email': 'user@example.com'
        })

        response = api_views.login_view(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'Email and password are required' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.User')
    def test_login_user_not_found(self, mock_user_model):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/login/', {
            'email': 'nonexistent@example.com',
            'password': 'testpass123'
        })

        # Create a proper DoesNotExist exception class
        mock_user_model.DoesNotExist = type('DoesNotExist', (Exception,), {})
        mock_user_model.objects.get.side_effect = mock_user_model.DoesNotExist

        response = api_views.login_view(request)

        assert response.status_code == status.HTTP_401_UNAUTHORIZED
        assert 'Invalid credentials' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.authenticate')
    @patch('smoothschedule.identity.users.api_views.User')
    def test_login_invalid_password(self, mock_user_model, mock_authenticate):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/login/', {
            'email': 'user@example.com',
            'password': 'wrongpassword'
        })

        mock_user = Mock()
        mock_user.username = 'user@example.com'
        mock_user_model.objects.get.return_value = mock_user
        mock_authenticate.return_value = None

        response = api_views.login_view(request)

        assert response.status_code == status.HTTP_401_UNAUTHORIZED
        assert 'Invalid credentials' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.authenticate')
    @patch('smoothschedule.identity.users.api_views.User')
    def test_login_inactive_user(self, mock_user_model, mock_authenticate):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/login/', {
            'email': 'inactive@example.com',
            'password': 'testpass123'
        })

        mock_user_db = Mock()
        mock_user_db.username = 'inactive@example.com'
        mock_user_model.objects.get.return_value = mock_user_db

        mock_user = Mock()
        mock_user.is_active = False
        mock_authenticate.return_value = mock_user

        response = api_views.login_view(request)

        assert response.status_code == status.HTTP_401_UNAUTHORIZED
        assert 'Account is disabled' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.authenticate')
    @patch('smoothschedule.identity.users.api_views.User')
    @patch('smoothschedule.identity.users.api_views.mfa_manager')
    def test_login_requires_mfa_not_trusted_device(self, mock_mfa_manager,
                                                    mock_user_model, mock_authenticate):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/login/', {
            'email': 'mfa@example.com',
            'password': 'testpass123'
        })

        mock_user = Mock()
        mock_user.id = 1
        mock_user.username = 'mfa@example.com'
        mock_user.is_active = True
        mock_user.phone = '+14155551234'

        mock_user_model.objects.get.return_value = mock_user
        mock_authenticate.return_value = mock_user
        mock_mfa_manager.requires_mfa.return_value = True
        mock_mfa_manager.is_device_trusted.return_value = False
        mock_mfa_manager.get_available_methods.return_value = ['SMS', 'TOTP']

        response = api_views.login_view(request)

        assert response.status_code == status.HTTP_200_OK
        assert response.data['mfa_required'] is True
        assert response.data['user_id'] == 1
        assert response.data['mfa_methods'] == ['SMS', 'TOTP']
        assert response.data['phone_last_4'] == '1234'

    @patch('smoothschedule.identity.users.api_views.authenticate')
    @patch('smoothschedule.identity.users.api_views.User')
    @patch('smoothschedule.identity.users.api_views.Token')
    @patch('smoothschedule.identity.users.api_views.mfa_manager')
    @patch('smoothschedule.identity.users.api_views._get_user_data')
    def test_login_mfa_enabled_but_device_trusted(self, mock_get_user_data, mock_mfa_manager,
                                                  mock_token_model, mock_user_model, mock_authenticate):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/login/', {
            'email': 'mfa@example.com',
            'password': 'testpass123'
        })

        mock_user = Mock()
        mock_user.id = 1
        mock_user.username = 'mfa@example.com'
        mock_user.is_active = True

        mock_user_model.objects.get.return_value = mock_user
        mock_authenticate.return_value = mock_user
        mock_mfa_manager.requires_mfa.return_value = True
        mock_mfa_manager.is_device_trusted.return_value = True

        mock_token = Mock()
        mock_token.key = 'test-token'
        mock_token_model.objects.get_or_create.return_value = (mock_token, True)

        mock_get_user_data.return_value = {'id': 1}

        response = api_views.login_view(request)

        assert response.status_code == status.HTTP_200_OK
        assert 'access' in response.data
        assert 'mfa_required' not in response.data

    def test_login_accepts_username_field_for_backward_compatibility(self):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/login/', {
            'username': 'user@example.com',  # Using 'username' instead of 'email'
            'password': 'testpass123'
        })

        with patch('smoothschedule.identity.users.api_views.User') as mock_user_model:
            mock_user_model.DoesNotExist = type('DoesNotExist', (Exception,), {})
            mock_user_model.objects.get.side_effect = mock_user_model.DoesNotExist
            response = api_views.login_view(request)

            # Should process the username field as email
            mock_user_model.objects.get.assert_called_once()
            call_args = mock_user_model.objects.get.call_args
            assert 'user@example.com' in str(call_args)

    @patch('smoothschedule.identity.users.api_views.authenticate')
    @patch('smoothschedule.identity.users.api_views.User')
    @patch('smoothschedule.identity.users.api_views.Token')
    @patch('smoothschedule.identity.users.api_views.mfa_manager')
    @patch('smoothschedule.identity.users.api_views._get_user_data')
    @patch('smoothschedule.identity.users.api_views._get_client_ip')
    def test_login_updates_last_login_ip(self, mock_get_client_ip, mock_get_user_data,
                                        mock_mfa_manager, mock_token_model,
                                        mock_user_model, mock_authenticate):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/login/', {
            'email': 'user@example.com',
            'password': 'testpass123'
        })

        mock_user = Mock()
        mock_user.is_active = True
        mock_user_model.objects.get.return_value = mock_user
        mock_authenticate.return_value = mock_user
        mock_mfa_manager.requires_mfa.return_value = False

        mock_token = Mock()
        mock_token.key = 'test-token'
        mock_token_model.objects.get_or_create.return_value = (mock_token, True)

        mock_get_client_ip.return_value = '192.168.1.100'
        mock_get_user_data.return_value = {}

        response = api_views.login_view(request)

        assert mock_user.last_login_ip == '192.168.1.100'
        mock_user.save.assert_called_once_with(update_fields=['last_login_ip'])


# ============================================================================
# Current User View Tests
# ============================================================================

class TestCurrentUserView:
    """Test current_user_view function"""

    @patch('smoothschedule.identity.users.api_views.Resource')
    @patch('smoothschedule.identity.users.api_views.schema_context')
    def test_returns_current_user_data(self, mock_schema_context, mock_resource):
        factory = APIRequestFactory()
        request = factory.get('/api/auth/me/')

        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_tenant.name = 'Test Business'
        mock_tenant.schema_name = 'testbiz'

        mock_domain = Mock()
        mock_domain.domain = 'testbiz.lvh.me'
        mock_tenant.domains.filter.return_value.first.return_value = mock_domain

        mock_user = Mock()
        mock_user.id = 10
        mock_user.username = 'testuser'
        mock_user.email = 'test@example.com'
        mock_user.full_name = 'Test User'
        mock_user.role = User.Role.CUSTOMER  # Use customer role to avoid quota check
        mock_user.email_verified = True
        mock_user.is_staff = False
        mock_user.is_superuser = False
        mock_user.is_active = True
        mock_user.tenant = mock_tenant
        mock_user.tenant_id = 1
        mock_user.permissions = {}
        mock_user.can_invite_staff.return_value = False
        mock_user.can_access_tickets.return_value = True

        request.user = mock_user

        # Mock resource
        mock_resource.objects.filter.return_value.first.return_value = None

        response = api_views.current_user_view(request)

        assert response.status_code == status.HTTP_200_OK
        assert response.data['id'] == 10
        assert response.data['email'] == 'test@example.com'
        assert response.data['role'] == 'customer'
        assert response.data['business_name'] == 'Test Business'
        assert response.data['business_subdomain'] == 'testbiz'

    def test_returns_user_without_tenant(self):
        factory = APIRequestFactory()
        request = factory.get('/api/auth/me/')

        mock_user = Mock()
        mock_user.id = 1
        mock_user.username = 'admin'
        mock_user.email = 'admin@platform.com'
        mock_user.full_name = 'Platform Admin'
        mock_user.role = User.Role.SUPERUSER
        mock_user.email_verified = True
        mock_user.is_staff = True
        mock_user.is_superuser = True
        mock_user.is_active = True
        mock_user.tenant = None
        mock_user.tenant_id = None
        mock_user.permissions = {}
        mock_user.can_invite_staff.return_value = False
        mock_user.can_access_tickets.return_value = True

        request.user = mock_user

        response = api_views.current_user_view(request)

        assert response.status_code == status.HTTP_200_OK
        assert response.data['business_name'] is None
        assert response.data['business_subdomain'] is None

    @patch('smoothschedule.identity.users.api_views.Resource')
    @patch('smoothschedule.identity.users.api_views.schema_context')
    def test_includes_quota_overages_for_owner(self, mock_schema_context, mock_resource):
        factory = APIRequestFactory()
        request = factory.get('/api/auth/me/')

        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_tenant.name = 'Test Business'
        mock_tenant.schema_name = 'testbiz'

        mock_domain = Mock()
        mock_domain.domain = 'testbiz.lvh.me'
        mock_tenant.domains.filter.return_value.first.return_value = mock_domain

        mock_user = Mock()
        mock_user.id = 1
        mock_user.username = 'owner'
        mock_user.email = 'owner@test.com'
        mock_user.full_name = 'Owner'
        mock_user.role = User.Role.TENANT_OWNER
        mock_user.email_verified = True
        mock_user.is_staff = False
        mock_user.is_superuser = False
        mock_user.is_active = True
        mock_user.tenant = mock_tenant
        mock_user.tenant_id = 1
        mock_user.permissions = {}
        mock_user.can_invite_staff.return_value = True
        mock_user.can_access_tickets.return_value = True

        request.user = mock_user

        # Mock resource query
        mock_resource.objects.filter.return_value.first.return_value = None

        # Patch QuotaService where it's imported (inside the view function)
        with patch('smoothschedule.identity.core.quota_service.QuotaService') as mock_quota_service:
            mock_service = Mock()
            mock_service.get_active_overages.return_value = [
                {'resource': 'staff', 'limit': 5, 'current': 7}
            ]
            mock_quota_service.return_value = mock_service

            response = api_views.current_user_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert len(response.data['quota_overages']) == 1
            assert response.data['quota_overages'][0]['resource'] == 'staff'

    @patch('smoothschedule.identity.users.api_views.Resource')
    @patch('smoothschedule.identity.users.api_views.schema_context')
    def test_handles_quota_service_exception_gracefully(self, mock_schema_context, mock_resource):
        factory = APIRequestFactory()
        request = factory.get('/api/auth/me/')

        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_tenant.schema_name = 'testbiz'

        mock_domain = Mock()
        mock_domain.domain = 'testbiz.lvh.me'
        mock_tenant.domains.filter.return_value.first.return_value = mock_domain

        mock_user = Mock()
        mock_user.id = 1
        mock_user.username = 'owner'
        mock_user.email = 'owner@test.com'
        mock_user.full_name = 'Owner'
        mock_user.role = User.Role.TENANT_OWNER
        mock_user.email_verified = True
        mock_user.is_staff = False
        mock_user.is_superuser = False
        mock_user.is_active = True
        mock_user.tenant = mock_tenant
        mock_user.tenant_id = 1
        mock_user.permissions = {}
        mock_user.can_invite_staff.return_value = True
        mock_user.can_access_tickets.return_value = True

        request.user = mock_user

        # Mock resource query
        mock_resource.objects.filter.return_value.first.return_value = None

        # Simulate quota service failure by patching where it's imported
        with patch('smoothschedule.identity.core.quota_service.QuotaService') as mock_quota_service:
            mock_quota_service.side_effect = Exception('Service unavailable')

            response = api_views.current_user_view(request)

            # Should not fail, just return empty overages
            assert response.status_code == status.HTTP_200_OK
            assert response.data['quota_overages'] == []


# ============================================================================
# Logout View Tests
# ============================================================================

class TestLogoutView:
    """Test logout_view function"""

    def test_logout_success(self):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/logout/')
        request.user = Mock()

        with patch('django.contrib.auth.logout') as mock_logout:
            response = api_views.logout_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert 'Successfully logged out' in response.data['detail']
            # Check that logout was called
            assert mock_logout.called


# ============================================================================
# Email Verification Tests
# ============================================================================

class TestSendVerificationEmail:
    """Test send_verification_email view"""

    def test_rejects_already_verified_email(self):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/email/verify/send/')

        mock_user = Mock()
        mock_user.email_verified = True
        request.user = mock_user

        response = api_views.send_verification_email(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'already verified' in response.data['detail']

    @patch('smoothschedule.identity.users.api_views.EmailVerificationToken')
    @patch('smoothschedule.identity.users.api_views.send_mail')
    @patch('smoothschedule.identity.users.api_views.settings')
    def test_creates_token_and_sends_email(self, mock_settings, mock_send_mail,
                                          mock_token_model):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/email/verify/send/')

        mock_tenant = Mock()
        mock_domain = Mock()
        mock_domain.domain = 'testbiz.lvh.me'
        mock_tenant.domains.filter.return_value.first.return_value = mock_domain

        mock_user = Mock()
        mock_user.email_verified = False
        mock_user.email = 'user@example.com'
        mock_user.full_name = 'Test User'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_token = Mock()
        mock_token.token = 'test-token-123'
        mock_token_model.create_for_user.return_value = mock_token

        mock_settings.DEBUG = True
        mock_settings.DEFAULT_FROM_EMAIL = 'noreply@test.com'

        response = api_views.send_verification_email(request)

        assert response.status_code == status.HTTP_200_OK
        assert 'Verification email sent' in response.data['detail']
        mock_token_model.create_for_user.assert_called_once_with(mock_user)
        mock_send_mail.assert_called_once()

    @patch('smoothschedule.identity.users.api_views.EmailVerificationToken')
    @patch('smoothschedule.identity.users.api_views.send_mail')
    @patch('smoothschedule.identity.users.api_views.settings')
    def test_handles_user_without_tenant(self, mock_settings, mock_send_mail, mock_token_model):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/email/verify/send/')

        mock_user = Mock()
        mock_user.email_verified = False
        mock_user.email = 'user@example.com'
        mock_user.full_name = 'Test User'
        mock_user.tenant = None
        request.user = mock_user

        mock_token = Mock()
        mock_token.token = 'test-token-123'
        mock_token_model.create_for_user.return_value = mock_token

        mock_settings.DEBUG = True
        mock_settings.DEFAULT_FROM_EMAIL = 'noreply@test.com'

        response = api_views.send_verification_email(request)

        assert response.status_code == status.HTTP_200_OK
        # Verify URL doesn't have subdomain
        email_body = mock_send_mail.call_args[0][1]
        assert 'http://lvh.me:5173/verify-email?token=' in email_body


class TestVerifyEmail:
    """Test verify_email view"""

    def test_missing_token(self):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/email/verify/', {})

        response = api_views.verify_email(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'Token is required' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.EmailVerificationToken')
    def test_invalid_token(self, mock_token_model):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/email/verify/', {
            'token': 'invalid-token'
        })

        mock_token_model.DoesNotExist = type('DoesNotExist', (Exception,), {})
        mock_token_model.objects.get.side_effect = mock_token_model.DoesNotExist

        response = api_views.verify_email(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'Invalid token' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.EmailVerificationToken')
    def test_expired_token(self, mock_token_model):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/email/verify/', {
            'token': 'expired-token'
        })

        mock_token = Mock()
        mock_token.is_valid.return_value = False
        mock_token_model.objects.get.return_value = mock_token

        response = api_views.verify_email(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'expired' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.EmailVerificationToken')
    def test_successful_verification(self, mock_token_model):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/email/verify/', {
            'token': 'valid-token'
        })

        mock_user = Mock()
        mock_user.email_verified = False

        mock_token = Mock()
        mock_token.is_valid.return_value = True
        mock_token.used = False
        mock_token.user = mock_user
        mock_token_model.objects.get.return_value = mock_token

        response = api_views.verify_email(request)

        assert response.status_code == status.HTTP_200_OK
        assert 'verified successfully' in response.data['detail']
        assert mock_token.used is True
        mock_token.save.assert_called_once()
        assert mock_user.email_verified is True
        mock_user.save.assert_called_once_with(update_fields=['email_verified'])


# ============================================================================
# Hijack/Masquerade Tests
# ============================================================================

class TestHijackAcquireView:
    """Test hijack_acquire_view function"""

    def test_missing_user_pk(self):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/hijack/acquire/', {})
        request.user = Mock()

        response = api_views.hijack_acquire_view(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'user_pk is required' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    @patch('smoothschedule.identity.users.api_views.can_hijack')
    def test_permission_denied_when_cannot_hijack(self, mock_can_hijack, mock_get_object):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/hijack/acquire/', {
            'user_pk': 100,
            'hijack_history': []
        })

        mock_hijacker = Mock()
        mock_hijacker.id = 1
        mock_hijacker.email = 'admin@test.com'
        request.user = mock_hijacker

        mock_hijacked = Mock()
        mock_hijacked.id = 100
        mock_get_object.return_value = mock_hijacked

        mock_can_hijack.return_value = False

        response = api_views.hijack_acquire_view(request)

        assert response.status_code == status.HTTP_403_FORBIDDEN
        assert 'permission' in response.data['error'].lower()

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    @patch('smoothschedule.identity.users.api_views.can_hijack')
    @patch('smoothschedule.identity.users.api_views.Token')
    @patch('smoothschedule.identity.users.api_views._get_user_data')
    def test_successful_first_level_hijack(self, mock_get_user_data, mock_token_model,
                                          mock_can_hijack, mock_get_object):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/hijack/acquire/', {
            'user_pk': 100,
            'hijack_history': []
        })

        mock_hijacker = Mock()
        mock_hijacker.id = 1
        mock_hijacker.username = 'admin'
        mock_hijacker.role = User.Role.SUPERUSER
        mock_hijacker.tenant_id = None
        mock_hijacker.tenant = None
        request.user = mock_hijacker

        mock_hijacked = Mock()
        mock_hijacked.id = 100
        mock_get_object.side_effect = [mock_hijacked]

        mock_can_hijack.return_value = True

        mock_token = Mock()
        mock_token.key = 'hijacked-token'
        mock_token_model.objects.filter.return_value.delete.return_value = None
        mock_token_model.objects.create.return_value = mock_token

        mock_get_user_data.return_value = {'id': 100, 'email': 'target@test.com'}

        response = api_views.hijack_acquire_view(request)

        assert response.status_code == status.HTTP_200_OK
        assert response.data['access'] == 'hijacked-token'
        assert len(response.data['masquerade_stack']) == 1
        assert response.data['masquerade_stack'][0]['user_id'] == 1
        assert response.data['masquerade_stack'][0]['role'] == 'superuser'

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    @patch('smoothschedule.identity.users.api_views.can_hijack')
    def test_multilevel_hijack_checks_original_user_permissions(self, mock_can_hijack,
                                                                mock_get_object):
        factory = APIRequestFactory()

        # Existing hijack history with original user
        hijack_history = [
            {'user_id': 1, 'username': 'original', 'role': 'superuser',
             'business_id': None, 'business_subdomain': None}
        ]

        # Use format='json' to properly encode the hijack_history list
        request = factory.post('/api/auth/hijack/acquire/', {
            'user_pk': 200,
            'hijack_history': hijack_history
        }, format='json')

        mock_current_hijacker = Mock()
        mock_current_hijacker.id = 100
        request.user = mock_current_hijacker

        mock_original_user = Mock()
        mock_original_user.id = 1
        mock_original_user.email = 'original@test.com'

        mock_new_target = Mock()
        mock_new_target.id = 200

        # get_object_or_404 called: first for new target, then for original user
        mock_get_object.side_effect = [mock_new_target, mock_original_user]

        mock_can_hijack.return_value = False

        response = api_views.hijack_acquire_view(request)

        assert response.status_code == status.HTTP_403_FORBIDDEN
        # Should check permissions against original user, not current
        # get_object_or_404 is called twice: once for hijacked user, once for original
        assert mock_get_object.call_count == 2
        mock_can_hijack.assert_called_once_with(mock_original_user, mock_new_target)

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    @patch('smoothschedule.identity.users.api_views.can_hijack')
    def test_rejects_hijack_when_max_depth_reached(self, mock_can_hijack, mock_get_object):
        factory = APIRequestFactory()

        # Build a deep hijack history (5 levels - at the max)
        hijack_history = [
            {'user_id': i, 'username': f'user{i}', 'role': 'superuser',
             'business_id': None, 'business_subdomain': None}
            for i in range(1, 6)
        ]

        # Use format='json' to properly encode the hijack_history list
        request = factory.post('/api/auth/hijack/acquire/', {
            'user_pk': 999,
            'hijack_history': hijack_history
        }, format='json')

        mock_hijacker = Mock()
        mock_hijacker.id = 5
        request.user = mock_hijacker

        mock_target = Mock()
        mock_target.id = 999

        # Return both target and original user
        mock_original_user = Mock()
        mock_original_user.id = 1
        mock_get_object.side_effect = [mock_target, mock_original_user]

        response = api_views.hijack_acquire_view(request)

        assert response.status_code == status.HTTP_403_FORBIDDEN
        assert 'Maximum masquerade depth' in response.data['error']


class TestHijackReleaseView:
    """Test hijack_release_view function"""

    def test_empty_masquerade_stack(self):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/hijack/release/', {
            'masquerade_stack': []
        })
        request.user = Mock()

        response = api_views.hijack_release_view(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'No masquerade session' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.User')
    @patch('smoothschedule.identity.users.api_views.Token')
    @patch('smoothschedule.identity.users.api_views._get_user_data')
    def test_successful_release(self, mock_get_user_data, mock_token_model, mock_user_model):
        factory = APIRequestFactory()

        masquerade_stack = [
            {'user_id': 1, 'username': 'admin', 'role': 'superuser',
             'business_id': None, 'business_subdomain': None}
        ]

        # Use format='json' to properly encode the masquerade_stack list
        request = factory.post('/api/auth/hijack/release/', {
            'masquerade_stack': masquerade_stack
        }, format='json')
        request.user = Mock()

        mock_original_user = Mock()
        mock_original_user.id = 1

        # Use get_object_or_404 via patching User.objects.get
        with patch('smoothschedule.identity.users.api_views.get_object_or_404') as mock_get_404:
            mock_get_404.return_value = mock_original_user

            mock_token = Mock()
            mock_token.key = 'original-token'
            mock_token_model.objects.filter.return_value.delete.return_value = None
            mock_token_model.objects.create.return_value = mock_token

            mock_get_user_data.return_value = {'id': 1, 'email': 'admin@test.com'}

            response = api_views.hijack_release_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert response.data['access'] == 'original-token'
            assert response.data['masquerade_stack'] == []


# ============================================================================
# Staff Invitation Tests
# ============================================================================

class TestStaffInvitationsView:
    """Test staff_invitations_view function"""

    def test_get_requires_permission(self):
        factory = APIRequestFactory()
        request = factory.get('/api/staff/invitations/')

        mock_user = Mock()
        mock_user.can_invite_staff.return_value = False
        request.user = mock_user

        response = api_views.staff_invitations_view(request)

        assert response.status_code == status.HTTP_403_FORBIDDEN
        assert 'permission' in response.data['error'].lower()

    def test_get_requires_tenant(self):
        factory = APIRequestFactory()
        request = factory.get('/api/staff/invitations/')

        mock_user = Mock()
        mock_user.can_invite_staff.return_value = True
        mock_user.tenant = None
        request.user = mock_user

        response = api_views.staff_invitations_view(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'No business' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.StaffInvitation')
    @patch('smoothschedule.identity.users.api_views.StaffInvitationSerializer')
    def test_get_lists_pending_invitations(self, mock_serializer_class, mock_invitation_model):
        factory = APIRequestFactory()
        request = factory.get('/api/staff/invitations/')

        mock_tenant = Mock()
        mock_user = Mock()
        mock_user.can_invite_staff.return_value = True
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_invitations = [Mock(), Mock()]
        mock_invitation_model.objects.filter.return_value = mock_invitations
        mock_invitation_model.Status.PENDING = 'PENDING'

        mock_serializer = Mock()
        mock_serializer.data = [{'id': 1}, {'id': 2}]
        mock_serializer_class.return_value = mock_serializer

        response = api_views.staff_invitations_view(request)

        assert response.status_code == status.HTTP_200_OK
        mock_invitation_model.objects.filter.assert_called_once()

    def test_post_missing_email(self):
        factory = APIRequestFactory()
        request = factory.post('/api/staff/invitations/', {})

        mock_tenant = Mock()
        mock_user = Mock()
        mock_user.can_invite_staff.return_value = True
        mock_user.tenant = mock_tenant
        request.user = mock_user

        response = api_views.staff_invitations_view(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'Email is required' in response.data['error']

    def test_post_invalid_role(self):
        factory = APIRequestFactory()
        request = factory.post('/api/staff/invitations/', {
            'email': 'staff@test.com',
            'role': 'INVALID_ROLE'
        })

        mock_tenant = Mock()
        mock_user = Mock()
        mock_user.can_invite_staff.return_value = True
        mock_user.tenant = mock_tenant
        request.user = mock_user

        response = api_views.staff_invitations_view(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'Invalid role' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.User')
    def test_post_manager_cannot_invite_manager(self, mock_user_model):
        factory = APIRequestFactory()
        request = factory.post('/api/staff/invitations/', {
            'email': 'manager@test.com',
            'role': User.Role.TENANT_MANAGER
        })

        mock_tenant = Mock()
        mock_user = Mock()
        mock_user.can_invite_staff.return_value = True
        mock_user.tenant = mock_tenant
        mock_user.role = User.Role.TENANT_MANAGER
        request.user = mock_user

        mock_user_model.Role.TENANT_MANAGER = 'TENANT_MANAGER'
        mock_user_model.Role.TENANT_STAFF = 'TENANT_STAFF'

        response = api_views.staff_invitations_view(request)

        assert response.status_code == status.HTTP_403_FORBIDDEN
        assert 'Managers can only invite staff' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.User')
    def test_post_rejects_existing_user(self, mock_user_model):
        factory = APIRequestFactory()
        request = factory.post('/api/staff/invitations/', {
            'email': 'existing@test.com',
            'role': User.Role.TENANT_STAFF
        })

        mock_tenant = Mock()
        mock_user = Mock()
        mock_user.can_invite_staff.return_value = True
        mock_user.tenant = mock_tenant
        mock_user.role = User.Role.TENANT_OWNER
        request.user = mock_user

        mock_user_model.Role.TENANT_MANAGER = 'TENANT_MANAGER'
        mock_user_model.Role.TENANT_STAFF = 'TENANT_STAFF'

        # User already exists
        mock_existing = Mock()
        mock_user_model.objects.filter.return_value.first.return_value = mock_existing

        response = api_views.staff_invitations_view(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'already exists' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.User')
    @patch('smoothschedule.identity.users.api_views.StaffInvitation')
    @patch('smoothschedule.identity.users.api_views._send_invitation_email')
    @patch('smoothschedule.identity.users.api_views.StaffInvitationSerializer')
    def test_post_creates_invitation_successfully(self, mock_serializer_class,
                                                  mock_send_email, mock_invitation_model,
                                                  mock_user_model):
        factory = APIRequestFactory()
        # Use format='json' to handle nested dict in permissions
        request = factory.post('/api/staff/invitations/', {
            'email': 'newstaff@test.com',
            'role': User.Role.TENANT_STAFF,
            'create_bookable_resource': True,
            'resource_name': 'John Doe',
            'permissions': {'can_access_tickets': True}
        }, format='json')

        mock_tenant = Mock()
        mock_user = Mock()
        mock_user.can_invite_staff.return_value = True
        mock_user.tenant = mock_tenant
        mock_user.role = User.Role.TENANT_OWNER
        request.user = mock_user

        mock_user_model.Role.TENANT_MANAGER = 'TENANT_MANAGER'
        mock_user_model.Role.TENANT_STAFF = 'TENANT_STAFF'
        mock_user_model.objects.filter.return_value.first.return_value = None

        mock_invitation = Mock()
        mock_invitation.id = 1
        mock_invitation_model.create_invitation.return_value = mock_invitation

        mock_serializer = Mock()
        mock_serializer.data = {'id': 1, 'email': 'newstaff@test.com'}
        mock_serializer_class.return_value = mock_serializer

        response = api_views.staff_invitations_view(request)

        assert response.status_code == status.HTTP_201_CREATED
        mock_invitation_model.create_invitation.assert_called_once()
        mock_send_email.assert_called_once_with(mock_invitation)


class TestCancelInvitationView:
    """Test cancel_invitation_view function"""

    def test_requires_permission(self):
        factory = APIRequestFactory()
        request = factory.delete('/api/staff/invitations/1/')

        mock_user = Mock()
        mock_user.can_manage_users.return_value = False
        request.user = mock_user

        response = api_views.cancel_invitation_view(request, 1)

        assert response.status_code == status.HTTP_403_FORBIDDEN

    def test_requires_tenant(self):
        factory = APIRequestFactory()
        request = factory.delete('/api/staff/invitations/1/')

        mock_user = Mock()
        mock_user.can_manage_users.return_value = True
        mock_user.tenant = None
        request.user = mock_user

        response = api_views.cancel_invitation_view(request, 1)

        assert response.status_code == status.HTTP_400_BAD_REQUEST

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    def test_rejects_non_pending_invitation(self, mock_get_object):
        factory = APIRequestFactory()
        request = factory.delete('/api/staff/invitations/1/')

        mock_tenant = Mock()
        mock_user = Mock()
        mock_user.can_manage_users.return_value = True
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_invitation = Mock()
        mock_invitation.status = 'ACCEPTED'
        mock_get_object.return_value = mock_invitation

        with patch('smoothschedule.identity.users.api_views.StaffInvitation') as mock_model:
            mock_model.Status.PENDING = 'PENDING'
            response = api_views.cancel_invitation_view(request, 1)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'Only pending invitations' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    @patch('smoothschedule.identity.users.api_views.StaffInvitation')
    def test_successfully_cancels_invitation(self, mock_invitation_model, mock_get_object):
        factory = APIRequestFactory()
        request = factory.delete('/api/staff/invitations/1/')

        mock_tenant = Mock()
        mock_user = Mock()
        mock_user.can_manage_users.return_value = True
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_invitation = Mock()
        mock_invitation.status = 'PENDING'
        mock_get_object.return_value = mock_invitation
        mock_invitation_model.Status.PENDING = 'PENDING'

        response = api_views.cancel_invitation_view(request, 1)

        assert response.status_code == status.HTTP_200_OK
        mock_invitation.cancel.assert_called_once()


class TestResendInvitationView:
    """Test resend_invitation_view function"""

    def test_requires_permission(self):
        factory = APIRequestFactory()
        request = factory.post('/api/staff/invitations/1/resend/')

        mock_user = Mock()
        mock_user.can_manage_users.return_value = False
        request.user = mock_user

        response = api_views.resend_invitation_view(request, 1)

        assert response.status_code == status.HTTP_403_FORBIDDEN

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    @patch('smoothschedule.identity.users.api_views._send_invitation_email')
    def test_resends_email_successfully(self, mock_send_email, mock_get_object):
        factory = APIRequestFactory()
        request = factory.post('/api/staff/invitations/1/resend/')

        mock_tenant = Mock()
        mock_user = Mock()
        mock_user.can_manage_users.return_value = True
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_invitation = Mock()
        mock_get_object.return_value = mock_invitation

        response = api_views.resend_invitation_view(request, 1)

        assert response.status_code == status.HTTP_200_OK
        mock_send_email.assert_called_once_with(mock_invitation)


class TestInvitationDetailsView:
    """Test invitation_details_view function"""

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    def test_returns_invitation_details(self, mock_get_object):
        factory = APIRequestFactory()
        request = factory.get('/api/staff/invitations/token/test-token/')

        mock_tenant = Mock()
        mock_tenant.name = 'Test Business'

        mock_inviter = Mock()
        mock_inviter.full_name = 'John Owner'

        mock_invitation = Mock()
        mock_invitation.email = 'newstaff@test.com'
        mock_invitation.role = 'TENANT_STAFF'
        mock_invitation.tenant = mock_tenant
        mock_invitation.invited_by = mock_inviter
        mock_invitation.expires_at = timezone.now() + timedelta(days=7)
        mock_invitation.create_bookable_resource = True
        mock_invitation.resource_name = 'New Staff'
        mock_invitation.is_valid.return_value = True

        mock_get_object.return_value = mock_invitation

        response = api_views.invitation_details_view(request, 'test-token')

        assert response.status_code == status.HTTP_200_OK
        assert response.data['email'] == 'newstaff@test.com'
        assert response.data['role'] == 'TENANT_STAFF'
        assert response.data['business_name'] == 'Test Business'
        assert response.data['invited_by'] == 'John Owner'

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    def test_rejects_invalid_invitation(self, mock_get_object):
        factory = APIRequestFactory()
        request = factory.get('/api/staff/invitations/token/expired-token/')

        mock_invitation = Mock()
        mock_invitation.is_valid.return_value = False
        mock_get_object.return_value = mock_invitation

        response = api_views.invitation_details_view(request, 'expired-token')

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'expired' in response.data['error'].lower()


class TestAcceptInvitationView:
    """Test accept_invitation_view function"""

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    def test_rejects_invalid_invitation(self, mock_get_object):
        factory = APIRequestFactory()
        request = factory.post('/api/staff/invitations/token/test-token/accept/', {})

        mock_invitation = Mock()
        mock_invitation.is_valid.return_value = False
        mock_get_object.return_value = mock_invitation

        response = api_views.accept_invitation_view(request, 'test-token')

        assert response.status_code == status.HTTP_400_BAD_REQUEST

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    def test_validates_required_fields(self, mock_get_object):
        factory = APIRequestFactory()
        request = factory.post('/api/staff/invitations/token/test-token/accept/', {
            'password': 'short'
        })

        mock_invitation = Mock()
        mock_invitation.is_valid.return_value = True
        mock_get_object.return_value = mock_invitation

        response = api_views.accept_invitation_view(request, 'test-token')

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'First name is required' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    def test_validates_password_length(self, mock_get_object):
        factory = APIRequestFactory()
        request = factory.post('/api/staff/invitations/token/test-token/accept/', {
            'first_name': 'John',
            'password': 'short'
        })

        mock_invitation = Mock()
        mock_invitation.is_valid.return_value = True
        mock_get_object.return_value = mock_invitation

        response = api_views.accept_invitation_view(request, 'test-token')

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'at least 8 characters' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    @patch('smoothschedule.identity.users.api_views.User')
    def test_rejects_existing_email(self, mock_user_model, mock_get_object):
        factory = APIRequestFactory()
        request = factory.post('/api/staff/invitations/token/test-token/accept/', {
            'first_name': 'John',
            'last_name': 'Doe',
            'password': 'password123'
        })

        mock_invitation = Mock()
        mock_invitation.is_valid.return_value = True
        mock_invitation.email = 'existing@test.com'
        mock_get_object.return_value = mock_invitation

        mock_user_model.objects.filter.return_value.exists.return_value = True

        response = api_views.accept_invitation_view(request, 'test-token')

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'already exists' in response.data['error']

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    @patch('smoothschedule.identity.users.api_views.User')
    @patch('smoothschedule.identity.users.api_views.Token')
    @patch('smoothschedule.identity.users.api_views._get_user_data')
    def test_creates_user_successfully(self, mock_get_user_data, mock_token_model,
                                      mock_user_model, mock_get_object):
        factory = APIRequestFactory()
        request = factory.post('/api/staff/invitations/token/test-token/accept/', {
            'first_name': 'John',
            'last_name': 'Doe',
            'password': 'password123'
        })
        request.sandbox_mode = False

        mock_tenant = Mock()
        mock_invitation = Mock()
        mock_invitation.is_valid.return_value = True
        mock_invitation.email = 'newuser@test.com'
        mock_invitation.role = User.Role.TENANT_STAFF
        mock_invitation.tenant = mock_tenant
        mock_invitation.permissions = {'can_access_tickets': True}
        mock_invitation.create_bookable_resource = False
        mock_get_object.return_value = mock_invitation

        mock_user_model.objects.filter.return_value.exists.return_value = False

        mock_user = Mock()
        mock_user.id = 100
        mock_user.full_name = 'John Doe'
        mock_user_model.objects.create_user.return_value = mock_user

        mock_token = Mock()
        mock_token.key = 'new-token'
        mock_token_model.objects.get_or_create.return_value = (mock_token, True)

        mock_get_user_data.return_value = {'id': 100, 'email': 'newuser@test.com'}

        response = api_views.accept_invitation_view(request, 'test-token')

        assert response.status_code == status.HTTP_201_CREATED
        assert response.data['access'] == 'new-token'
        mock_user_model.objects.create_user.assert_called_once()
        mock_invitation.accept.assert_called_once_with(mock_user)

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    @patch('smoothschedule.identity.users.api_views.User')
    @patch('smoothschedule.identity.users.api_views.Token')
    @patch('smoothschedule.identity.users.api_views._get_user_data')
    @patch('smoothschedule.identity.users.api_views.ResourceType')
    @patch('smoothschedule.identity.users.api_views.Resource')
    def test_creates_bookable_resource_when_configured(self, mock_resource_model,
                                                       mock_resource_type_model,
                                                       mock_get_user_data,
                                                       mock_token_model,
                                                       mock_user_model,
                                                       mock_get_object):
        factory = APIRequestFactory()
        request = factory.post('/api/staff/invitations/token/test-token/accept/', {
            'first_name': 'Jane',
            'last_name': 'Smith',
            'password': 'password123'
        })
        request.sandbox_mode = False

        mock_tenant = Mock()
        mock_invitation = Mock()
        mock_invitation.is_valid.return_value = True
        mock_invitation.email = 'jane@test.com'
        mock_invitation.role = User.Role.TENANT_STAFF
        mock_invitation.tenant = mock_tenant
        mock_invitation.permissions = {}
        mock_invitation.create_bookable_resource = True
        mock_invitation.resource_name = 'Jane Smith Resource'
        mock_get_object.return_value = mock_invitation

        mock_user_model.objects.filter.return_value.exists.return_value = False

        mock_user = Mock()
        mock_user.id = 200
        mock_user.full_name = 'Jane Smith'
        mock_user_model.objects.create_user.return_value = mock_user

        mock_token = Mock()
        mock_token.key = 'new-token'
        mock_token_model.objects.get_or_create.return_value = (mock_token, True)

        mock_get_user_data.return_value = {'id': 200}

        # Mock resource type
        mock_staff_type = Mock()
        mock_resource_type_model.objects.filter.return_value.first.return_value = mock_staff_type
        mock_resource_type_model.Category.STAFF = 'STAFF'

        # Mock resource creation
        mock_resource = Mock()
        mock_resource.id = 50
        mock_resource.name = 'Jane Smith Resource'
        mock_resource_model.objects.create.return_value = mock_resource
        mock_resource_model.Type.STAFF = 'STAFF'

        response = api_views.accept_invitation_view(request, 'test-token')

        assert response.status_code == status.HTTP_201_CREATED
        assert 'resource_created' in response.data
        assert response.data['resource_created']['id'] == 50
        mock_resource_model.objects.create.assert_called_once()


class TestDeclineInvitationView:
    """Test decline_invitation_view function"""

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    def test_rejects_non_pending_invitation(self, mock_get_object):
        factory = APIRequestFactory()
        request = factory.post('/api/staff/invitations/token/test-token/decline/')

        mock_invitation = Mock()
        mock_invitation.status = 'ACCEPTED'
        mock_get_object.return_value = mock_invitation

        with patch('smoothschedule.identity.users.api_views.StaffInvitation') as mock_model:
            mock_model.Status.PENDING = 'PENDING'
            response = api_views.decline_invitation_view(request, 'test-token')

        assert response.status_code == status.HTTP_400_BAD_REQUEST

    @patch('smoothschedule.identity.users.api_views.get_object_or_404')
    @patch('smoothschedule.identity.users.api_views.StaffInvitation')
    def test_declines_invitation_successfully(self, mock_model, mock_get_object):
        factory = APIRequestFactory()
        request = factory.post('/api/staff/invitations/token/test-token/decline/')

        mock_invitation = Mock()
        mock_invitation.status = 'PENDING'
        mock_get_object.return_value = mock_invitation
        mock_model.Status.PENDING = 'PENDING'

        response = api_views.decline_invitation_view(request, 'test-token')

        assert response.status_code == status.HTTP_200_OK
        mock_invitation.decline.assert_called_once()


# ============================================================================
# Subdomain & Signup Tests
# ============================================================================

class TestCheckSubdomainView:
    """Test check_subdomain_view function"""

    def test_missing_subdomain(self):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/signup/check-subdomain/', {})

        response = api_views.check_subdomain_view(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'Subdomain is required' in response.data['error']

    def test_rejects_reserved_subdomain(self):
        reserved_words = ['www', 'api', 'admin', 'platform']

        for word in reserved_words:
            factory = APIRequestFactory()
            request = factory.post('/api/auth/signup/check-subdomain/', {
                'subdomain': word
            })

            response = api_views.check_subdomain_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert response.data['available'] is False
            assert response.data['reason'] == 'Reserved'

    @patch('smoothschedule.identity.users.api_views.Tenant')
    def test_rejects_existing_tenant_schema(self, mock_tenant_model):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/signup/check-subdomain/', {
            'subdomain': 'existing'
        })

        mock_tenant_model.objects.filter.return_value.exists.return_value = True

        response = api_views.check_subdomain_view(request)

        assert response.status_code == status.HTTP_200_OK
        assert response.data['available'] is False

    @patch('smoothschedule.identity.users.api_views.Tenant')
    @patch('smoothschedule.identity.users.api_views.Domain')
    def test_rejects_existing_domain(self, mock_domain_model, mock_tenant_model):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/signup/check-subdomain/', {
            'subdomain': 'existing'
        })

        mock_tenant_model.objects.filter.return_value.exists.return_value = False
        mock_domain_model.objects.filter.return_value.exists.return_value = True

        response = api_views.check_subdomain_view(request)

        assert response.status_code == status.HTTP_200_OK
        assert response.data['available'] is False

    @patch('smoothschedule.identity.users.api_views.Tenant')
    @patch('smoothschedule.identity.users.api_views.Domain')
    def test_accepts_available_subdomain(self, mock_domain_model, mock_tenant_model):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/signup/check-subdomain/', {
            'subdomain': 'newbusiness'
        })

        mock_tenant_model.objects.filter.return_value.exists.return_value = False
        mock_domain_model.objects.filter.return_value.exists.return_value = False

        response = api_views.check_subdomain_view(request)

        assert response.status_code == status.HTTP_200_OK
        assert response.data['available'] is True


class TestSignupView:
    """Test signup_view function"""

    def test_missing_subdomain(self):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/signup/', {})

        response = api_views.signup_view(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'Subdomain is required' in response.data['detail']

    @patch('smoothschedule.identity.users.api_views.Tenant')
    def test_rejects_existing_subdomain(self, mock_tenant_model):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/signup/', {
            'subdomain': 'existing',
            'email': 'user@test.com',
            'password': 'password123'
        })

        mock_tenant_model.objects.filter.return_value.exists.return_value = True

        response = api_views.signup_view(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'already taken' in response.data['detail']

    @patch('smoothschedule.identity.users.api_views.Tenant')
    @patch('smoothschedule.identity.users.api_views.User')
    def test_rejects_existing_email(self, mock_user_model, mock_tenant_model):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/signup/', {
            'subdomain': 'newbiz',
            'email': 'existing@test.com',
            'password': 'password123'
        })

        mock_tenant_model.objects.filter.return_value.exists.return_value = False
        mock_user_model.objects.filter.return_value.exists.return_value = True

        response = api_views.signup_view(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'already exists' in response.data['detail']

    @patch('smoothschedule.billing.models.Plan')
    @patch('smoothschedule.billing.models.Subscription')
    @patch('smoothschedule.billing.models.PlanVersion')
    @patch('smoothschedule.identity.users.api_views.schema_context')
    @patch('smoothschedule.identity.users.api_views.Tenant')
    @patch('smoothschedule.identity.users.api_views.Domain')
    @patch('smoothschedule.identity.users.api_views.User')
    @patch('smoothschedule.identity.users.api_views.Token')
    @patch('smoothschedule.identity.users.api_views._get_user_data')
    def test_creates_tenant_and_owner_successfully(self, mock_get_user_data,
                                                   mock_token_model, mock_user_model,
                                                   mock_domain_model, mock_tenant_model,
                                                   mock_schema_context, mock_plan_version,
                                                   mock_subscription, mock_plan_model):
        factory = APIRequestFactory()
        request = factory.post('/api/auth/signup/', {
            'subdomain': 'newbiz',
            'business_name': 'New Business',
            'email': 'owner@newbiz.com',
            'password': 'password123',
            'first_name': 'John',
            'last_name': 'Owner',
            'tier': 'STARTER'
        })
        request.get_host = Mock(return_value='lvh.me:8000')

        mock_tenant_model.objects.filter.return_value.exists.return_value = False
        mock_user_model.objects.filter.return_value.exists.return_value = False

        # Mock Plan lookup
        mock_plan = Mock()
        mock_plan.code = 'starter'
        mock_plan.id = 1
        mock_plan_model.objects.get.return_value = mock_plan
        mock_plan_model.DoesNotExist = Exception

        # Mock PlanVersion
        mock_version = Mock()
        mock_version.id = 1
        mock_plan_version.objects.filter.return_value.order_by.return_value.first.return_value = mock_version

        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_tenant.name = 'New Business'
        mock_tenant_model.objects.create.return_value = mock_tenant

        mock_user = Mock()
        mock_user.id = 10
        mock_user_model.objects.create_user.return_value = mock_user
        mock_user_model.Role.TENANT_OWNER = 'TENANT_OWNER'

        mock_token = Mock()
        mock_token.key = 'signup-token'
        mock_token_model.objects.get_or_create.return_value = (mock_token, True)

        mock_get_user_data.return_value = {'id': 10, 'email': 'owner@newbiz.com'}

        response = api_views.signup_view(request)

        assert response.status_code == status.HTTP_201_CREATED
        assert response.data['access'] == 'signup-token'
        assert 'user' in response.data
        assert 'tenant' in response.data
        mock_tenant_model.objects.create.assert_called_once()
        mock_domain_model.objects.create.assert_called_once()
        mock_user_model.objects.create_user.assert_called_once()

    @patch('smoothschedule.billing.models.Plan')
    @patch('smoothschedule.billing.models.Subscription')
    @patch('smoothschedule.billing.models.PlanVersion')
    @patch('smoothschedule.identity.users.api_views.schema_context')
    @patch('smoothschedule.identity.users.api_views.Tenant')
    @patch('smoothschedule.identity.users.api_views.Domain')
    @patch('smoothschedule.identity.users.api_views.User')
    @patch('smoothschedule.identity.users.api_views.Token')
    @patch('smoothschedule.identity.users.api_views._get_user_data')
    def test_applies_tier_permissions_correctly(self, mock_get_user_data,
                                                mock_token_model, mock_user_model,
                                                mock_domain_model, mock_tenant_model,
                                                mock_schema_context, mock_plan_version,
                                                mock_subscription, mock_plan_model):
        """Test that signup creates a subscription with the selected plan.

        Note: Tier permissions are now managed via the billing system (Plan/PlanVersion),
        not as direct attributes on the Tenant model.
        """
        factory = APIRequestFactory()
        request = factory.post('/api/auth/signup/', {
            'subdomain': 'professional',
            'business_name': 'Pro Business',
            'email': 'owner@pro.com',
            'password': 'password123',
            'tier': 'PROFESSIONAL'
        })
        request.get_host = Mock(return_value='lvh.me:8000')

        mock_tenant_model.objects.filter.return_value.exists.return_value = False
        mock_user_model.objects.filter.return_value.exists.return_value = False

        # Mock Plan lookup with versions
        mock_version = Mock()
        mock_version.id = 1

        mock_plan = Mock()
        mock_plan.code = 'professional'
        mock_plan.id = 1
        mock_plan.versions.filter.return_value.order_by.return_value.first.return_value = mock_version
        mock_plan_model.objects.get.return_value = mock_plan
        mock_plan_model.DoesNotExist = Exception

        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_tenant_model.objects.create.return_value = mock_tenant

        mock_user = Mock()
        mock_user_model.objects.create_user.return_value = mock_user
        mock_user_model.Role.TENANT_OWNER = 'TENANT_OWNER'

        mock_token = Mock()
        mock_token.key = 'token'
        mock_token_model.objects.get_or_create.return_value = (mock_token, True)

        mock_get_user_data.return_value = {}

        response = api_views.signup_view(request)

        # Verify the response is successful
        assert response.status_code == status.HTTP_201_CREATED

        # Verify a subscription was created with the professional plan
        mock_subscription.objects.create.assert_called_once()
        create_call = mock_subscription.objects.create.call_args
        assert create_call[1]['business'] == mock_tenant
        assert create_call[1]['plan_version'] == mock_version


# ============================================================================
# Send Invitation Email Tests
# ============================================================================

class TestSendInvitationEmail:
    """Test _send_invitation_email helper function"""

    @patch('smoothschedule.identity.users.api_views.send_mail')
    @patch('smoothschedule.identity.users.api_views.settings')
    def test_sends_email_with_correct_content(self, mock_settings, mock_send_mail):
        mock_tenant = Mock()
        mock_tenant.name = 'Test Business'
        mock_tenant.schema_name = 'testbiz'

        mock_domain = Mock()
        mock_domain.domain = 'testbiz.lvh.me'
        mock_tenant.domains.filter.return_value.first.return_value = mock_domain

        mock_inviter = Mock()
        mock_inviter.full_name = 'John Owner'

        mock_invitation = Mock()
        mock_invitation.email = 'newstaff@test.com'
        mock_invitation.role = 'TENANT_STAFF'
        mock_invitation.tenant = mock_tenant
        mock_invitation.invited_by = mock_inviter
        mock_invitation.token = 'test-token-123'

        mock_settings.DEBUG = True
        mock_settings.DEFAULT_FROM_EMAIL = 'noreply@test.com'

        api_views._send_invitation_email(mock_invitation)

        mock_send_mail.assert_called_once()
        call_args = mock_send_mail.call_args

        # Verify subject
        assert 'Test Business' in call_args[0][0]

        # Verify message content
        message = call_args[0][1]
        assert 'John Owner' in message
        assert 'testbiz.lvh.me:5173/accept-invite?token=test-token-123' in message

        # Verify recipient
        assert call_args[0][3] == ['newstaff@test.com']
