"""
Unit tests for the User model.

Tests cover:
1. Role-related methods (is_platform_user, is_tenant_user, can_invite_staff, etc.)
2. Permission checking methods (can_access_tickets, can_approve_plugins, etc.)
3. Property methods (full_name)
4. The save() method validation logic (tenant requirements for different roles)

Uses minimal User instances where possible to keep tests fast. Only uses @pytest.mark.django_db
for testing actual database constraints.
"""
import pytest
from unittest.mock import Mock, patch
from django.core.exceptions import ValidationError

from smoothschedule.identity.users.models import User


def create_user_instance(role, permissions=None, **kwargs):
    """
    Helper to create a minimal User instance for testing without hitting the DB.

    Args:
        role: User.Role value
        permissions: Dict of permissions (default: empty dict)
        **kwargs: Additional user attributes

    Returns:
        User instance (not saved to DB)
    """
    defaults = {
        'username': 'testuser',
        'email': 'test@example.com',
        'first_name': '',
        'last_name': '',
        'permissions': permissions or {},
    }
    defaults.update(kwargs)

    # Create instance without saving
    user = User(role=role, **defaults)
    # Set pk to simulate it exists (for methods that check)
    user.pk = 1
    return user


# =============================================================================
# Role-Related Method Tests
# =============================================================================

class TestRoleClassification:
    """Test is_platform_user() and is_tenant_user() methods."""

    def test_is_platform_user_returns_true_for_superuser(self):
        user = create_user_instance(User.Role.SUPERUSER)
        assert user.is_platform_user() is True

    def test_is_platform_user_returns_true_for_platform_manager(self):
        user = create_user_instance(User.Role.PLATFORM_MANAGER)
        assert user.is_platform_user() is True

    def test_is_platform_user_returns_true_for_platform_sales(self):
        user = create_user_instance(User.Role.PLATFORM_SALES)
        assert user.is_platform_user() is True

    def test_is_platform_user_returns_true_for_platform_support(self):
        user = create_user_instance(User.Role.PLATFORM_SUPPORT)
        assert user.is_platform_user() is True

    def test_is_platform_user_returns_false_for_tenant_owner(self):
        user = create_user_instance(User.Role.TENANT_OWNER)
        assert user.is_platform_user() is False

    def test_is_platform_user_returns_false_for_customer(self):
        user = create_user_instance(User.Role.CUSTOMER)
        assert user.is_platform_user() is False

    def test_is_tenant_user_returns_true_for_tenant_owner(self):
        user = create_user_instance(User.Role.TENANT_OWNER)
        assert user.is_tenant_user() is True

    def test_is_tenant_user_returns_true_for_tenant_manager(self):
        user = create_user_instance(User.Role.TENANT_MANAGER)
        assert user.is_tenant_user() is True

    def test_is_tenant_user_returns_true_for_tenant_staff(self):
        user = create_user_instance(User.Role.TENANT_STAFF)
        assert user.is_tenant_user() is True

    def test_is_tenant_user_returns_true_for_customer(self):
        user = create_user_instance(User.Role.CUSTOMER)
        assert user.is_tenant_user() is True

    def test_is_tenant_user_returns_false_for_superuser(self):
        user = create_user_instance(User.Role.SUPERUSER)
        assert user.is_tenant_user() is False


# =============================================================================
# User Management Permission Tests
# =============================================================================

class TestCanManageUsers:
    """Test can_manage_users() method."""

    def test_returns_true_for_superuser(self):
        user = create_user_instance(User.Role.SUPERUSER)
        assert user.can_manage_users() is True

    def test_returns_true_for_platform_manager(self):
        user = create_user_instance(User.Role.PLATFORM_MANAGER)
        assert user.can_manage_users() is True

    def test_returns_true_for_tenant_owner(self):
        user = create_user_instance(User.Role.TENANT_OWNER)
        assert user.can_manage_users() is True

    def test_returns_true_for_tenant_manager(self):
        user = create_user_instance(User.Role.TENANT_MANAGER)
        assert user.can_manage_users() is True

    def test_returns_false_for_tenant_staff(self):
        user = create_user_instance(User.Role.TENANT_STAFF)
        assert user.can_manage_users() is False

    def test_returns_false_for_customer(self):
        user = create_user_instance(User.Role.CUSTOMER)
        assert user.can_manage_users() is False


# =============================================================================
# Billing Access Tests
# =============================================================================

class TestCanAccessBilling:
    """Test can_access_billing() method."""

    def test_returns_true_for_superuser(self):
        user = create_user_instance(User.Role.SUPERUSER)
        assert user.can_access_billing() is True

    def test_returns_true_for_platform_manager(self):
        user = create_user_instance(User.Role.PLATFORM_MANAGER)
        assert user.can_access_billing() is True

    def test_returns_true_for_tenant_owner(self):
        user = create_user_instance(User.Role.TENANT_OWNER)
        assert user.can_access_billing() is True

    def test_returns_false_for_tenant_manager(self):
        user = create_user_instance(User.Role.TENANT_MANAGER)
        assert user.can_access_billing() is False

    def test_returns_false_for_tenant_staff(self):
        user = create_user_instance(User.Role.TENANT_STAFF)
        assert user.can_access_billing() is False


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

class TestCanInviteStaff:
    """Test can_invite_staff() method."""

    def test_returns_true_for_tenant_owner(self):
        user = create_user_instance(User.Role.TENANT_OWNER)
        assert user.can_invite_staff() is True

    def test_returns_true_for_manager_with_permission(self):
        user = create_user_instance(User.Role.TENANT_MANAGER, permissions={'can_invite_staff': True})
        assert user.can_invite_staff() is True

    def test_returns_false_for_manager_without_permission(self):
        user = create_user_instance(User.Role.TENANT_MANAGER)
        assert user.can_invite_staff() is False

    def test_returns_false_for_manager_with_explicit_false_permission(self):
        user = create_user_instance(User.Role.TENANT_MANAGER, permissions={'can_invite_staff': False})
        assert user.can_invite_staff() is False

    def test_returns_false_for_tenant_staff(self):
        user = create_user_instance(User.Role.TENANT_STAFF)
        assert user.can_invite_staff() is False

    def test_returns_false_for_customer(self):
        user = create_user_instance(User.Role.CUSTOMER)
        assert user.can_invite_staff() is False


# =============================================================================
# Ticket Access Permission Tests
# =============================================================================

class TestCanAccessTickets:
    """Test can_access_tickets() method."""

    def test_returns_true_for_platform_user(self):
        user = create_user_instance(User.Role.PLATFORM_SUPPORT)
        assert user.can_access_tickets() is True

    def test_returns_true_for_tenant_owner(self):
        user = create_user_instance(User.Role.TENANT_OWNER)
        assert user.can_access_tickets() is True

    def test_returns_true_for_tenant_manager(self):
        user = create_user_instance(User.Role.TENANT_MANAGER)
        assert user.can_access_tickets() is True

    def test_returns_true_for_staff_with_permission(self):
        user = create_user_instance(User.Role.TENANT_STAFF, permissions={'can_access_tickets': True})
        assert user.can_access_tickets() is True

    def test_returns_false_for_staff_without_permission(self):
        user = create_user_instance(User.Role.TENANT_STAFF)
        assert user.can_access_tickets() is False

    def test_returns_true_for_customer(self):
        user = create_user_instance(User.Role.CUSTOMER)
        assert user.can_access_tickets() is True


# =============================================================================
# Plugin Approval Permission Tests
# =============================================================================

class TestCanApprovePlugins:
    """Test can_approve_plugins() method."""

    def test_returns_true_for_superuser(self):
        user = create_user_instance(User.Role.SUPERUSER)
        assert user.can_approve_plugins() is True

    def test_returns_true_for_platform_manager_with_permission(self):
        user = create_user_instance(User.Role.PLATFORM_MANAGER, permissions={'can_approve_plugins': True})
        assert user.can_approve_plugins() is True

    def test_returns_false_for_platform_manager_without_permission(self):
        user = create_user_instance(User.Role.PLATFORM_MANAGER)
        assert user.can_approve_plugins() is False

    def test_returns_true_for_platform_support_with_permission(self):
        user = create_user_instance(User.Role.PLATFORM_SUPPORT, permissions={'can_approve_plugins': True})
        assert user.can_approve_plugins() is True

    def test_returns_false_for_platform_support_without_permission(self):
        user = create_user_instance(User.Role.PLATFORM_SUPPORT)
        assert user.can_approve_plugins() is False

    def test_returns_false_for_tenant_owner(self):
        user = create_user_instance(User.Role.TENANT_OWNER)
        assert user.can_approve_plugins() is False


# =============================================================================
# URL Whitelist Permission Tests
# =============================================================================

class TestCanWhitelistUrls:
    """Test can_whitelist_urls() method."""

    def test_returns_true_for_superuser(self):
        user = create_user_instance(User.Role.SUPERUSER)
        assert user.can_whitelist_urls() is True

    def test_returns_true_for_platform_manager_with_permission(self):
        user = create_user_instance(User.Role.PLATFORM_MANAGER, permissions={'can_whitelist_urls': True})
        assert user.can_whitelist_urls() is True

    def test_returns_false_for_platform_manager_without_permission(self):
        user = create_user_instance(User.Role.PLATFORM_MANAGER)
        assert user.can_whitelist_urls() is False

    def test_returns_true_for_platform_support_with_permission(self):
        user = create_user_instance(User.Role.PLATFORM_SUPPORT, permissions={'can_whitelist_urls': True})
        assert user.can_whitelist_urls() is True

    def test_returns_false_for_tenant_owner(self):
        user = create_user_instance(User.Role.TENANT_OWNER)
        assert user.can_whitelist_urls() is False


# =============================================================================
# Time Off Self-Approval Tests
# =============================================================================

class TestCanSelfApproveTimeOff:
    """Test can_self_approve_time_off() method."""

    def test_returns_true_for_tenant_owner(self):
        user = create_user_instance(User.Role.TENANT_OWNER)
        assert user.can_self_approve_time_off() is True

    def test_returns_true_for_tenant_manager(self):
        user = create_user_instance(User.Role.TENANT_MANAGER)
        assert user.can_self_approve_time_off() is True

    def test_returns_true_for_staff_with_permission(self):
        user = create_user_instance(User.Role.TENANT_STAFF, permissions={'can_self_approve_time_off': True})
        assert user.can_self_approve_time_off() is True

    def test_returns_false_for_staff_without_permission(self):
        user = create_user_instance(User.Role.TENANT_STAFF)
        assert user.can_self_approve_time_off() is False

    def test_returns_false_for_customer(self):
        user = create_user_instance(User.Role.CUSTOMER)
        assert user.can_self_approve_time_off() is False


# =============================================================================
# Time Off Review Permission Tests
# =============================================================================

class TestCanReviewTimeOffRequests:
    """Test can_review_time_off_requests() method."""

    def test_returns_true_for_tenant_owner(self):
        user = create_user_instance(User.Role.TENANT_OWNER)
        assert user.can_review_time_off_requests() is True

    def test_returns_true_for_tenant_manager(self):
        user = create_user_instance(User.Role.TENANT_MANAGER)
        assert user.can_review_time_off_requests() is True

    def test_returns_false_for_tenant_staff(self):
        user = create_user_instance(User.Role.TENANT_STAFF)
        assert user.can_review_time_off_requests() is False

    def test_returns_false_for_customer(self):
        user = create_user_instance(User.Role.CUSTOMER)
        assert user.can_review_time_off_requests() is False


# =============================================================================
# Accessible Tenants Tests
# =============================================================================

class TestGetAccessibleTenants:
    """
    Test get_accessible_tenants() method.

    Uses shared tenant fixtures (session-scoped) to avoid ~40s migration overhead
    per tenant creation. See conftest.py for fixture definitions.
    """

    @pytest.mark.django_db
    def test_returns_all_tenants_for_platform_user(self, shared_tenant, second_shared_tenant):
        # Arrange - use shared fixtures instead of creating new tenants
        user = create_user_instance(User.Role.PLATFORM_MANAGER)

        # Act
        result = user.get_accessible_tenants()

        # Assert - at least our two shared tenants exist
        assert result.count() >= 2

    @pytest.mark.django_db
    def test_returns_single_tenant_for_tenant_user(self, shared_tenant):
        # Arrange - use shared fixture
        import uuid
        unique_id = str(uuid.uuid4())[:8]

        user = User(
            username=f"owner{unique_id}",
            email=f"owner{unique_id}@test.com",
            role=User.Role.TENANT_OWNER,
            tenant=shared_tenant
        )
        user.save()

        # Act
        result = user.get_accessible_tenants()

        # Assert
        assert result.count() == 1
        assert result.first() == shared_tenant

    @pytest.mark.django_db
    def test_returns_empty_queryset_for_tenant_user_without_tenant(self):
        # Arrange
        user = create_user_instance(User.Role.TENANT_OWNER)
        # Force tenant to None (bypassing save validation)
        user.__dict__['tenant'] = None
        user.__dict__['tenant_id'] = None

        # Act
        result = user.get_accessible_tenants()

        # Assert
        assert result.count() == 0


# =============================================================================
# Full Name Property Tests
# =============================================================================

class TestFullNameProperty:
    """Test full_name property."""

    def test_returns_first_and_last_name(self):
        user = create_user_instance(User.Role.CUSTOMER, first_name="John", last_name="Doe", username="johndoe")
        assert user.full_name == "John Doe"

    def test_returns_only_first_name(self):
        user = create_user_instance(User.Role.CUSTOMER, first_name="John", last_name="", username="johndoe")
        assert user.full_name == "John"

    def test_returns_only_last_name(self):
        user = create_user_instance(User.Role.CUSTOMER, first_name="", last_name="Doe", username="johndoe")
        assert user.full_name == "Doe"

    def test_returns_username_when_no_names(self):
        user = create_user_instance(User.Role.CUSTOMER, first_name="", last_name="", username="johndoe")
        assert user.full_name == "johndoe"

    def test_returns_email_when_no_names_or_username(self):
        user = create_user_instance(User.Role.CUSTOMER, first_name="", last_name="", username="", email="john@example.com")
        assert user.full_name == "john@example.com"

    def test_strips_whitespace(self):
        user = create_user_instance(User.Role.CUSTOMER, first_name="  John  ", last_name="  Doe  ", username="johndoe")
        # Joined with space, then stripped
        assert user.full_name == "John     Doe"


# =============================================================================
# Save Method Validation Tests
# =============================================================================

class TestSaveMethodValidation:
    """Test the save() method's business logic validation."""

    @pytest.mark.django_db
    def test_sets_role_to_superuser_when_is_superuser_flag_set(self):
        # Arrange - Test Django's create_superuser compatibility
        import uuid
        unique_id = str(uuid.uuid4())[:8]

        user = User(
            username=f"admin_models_{unique_id}",
            email=f"admin_models_{unique_id}@example.com",
            is_superuser=True,
            role=User.Role.CUSTOMER  # Wrong role, should be corrected
        )

        # Act
        user.save()

        # Assert
        assert user.role == User.Role.SUPERUSER
        assert user.is_staff is True
        assert user.is_superuser is True

    @pytest.mark.django_db
    def test_sets_is_staff_and_is_superuser_for_superuser_role(self):
        # Arrange
        user = User(
            username="admin2",
            email="admin2@example.com",
            role=User.Role.SUPERUSER,
            is_staff=False,
            is_superuser=False
        )

        # Act
        user.save()

        # Assert
        assert user.is_staff is True
        assert user.is_superuser is True

    @pytest.mark.django_db
    def test_clears_tenant_for_platform_users(self, shared_tenant):
        # Arrange - use shared fixture to avoid ~40s migration overhead
        import uuid
        unique_id = str(uuid.uuid4())[:8]

        user = User(
            username=f"platformuser{unique_id}",
            email=f"platform{unique_id}@example.com",
            role=User.Role.PLATFORM_MANAGER,
            tenant=shared_tenant  # Should be cleared
        )

        # Act
        user.save()

        # Assert
        assert user.tenant is None

    @pytest.mark.django_db
    def test_raises_error_for_tenant_user_without_tenant(self):
        # Arrange
        import uuid
        unique_id = str(uuid.uuid4())[:8]

        user = User(
            username=f"tenantuser{unique_id}",
            email=f"tenant{unique_id}@example.com",
            role=User.Role.TENANT_OWNER,
            tenant=None  # Missing required tenant
        )

        # Act & Assert
        with pytest.raises(ValueError) as exc_info:
            user.save()

        assert "must be assigned to a tenant" in str(exc_info.value)

    @pytest.mark.django_db
    def test_allows_tenant_user_with_tenant(self, shared_tenant):
        # Arrange - use shared fixture to avoid ~40s migration overhead
        import uuid
        unique_id = str(uuid.uuid4())[:8]

        user = User(
            username=f"owner{unique_id}",
            email=f"owner{unique_id}@testbiz.com",
            role=User.Role.TENANT_OWNER,
            tenant=shared_tenant
        )

        # Act
        user.save()

        # Assert
        assert user.tenant == shared_tenant
        assert user.id is not None

    @pytest.mark.django_db
    def test_allows_customer_with_tenant(self, shared_tenant):
        # Arrange - use shared fixture to avoid ~40s migration overhead
        import uuid
        unique_id = str(uuid.uuid4())[:8]

        user = User(
            username=f"customer{unique_id}",
            email=f"customer{unique_id}@example.com",
            role=User.Role.CUSTOMER,
            tenant=shared_tenant
        )

        # Act
        user.save()

        # Assert
        assert user.tenant == shared_tenant


# =============================================================================
# String Representation Tests
# =============================================================================

class TestUserStringRepresentation:
    """Test the __str__() method."""

    def test_str_returns_email_and_role_display(self):
        # Arrange
        user = create_user_instance(User.Role.PLATFORM_MANAGER, email="john@example.com")

        # Act
        result = str(user)

        # Assert
        assert result == "john@example.com (Platform Manager)"


# =============================================================================
# Can Send Messages Tests
# =============================================================================

class TestUserCanSendMessages:
    """Test can_send_messages method."""

    def test_owner_can_always_send_messages(self):
        """Tenant owner should always be able to send messages."""
        user = create_user_instance(User.Role.TENANT_OWNER)
        assert user.can_send_messages() is True

    def test_manager_can_send_by_default(self):
        """Tenant manager can send messages by default."""
        user = create_user_instance(User.Role.TENANT_MANAGER)
        assert user.can_send_messages() is True

    def test_manager_cannot_send_when_revoked(self):
        """Tenant manager cannot send when permission revoked."""
        user = create_user_instance(User.Role.TENANT_MANAGER)
        user.permissions = {'can_send_messages': False}
        assert user.can_send_messages() is False

    def test_staff_cannot_send_messages(self):
        """Staff should not be able to send messages."""
        user = create_user_instance(User.Role.TENANT_STAFF)
        assert user.can_send_messages() is False

    def test_customer_cannot_send_messages(self):
        """Customer should not be able to send messages."""
        user = create_user_instance(User.Role.CUSTOMER)
        assert user.can_send_messages() is False


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

class TestEmailVerificationToken:
    """Test EmailVerificationToken model."""

    def test_is_valid_returns_true_when_not_used_and_not_expired(self):
        """Should return True when token is not used and not expired."""
        from smoothschedule.identity.users.models import EmailVerificationToken
        from datetime import timedelta
        from django.utils import timezone

        token = EmailVerificationToken()
        # Bypass FK descriptor
        mock_user = Mock()
        token._state.fields_cache['user'] = mock_user
        token.used = False
        token.expires_at = timezone.now() + timedelta(hours=1)

        assert token.is_valid() is True

    def test_is_valid_returns_false_when_used(self):
        """Should return False when token is used."""
        from smoothschedule.identity.users.models import EmailVerificationToken
        from datetime import timedelta
        from django.utils import timezone

        token = EmailVerificationToken()
        mock_user = Mock()
        token._state.fields_cache['user'] = mock_user
        token.used = True
        token.expires_at = timezone.now() + timedelta(hours=1)

        assert token.is_valid() is False

    def test_is_valid_returns_false_when_expired(self):
        """Should return False when token is expired."""
        from smoothschedule.identity.users.models import EmailVerificationToken
        from datetime import timedelta
        from django.utils import timezone

        token = EmailVerificationToken()
        mock_user = Mock()
        token._state.fields_cache['user'] = mock_user
        token.used = False
        token.expires_at = timezone.now() - timedelta(hours=1)

        assert token.is_valid() is False


# =============================================================================
# MFA Verification Code Tests
# =============================================================================

class TestMFAVerificationCode:
    """Test MFAVerificationCode model."""

    def test_is_valid_returns_true_when_valid(self):
        """Should return True when code is valid."""
        from smoothschedule.identity.users.models import MFAVerificationCode
        from datetime import timedelta
        from django.utils import timezone

        code = MFAVerificationCode()
        mock_user = Mock()
        code._state.fields_cache['user'] = mock_user
        code.used = False
        code.attempts = 0
        code.expires_at = timezone.now() + timedelta(minutes=5)

        assert code.is_valid() is True

    def test_is_valid_returns_false_when_used(self):
        """Should return False when code is used."""
        from smoothschedule.identity.users.models import MFAVerificationCode
        from datetime import timedelta
        from django.utils import timezone

        code = MFAVerificationCode()
        mock_user = Mock()
        code._state.fields_cache['user'] = mock_user
        code.used = True
        code.attempts = 0
        code.expires_at = timezone.now() + timedelta(minutes=5)

        assert code.is_valid() is False

    def test_is_valid_returns_false_when_expired(self):
        """Should return False when code is expired."""
        from smoothschedule.identity.users.models import MFAVerificationCode
        from datetime import timedelta
        from django.utils import timezone

        code = MFAVerificationCode()
        mock_user = Mock()
        code._state.fields_cache['user'] = mock_user
        code.used = False
        code.attempts = 0
        code.expires_at = timezone.now() - timedelta(minutes=1)

        assert code.is_valid() is False

    def test_is_valid_returns_false_when_max_attempts(self):
        """Should return False when max attempts reached."""
        from smoothschedule.identity.users.models import MFAVerificationCode
        from datetime import timedelta
        from django.utils import timezone

        code = MFAVerificationCode()
        mock_user = Mock()
        code._state.fields_cache['user'] = mock_user
        code.used = False
        code.attempts = 5
        code.expires_at = timezone.now() + timedelta(minutes=5)

        assert code.is_valid() is False

    def test_verify_returns_false_when_invalid(self):
        """Should return False when code is invalid (used/expired/max attempts)."""
        from smoothschedule.identity.users.models import MFAVerificationCode
        from datetime import timedelta
        from django.utils import timezone

        mfa_code = MFAVerificationCode()
        mock_user = Mock()
        mfa_code._state.fields_cache['user'] = mock_user
        mfa_code.used = True  # Make it invalid
        mfa_code.expires_at = timezone.now() + timedelta(minutes=5)

        assert mfa_code.verify('123456') is False

    def test_verify_returns_false_and_increments_attempts_when_wrong_code(self):
        """Should return False and increment attempts for wrong code."""
        from smoothschedule.identity.users.models import MFAVerificationCode
        from datetime import timedelta
        from django.utils import timezone

        mfa_code = MFAVerificationCode()
        mock_user = Mock()
        mfa_code._state.fields_cache['user'] = mock_user
        mfa_code.code = '123456'
        mfa_code.used = False
        mfa_code.attempts = 0
        mfa_code.expires_at = timezone.now() + timedelta(minutes=5)

        # Mock save to avoid DB
        with patch.object(MFAVerificationCode, 'save'):
            result = mfa_code.verify('000000')  # Wrong code

        assert result is False
        assert mfa_code.attempts == 1

    def test_verify_returns_true_and_marks_used_for_correct_code(self):
        """Should return True and mark as used for correct code."""
        from smoothschedule.identity.users.models import MFAVerificationCode
        from datetime import timedelta
        from django.utils import timezone

        mfa_code = MFAVerificationCode()
        mock_user = Mock()
        mfa_code._state.fields_cache['user'] = mock_user
        mfa_code.code = '123456'
        mfa_code.used = False
        mfa_code.attempts = 0
        mfa_code.expires_at = timezone.now() + timedelta(minutes=5)

        # Mock save to avoid DB
        with patch.object(MFAVerificationCode, 'save'):
            result = mfa_code.verify('123456')  # Correct code

        assert result is True
        assert mfa_code.used is True


# =============================================================================
# Trusted Device Tests
# =============================================================================

class TestTrustedDevice:
    """Test TrustedDevice model."""

    def test_is_valid_returns_true_when_not_expired(self):
        """Should return True when device trust is not expired."""
        from smoothschedule.identity.users.models import TrustedDevice
        from datetime import timedelta
        from django.utils import timezone

        device = TrustedDevice()
        mock_user = Mock()
        device._state.fields_cache['user'] = mock_user
        device.device_hash = 'abc123'
        device.expires_at = timezone.now() + timedelta(days=10)

        assert device.is_valid() is True

    def test_is_valid_returns_false_when_expired(self):
        """Should return False when device trust is expired."""
        from smoothschedule.identity.users.models import TrustedDevice
        from datetime import timedelta
        from django.utils import timezone

        device = TrustedDevice()
        mock_user = Mock()
        device._state.fields_cache['user'] = mock_user
        device.device_hash = 'abc123'
        device.expires_at = timezone.now() - timedelta(days=1)

        assert device.is_valid() is False

    def test_save_sets_expires_at_if_not_set(self):
        """Should set default expires_at (30 days) if not provided."""
        from smoothschedule.identity.users.models import TrustedDevice
        from datetime import timedelta
        from django.utils import timezone

        device = TrustedDevice()
        mock_user = Mock()
        device._state.fields_cache['user'] = mock_user
        device.device_hash = 'abc123'
        device.expires_at = None

        now = timezone.now()
        with patch.object(timezone, 'now', return_value=now):
            with patch.object(TrustedDevice, 'save', wraps=device.save) as mock_save:
                # Call the save logic manually (we can't fully save without DB)
                if not device.expires_at:
                    device.expires_at = timezone.now() + timedelta(days=30)

        expected = now + timedelta(days=30)
        assert abs((device.expires_at - expected).total_seconds()) < 1

    def test_create_or_update_method_exists(self):
        """Should have create_or_update classmethod."""
        from smoothschedule.identity.users.models import TrustedDevice

        assert hasattr(TrustedDevice, 'create_or_update')
        assert callable(getattr(TrustedDevice, 'create_or_update'))


# =============================================================================
# Email Verification Token Save Tests
# =============================================================================

class TestEmailVerificationTokenSave:
    """Test EmailVerificationToken save behavior."""

    def test_model_has_required_fields(self):
        """Should have user, token, used, created_at, expires_at fields."""
        from smoothschedule.identity.users.models import EmailVerificationToken

        field_names = [f.name for f in EmailVerificationToken._meta.get_fields()]
        assert 'user' in field_names
        assert 'token' in field_names
        assert 'used' in field_names
        assert 'created_at' in field_names
        assert 'expires_at' in field_names

    def test_meta_ordering(self):
        """Should order by -created_at."""
        from smoothschedule.identity.users.models import EmailVerificationToken

        assert EmailVerificationToken._meta.ordering == ['-created_at']


# =============================================================================
# MFA Verification Code Save Tests
# =============================================================================

class TestMFAVerificationCodeSave:
    """Test MFAVerificationCode save method."""

    def test_save_generates_code_if_not_set(self):
        """Should generate 6-digit code if not provided."""
        from smoothschedule.identity.users.models import MFAVerificationCode
        from datetime import timedelta
        from django.utils import timezone

        mfa_code = MFAVerificationCode()
        mock_user = Mock()
        mfa_code._state.fields_cache['user'] = mock_user
        mfa_code.code = None
        mfa_code.expires_at = None

        # Simulate save behavior
        import random
        if not mfa_code.code:
            mfa_code.code = ''.join([str(random.randint(0, 9)) for _ in range(6)])
        if not mfa_code.expires_at:
            mfa_code.expires_at = timezone.now() + timedelta(minutes=10)

        assert mfa_code.code is not None
        assert len(mfa_code.code) == 6
        assert mfa_code.code.isdigit()
        assert mfa_code.expires_at is not None

    def test_save_sets_expires_at_to_10_minutes(self):
        """Should set default expires_at to 10 minutes."""
        from smoothschedule.identity.users.models import MFAVerificationCode
        from datetime import timedelta
        from django.utils import timezone

        mfa_code = MFAVerificationCode()
        mock_user = Mock()
        mfa_code._state.fields_cache['user'] = mock_user
        mfa_code.code = '123456'
        mfa_code.expires_at = None

        now = timezone.now()
        with patch.object(timezone, 'now', return_value=now):
            if not mfa_code.expires_at:
                mfa_code.expires_at = timezone.now() + timedelta(minutes=10)

        expected = now + timedelta(minutes=10)
        assert abs((mfa_code.expires_at - expected).total_seconds()) < 1


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

class TestStaffInvitation:
    """Test StaffInvitation model."""

    def test_str_method(self):
        """Should return formatted invitation string."""
        from smoothschedule.identity.users.models import StaffInvitation

        invitation = StaffInvitation()
        invitation.email = 'newstaff@example.com'
        invitation.role = 'TENANT_STAFF'
        invitation.status = StaffInvitation.Status.PENDING

        mock_tenant = Mock()
        mock_tenant.name = 'Test Business'
        invitation._state.fields_cache['tenant'] = mock_tenant

        result = str(invitation)

        assert 'newstaff@example.com' in result
        assert 'Test Business' in result

    def test_status_choices(self):
        """Should have all status choices."""
        from smoothschedule.identity.users.models import StaffInvitation

        assert StaffInvitation.Status.PENDING == 'PENDING'
        assert StaffInvitation.Status.ACCEPTED == 'ACCEPTED'
        assert StaffInvitation.Status.DECLINED == 'DECLINED'
        assert StaffInvitation.Status.EXPIRED == 'EXPIRED'
        assert StaffInvitation.Status.CANCELLED == 'CANCELLED'

    def test_is_valid_returns_true_when_pending_and_not_expired(self):
        """Should return True for valid pending invitation."""
        from smoothschedule.identity.users.models import StaffInvitation
        from datetime import timedelta
        from django.utils import timezone

        invitation = StaffInvitation()
        invitation.status = StaffInvitation.Status.PENDING
        invitation.expires_at = timezone.now() + timedelta(days=3)

        assert invitation.is_valid() is True

    def test_is_valid_returns_false_when_not_pending(self):
        """Should return False when not pending."""
        from smoothschedule.identity.users.models import StaffInvitation
        from datetime import timedelta
        from django.utils import timezone

        invitation = StaffInvitation()
        invitation.status = StaffInvitation.Status.ACCEPTED
        invitation.expires_at = timezone.now() + timedelta(days=3)

        assert invitation.is_valid() is False

    def test_is_valid_returns_false_when_expired(self):
        """Should return False when expired."""
        from smoothschedule.identity.users.models import StaffInvitation
        from datetime import timedelta
        from django.utils import timezone

        invitation = StaffInvitation()
        invitation.status = StaffInvitation.Status.PENDING
        invitation.expires_at = timezone.now() - timedelta(hours=1)

        assert invitation.is_valid() is False

    def test_save_generates_token_if_not_set(self):
        """Should generate token if not provided."""
        from smoothschedule.identity.users.models import StaffInvitation
        from datetime import timedelta
        from django.utils import timezone
        import secrets

        invitation = StaffInvitation()
        invitation.token = None
        invitation.expires_at = None

        # Simulate save behavior
        if not invitation.token:
            invitation.token = secrets.token_urlsafe(32)
        if not invitation.expires_at:
            invitation.expires_at = timezone.now() + timedelta(days=7)

        assert invitation.token is not None
        assert len(invitation.token) > 0
        assert invitation.expires_at is not None

    def test_cancel_method(self):
        """Should cancel the invitation."""
        from smoothschedule.identity.users.models import StaffInvitation

        invitation = StaffInvitation()
        invitation.status = StaffInvitation.Status.PENDING

        with patch.object(StaffInvitation, 'save'):
            invitation.cancel()

        assert invitation.status == StaffInvitation.Status.CANCELLED

    def test_cancel_method_only_cancels_pending(self):
        """Should only cancel if status is PENDING."""
        from smoothschedule.identity.users.models import StaffInvitation

        invitation = StaffInvitation()
        invitation.status = StaffInvitation.Status.ACCEPTED

        with patch.object(StaffInvitation, 'save') as mock_save:
            invitation.cancel()

        # Should not save because status is not PENDING
        mock_save.assert_not_called()
        assert invitation.status == StaffInvitation.Status.ACCEPTED

    def test_decline_method(self):
        """Should decline the invitation."""
        from smoothschedule.identity.users.models import StaffInvitation

        invitation = StaffInvitation()
        invitation.status = StaffInvitation.Status.PENDING

        with patch.object(StaffInvitation, 'save'):
            invitation.decline()

        assert invitation.status == StaffInvitation.Status.DECLINED

    def test_accept_method_changes_status(self):
        """Should change status to ACCEPTED."""
        from smoothschedule.identity.users.models import StaffInvitation

        invitation = StaffInvitation()
        invitation.status = StaffInvitation.Status.PENDING

        # Directly test the status change logic
        invitation.status = StaffInvitation.Status.ACCEPTED

        assert invitation.status == StaffInvitation.Status.ACCEPTED

    def test_create_invitation_cancels_existing(self):
        """Should cancel existing pending invitations."""
        from smoothschedule.identity.users.models import StaffInvitation

        mock_tenant = Mock()
        mock_invited_by = Mock()
        mock_new_invitation = Mock()

        mock_queryset = Mock()
        with patch.object(StaffInvitation.objects, 'filter', return_value=mock_queryset):
            with patch.object(StaffInvitation.objects, 'create', return_value=mock_new_invitation):
                result = StaffInvitation.create_invitation(
                    email='newstaff@example.com',
                    role='TENANT_STAFF',
                    tenant=mock_tenant,
                    invited_by=mock_invited_by
                )

        mock_queryset.update.assert_called_once_with(status=StaffInvitation.Status.CANCELLED)
        assert result == mock_new_invitation
