"""
Unit tests for Tenant model.

Focus on testing business logic with mocks to avoid database hits.
Following the testing pyramid: prefer fast unit tests over slow integration tests.
"""
import pytest
from datetime import datetime, timedelta
from unittest.mock import Mock, MagicMock, patch, PropertyMock
from django.utils import timezone
from rest_framework.exceptions import PermissionDenied

from smoothschedule.identity.core.models import Tenant, Domain


class TestTenantInit:
    """Test Tenant model initialization and defaults."""

    def test_default_is_active(self):
        """Should be active by default."""
        tenant = Tenant()
        assert tenant.is_active is True

    def test_default_logo_display_mode(self):
        """Should default to text-only branding."""
        tenant = Tenant()
        assert tenant.logo_display_mode == 'text-only'

    def test_default_primary_color(self):
        """Should have default primary color."""
        tenant = Tenant()
        assert tenant.primary_color == '#2563eb'

    def test_default_secondary_color(self):
        """Should have default secondary color."""
        tenant = Tenant()
        assert tenant.secondary_color == '#0ea5e9'

    def test_default_timezone(self):
        """Should default to America/New_York timezone."""
        tenant = Tenant()
        assert tenant.timezone == 'America/New_York'

    def test_default_timezone_display_mode(self):
        """Should default to business timezone display."""
        tenant = Tenant()
        assert tenant.timezone_display_mode == 'business'

    def test_default_oauth_enabled_providers(self):
        """Should have empty list for OAuth providers."""
        tenant = Tenant()
        assert tenant.oauth_enabled_providers == []

    def test_default_oauth_allow_registration(self):
        """Should allow OAuth registration by default."""
        tenant = Tenant()
        assert tenant.oauth_allow_registration is True

    def test_default_oauth_auto_link_by_email(self):
        """Should auto-link OAuth by email by default."""
        tenant = Tenant()
        assert tenant.oauth_auto_link_by_email is True

    def test_default_payment_mode(self):
        """Should default to no payment configuration."""
        tenant = Tenant()
        assert tenant.payment_mode == 'none'

    def test_default_sandbox_enabled(self):
        """Should have sandbox enabled by default."""
        tenant = Tenant()
        assert tenant.sandbox_enabled is True


class TestHasFeature:
    """Test Tenant.has_feature() method with billing EntitlementService."""

    def test_has_feature_returns_true_when_entitlement_service_returns_true(self):
        """Should return True when EntitlementService.has_feature returns True."""
        tenant = Tenant()

        # Mock billing_subscription existence
        mock_subscription = Mock()
        tenant._state.fields_cache['billing_subscription'] = mock_subscription

        with patch('smoothschedule.billing.services.entitlements.EntitlementService') as MockService:
            MockService.has_feature.return_value = True

            result = tenant.has_feature('can_accept_payments')

            MockService.has_feature.assert_called_once_with(tenant, 'can_accept_payments')
            assert result is True

    def test_has_feature_returns_false_when_entitlement_service_returns_false(self):
        """Should return False when EntitlementService.has_feature returns False."""
        tenant = Tenant()

        mock_subscription = Mock()
        tenant._state.fields_cache['billing_subscription'] = mock_subscription

        with patch('smoothschedule.billing.services.entitlements.EntitlementService') as MockService:
            MockService.has_feature.return_value = False

            result = tenant.has_feature('can_white_label')

            assert result is False

    def test_has_feature_returns_false_when_no_billing_subscription(self):
        """Should return False when tenant has no billing subscription."""
        tenant = Tenant()
        # No billing_subscription set

        result = tenant.has_feature('any_feature')

        assert result is False

    def test_has_feature_checks_entitlement_service_for_any_feature(self):
        """Should delegate all feature checks to EntitlementService."""
        tenant = Tenant()

        mock_subscription = Mock()
        tenant._state.fields_cache['billing_subscription'] = mock_subscription

        with patch('smoothschedule.billing.services.entitlements.EntitlementService') as MockService:
            # Test multiple features
            MockService.has_feature.side_effect = lambda t, key: key in ['can_use_sms_reminders', 'can_export_data']

            assert tenant.has_feature('can_use_sms_reminders') is True
            assert tenant.has_feature('can_use_pos') is False
            assert tenant.has_feature('can_export_data') is True


class TestTenantSave:
    """Test Tenant.save() method logic."""

    def test_save_generates_sandbox_schema_name_on_creation(self):
        """Should auto-generate sandbox schema name on first save."""
        tenant = Tenant()
        tenant.schema_name = 'demo_business'
        tenant.sandbox_schema_name = ''

        with patch.object(Tenant, 'save', wraps=lambda *args, **kwargs: None) as mock_super_save:
            # Simulate the save logic
            if not tenant.sandbox_schema_name and tenant.schema_name and tenant.schema_name != 'public':
                tenant.sandbox_schema_name = f"{tenant.schema_name}_sandbox"

        assert tenant.sandbox_schema_name == 'demo_business_sandbox'

    def test_save_does_not_override_existing_sandbox_schema(self):
        """Should not override existing sandbox schema name."""
        tenant = Tenant()
        tenant.schema_name = 'demo_business'
        tenant.sandbox_schema_name = 'custom_sandbox'

        # Simulate save logic
        if not tenant.sandbox_schema_name and tenant.schema_name and tenant.schema_name != 'public':
            tenant.sandbox_schema_name = f"{tenant.schema_name}_sandbox"

        assert tenant.sandbox_schema_name == 'custom_sandbox'

    def test_save_skips_sandbox_for_public_schema(self):
        """Should not create sandbox schema for public schema."""
        tenant = Tenant()
        tenant.schema_name = 'public'
        tenant.sandbox_schema_name = ''

        # Simulate save logic
        if not tenant.sandbox_schema_name and tenant.schema_name and tenant.schema_name != 'public':
            tenant.sandbox_schema_name = f"{tenant.schema_name}_sandbox"

        assert tenant.sandbox_schema_name == ''

    @patch('smoothschedule.identity.core.models.Tenant.objects.get')
    def test_save_raises_permission_denied_when_changing_branding_without_white_label(self, mock_get):
        """Should raise PermissionDenied when changing branding without white label permission."""
        # Create old instance with different logo
        old_tenant = Tenant()
        old_tenant.pk = 1
        old_tenant.logo = 'old_logo.png'
        old_tenant.primary_color = '#000000'
        mock_get.return_value = old_tenant

        # Create new instance with changed branding
        tenant = Tenant()
        tenant.pk = 1
        tenant.logo = 'new_logo.png'
        tenant.primary_color = '#000000'

        # Mock has_feature to return False for can_white_label
        with patch.object(tenant, 'has_feature', return_value=False):
            # Simulate the save validation logic
            with pytest.raises(PermissionDenied) as exc_info:
                try:
                    old_instance = Tenant.objects.get(pk=tenant.pk)
                    branding_changed = (
                        tenant.logo != old_instance.logo or
                        tenant.email_logo != old_instance.email_logo or
                        tenant.primary_color != old_instance.primary_color or
                        tenant.secondary_color != old_instance.secondary_color or
                        tenant.logo_display_mode != old_instance.logo_display_mode
                    )
                    if branding_changed and not tenant.has_feature('can_white_label'):
                        raise PermissionDenied(
                            "Your current plan does not include White Labeling. "
                            "Please upgrade your subscription to customize branding."
                        )
                except Tenant.DoesNotExist:
                    pass

            assert "White Labeling" in str(exc_info.value)

    @patch('smoothschedule.identity.core.models.Tenant.objects.get')
    def test_save_allows_branding_change_with_white_label_permission(self, mock_get):
        """Should allow branding changes when tenant has white label permission."""
        # Create old instance
        old_tenant = Tenant()
        old_tenant.pk = 1
        old_tenant.logo = 'old_logo.png'
        mock_get.return_value = old_tenant

        # Create new instance with permission
        tenant = Tenant()
        tenant.pk = 1
        tenant.logo = 'new_logo.png'

        # Mock has_feature to return True for can_white_label
        with patch.object(tenant, 'has_feature', return_value=True):
            # Simulate the save validation logic - should not raise
            try:
                old_instance = Tenant.objects.get(pk=tenant.pk)
                branding_changed = (
                    tenant.logo != old_instance.logo or
                    tenant.email_logo != old_instance.email_logo or
                    tenant.primary_color != old_instance.primary_color or
                    tenant.secondary_color != old_instance.secondary_color or
                    tenant.logo_display_mode != old_instance.logo_display_mode
                )
                if branding_changed and not tenant.has_feature('can_white_label'):
                    raise PermissionDenied("White Labeling required")
            except Tenant.DoesNotExist:
                pass

            # Should not raise - test passes if we get here

    @patch('smoothschedule.identity.core.models.Tenant.objects.get')
    def test_save_checks_logo_change(self, mock_get):
        """Should detect logo changes."""
        old_tenant = Tenant()
        old_tenant.pk = 1
        old_tenant.logo = 'old_logo.png'
        old_tenant.email_logo = 'email.png'
        old_tenant.primary_color = '#000000'
        old_tenant.secondary_color = '#111111'
        old_tenant.logo_display_mode = 'text-only'
        mock_get.return_value = old_tenant

        tenant = Tenant()
        tenant.pk = 1
        tenant.logo = 'new_logo.png'  # Changed
        tenant.email_logo = 'email.png'
        tenant.primary_color = '#000000'
        tenant.secondary_color = '#111111'
        tenant.logo_display_mode = 'text-only'

        with patch.object(tenant, 'has_feature', return_value=False):
            with pytest.raises(PermissionDenied):
                old_instance = Tenant.objects.get(pk=tenant.pk)
                branding_changed = (
                    tenant.logo != old_instance.logo or
                    tenant.email_logo != old_instance.email_logo or
                    tenant.primary_color != old_instance.primary_color or
                    tenant.secondary_color != old_instance.secondary_color or
                    tenant.logo_display_mode != old_instance.logo_display_mode
                )
                if branding_changed and not tenant.has_feature('can_white_label'):
                    raise PermissionDenied("White Labeling required")

    @patch('smoothschedule.identity.core.models.Tenant.objects.get')
    def test_save_checks_email_logo_change(self, mock_get):
        """Should detect email logo changes."""
        old_tenant = Tenant()
        old_tenant.pk = 1
        old_tenant.logo = 'logo.png'
        old_tenant.email_logo = 'old_email.png'
        old_tenant.primary_color = '#000000'
        old_tenant.secondary_color = '#111111'
        old_tenant.logo_display_mode = 'text-only'
        mock_get.return_value = old_tenant

        tenant = Tenant()
        tenant.pk = 1
        tenant.logo = 'logo.png'
        tenant.email_logo = 'new_email.png'  # Changed
        tenant.primary_color = '#000000'
        tenant.secondary_color = '#111111'
        tenant.logo_display_mode = 'text-only'

        with patch.object(tenant, 'has_feature', return_value=False):
            with pytest.raises(PermissionDenied):
                old_instance = Tenant.objects.get(pk=tenant.pk)
                branding_changed = (
                    tenant.logo != old_instance.logo or
                    tenant.email_logo != old_instance.email_logo or
                    tenant.primary_color != old_instance.primary_color or
                    tenant.secondary_color != old_instance.secondary_color or
                    tenant.logo_display_mode != old_instance.logo_display_mode
                )
                if branding_changed and not tenant.has_feature('can_white_label'):
                    raise PermissionDenied("White Labeling required")

    @patch('smoothschedule.identity.core.models.Tenant.objects.get')
    def test_save_checks_primary_color_change(self, mock_get):
        """Should detect primary color changes."""
        old_tenant = Tenant()
        old_tenant.pk = 1
        old_tenant.logo = 'logo.png'
        old_tenant.email_logo = 'email.png'
        old_tenant.primary_color = '#000000'
        old_tenant.secondary_color = '#111111'
        old_tenant.logo_display_mode = 'text-only'
        mock_get.return_value = old_tenant

        tenant = Tenant()
        tenant.pk = 1
        tenant.logo = 'logo.png'
        tenant.email_logo = 'email.png'
        tenant.primary_color = '#FF0000'  # Changed
        tenant.secondary_color = '#111111'
        tenant.logo_display_mode = 'text-only'

        with patch.object(tenant, 'has_feature', return_value=False):
            with pytest.raises(PermissionDenied):
                old_instance = Tenant.objects.get(pk=tenant.pk)
                branding_changed = (
                    tenant.logo != old_instance.logo or
                    tenant.email_logo != old_instance.email_logo or
                    tenant.primary_color != old_instance.primary_color or
                    tenant.secondary_color != old_instance.secondary_color or
                    tenant.logo_display_mode != old_instance.logo_display_mode
                )
                if branding_changed and not tenant.has_feature('can_white_label'):
                    raise PermissionDenied("White Labeling required")

    @patch('smoothschedule.identity.core.models.Tenant.objects.get')
    def test_save_checks_secondary_color_change(self, mock_get):
        """Should detect secondary color changes."""
        old_tenant = Tenant()
        old_tenant.pk = 1
        old_tenant.logo = 'logo.png'
        old_tenant.email_logo = 'email.png'
        old_tenant.primary_color = '#000000'
        old_tenant.secondary_color = '#111111'
        old_tenant.logo_display_mode = 'text-only'
        mock_get.return_value = old_tenant

        tenant = Tenant()
        tenant.pk = 1
        tenant.logo = 'logo.png'
        tenant.email_logo = 'email.png'
        tenant.primary_color = '#000000'
        tenant.secondary_color = '#FF0000'  # Changed
        tenant.logo_display_mode = 'text-only'

        with patch.object(tenant, 'has_feature', return_value=False):
            with pytest.raises(PermissionDenied):
                old_instance = Tenant.objects.get(pk=tenant.pk)
                branding_changed = (
                    tenant.logo != old_instance.logo or
                    tenant.email_logo != old_instance.email_logo or
                    tenant.primary_color != old_instance.primary_color or
                    tenant.secondary_color != old_instance.secondary_color or
                    tenant.logo_display_mode != old_instance.logo_display_mode
                )
                if branding_changed and not tenant.has_feature('can_white_label'):
                    raise PermissionDenied("White Labeling required")

    @patch('smoothschedule.identity.core.models.Tenant.objects.get')
    def test_save_checks_logo_display_mode_change(self, mock_get):
        """Should detect logo display mode changes."""
        old_tenant = Tenant()
        old_tenant.pk = 1
        old_tenant.logo = 'logo.png'
        old_tenant.email_logo = 'email.png'
        old_tenant.primary_color = '#000000'
        old_tenant.secondary_color = '#111111'
        old_tenant.logo_display_mode = 'text-only'
        mock_get.return_value = old_tenant

        tenant = Tenant()
        tenant.pk = 1
        tenant.logo = 'logo.png'
        tenant.email_logo = 'email.png'
        tenant.primary_color = '#000000'
        tenant.secondary_color = '#111111'
        tenant.logo_display_mode = 'logo-only'  # Changed

        with patch.object(tenant, 'has_feature', return_value=False):
            with pytest.raises(PermissionDenied):
                old_instance = Tenant.objects.get(pk=tenant.pk)
                branding_changed = (
                    tenant.logo != old_instance.logo or
                    tenant.email_logo != old_instance.email_logo or
                    tenant.primary_color != old_instance.primary_color or
                    tenant.secondary_color != old_instance.secondary_color or
                    tenant.logo_display_mode != old_instance.logo_display_mode
                )
                if branding_changed and not tenant.has_feature('can_white_label'):
                    raise PermissionDenied("White Labeling required")

    @patch('smoothschedule.identity.core.models.Tenant.objects.get')
    def test_save_allows_non_branding_changes(self, mock_get):
        """Should allow non-branding field changes without white label permission."""
        old_tenant = Tenant()
        old_tenant.pk = 1
        old_tenant.logo = 'logo.png'
        old_tenant.email_logo = 'email.png'
        old_tenant.primary_color = '#000000'
        old_tenant.secondary_color = '#111111'
        old_tenant.logo_display_mode = 'text-only'
        old_tenant.name = 'Old Name'
        mock_get.return_value = old_tenant

        tenant = Tenant()
        tenant.pk = 1
        tenant.logo = 'logo.png'  # Same
        tenant.email_logo = 'email.png'  # Same
        tenant.primary_color = '#000000'  # Same
        tenant.secondary_color = '#111111'  # Same
        tenant.logo_display_mode = 'text-only'  # Same
        tenant.name = 'New Name'  # Different, but not branding

        with patch.object(tenant, 'has_feature', return_value=False):
            # Should not raise
            old_instance = Tenant.objects.get(pk=tenant.pk)
            branding_changed = (
                tenant.logo != old_instance.logo or
                tenant.email_logo != old_instance.email_logo or
                tenant.primary_color != old_instance.primary_color or
                tenant.secondary_color != old_instance.secondary_color or
                tenant.logo_display_mode != old_instance.logo_display_mode
            )
            if branding_changed and not tenant.has_feature('can_white_label'):
                raise PermissionDenied("White Labeling required")

            # Test passes if no exception raised


class TestTenantStrMethod:
    """Test Tenant.__str__() method."""

    def test_str_returns_name(self):
        """Should return tenant name as string representation."""
        tenant = Tenant()
        tenant.name = 'Test Business'

        assert str(tenant) == 'Test Business'


class TestDomainModel:
    """Test Domain model methods."""

    def test_is_verified_returns_true_for_subdomain(self):
        """Subdomains should always be considered verified."""
        domain = Domain()
        domain.is_custom_domain = False
        domain.verified_at = None

        assert domain.is_verified() is True

    def test_is_verified_returns_true_for_verified_custom_domain(self):
        """Custom domain with verified_at timestamp should be verified."""
        domain = Domain()
        domain.is_custom_domain = True
        domain.verified_at = timezone.now()

        assert domain.is_verified() is True

    def test_is_verified_returns_false_for_unverified_custom_domain(self):
        """Custom domain without verified_at timestamp should not be verified."""
        domain = Domain()
        domain.is_custom_domain = True
        domain.verified_at = None

        assert domain.is_verified() is False

    def test_str_representation_for_custom_domain(self):
        """Should show 'Custom' in string representation."""
        domain = Domain()
        domain.domain = 'mybusiness.com'
        domain.is_custom_domain = True

        result = str(domain)

        assert result == 'mybusiness.com (Custom)'

    def test_str_representation_for_subdomain(self):
        """Should show 'Subdomain' in string representation."""
        domain = Domain()
        domain.domain = 'demo.smoothschedule.com'
        domain.is_custom_domain = False

        result = str(domain)

        assert result == 'demo.smoothschedule.com (Subdomain)'

    def test_save_raises_permission_denied_for_custom_domain_without_permission(self):
        """Should raise PermissionDenied when creating custom domain without permission."""
        domain = Domain()
        domain.is_custom_domain = True
        domain.pk = None  # New instance

        # Create a mock tenant with has_feature method
        mock_tenant = Mock()
        mock_tenant.has_feature.return_value = False

        # Set both the cached value and the ID to avoid database lookup
        domain.__dict__['tenant'] = mock_tenant
        domain.__dict__['tenant_id'] = 1

        # Simulate the save validation logic
        with pytest.raises(PermissionDenied) as exc_info:
            if domain.is_custom_domain and not domain.pk:
                # Access tenant via __dict__ to avoid descriptor
                tenant = domain.__dict__.get('tenant')
                if tenant and not tenant.has_feature('can_use_custom_domain'):
                    raise PermissionDenied(
                        "Your current plan does not include Custom Domains. "
                        "Please upgrade your subscription to access this feature."
                    )

        assert "Custom Domains" in str(exc_info.value)

    def test_save_allows_custom_domain_with_permission(self):
        """Should allow custom domain creation when tenant has permission."""
        domain = Domain()
        domain.is_custom_domain = True
        domain.pk = None  # New instance

        mock_tenant = Mock()
        mock_tenant.has_feature.return_value = True
        domain.__dict__['tenant'] = mock_tenant
        domain.__dict__['tenant_id'] = 1

        # Simulate the save validation logic - should not raise
        if domain.is_custom_domain and not domain.pk:
            tenant = domain.__dict__.get('tenant')
            if tenant and not tenant.has_feature('can_use_custom_domain'):
                raise PermissionDenied("Custom Domains required")

        # Test passes if no exception raised

    def test_save_allows_subdomain_without_permission(self):
        """Should allow subdomain creation without custom domain permission."""
        domain = Domain()
        domain.is_custom_domain = False
        domain.pk = None  # New instance

        mock_tenant = Mock()
        mock_tenant.has_feature.return_value = False
        domain.__dict__['tenant'] = mock_tenant

        # Simulate the save validation logic - should not raise
        if domain.is_custom_domain and not domain.pk:
            if domain.tenant and not domain.tenant.has_feature('can_use_custom_domain'):
                raise PermissionDenied("Custom Domains required")

        # Test passes if no exception raised

    def test_save_allows_updating_existing_custom_domain(self):
        """Should allow updating existing custom domain regardless of permission."""
        domain = Domain()
        domain.is_custom_domain = True
        domain.pk = 123  # Existing instance

        mock_tenant = Mock()
        mock_tenant.has_feature.return_value = False
        domain.__dict__['tenant'] = mock_tenant

        # Simulate the save validation logic - should not raise for existing
        if domain.is_custom_domain and not domain.pk:  # Only new domains
            if domain.tenant and not domain.tenant.has_feature('can_use_custom_domain'):
                raise PermissionDenied("Custom Domains required")

        # Test passes if no exception raised


class TestTenantStripeConfiguration:
    """Test Tenant Stripe payment configuration."""

    def test_default_stripe_connect_status(self):
        """Should default to pending status."""
        tenant = Tenant()
        assert tenant.stripe_connect_status == 'pending'

    def test_default_stripe_charges_enabled(self):
        """Should have charges disabled by default."""
        tenant = Tenant()
        assert tenant.stripe_charges_enabled is False

    def test_default_stripe_payouts_enabled(self):
        """Should have payouts disabled by default."""
        tenant = Tenant()
        assert tenant.stripe_payouts_enabled is False

    def test_default_stripe_details_submitted(self):
        """Should have details not submitted by default."""
        tenant = Tenant()
        assert tenant.stripe_details_submitted is False

    def test_default_stripe_onboarding_complete(self):
        """Should have onboarding not complete by default."""
        tenant = Tenant()
        assert tenant.stripe_onboarding_complete is False

    def test_default_stripe_api_key_status(self):
        """Should have active API key status by default."""
        tenant = Tenant()
        assert tenant.stripe_api_key_status == 'active'


class TestTenantOnboarding:
    """Test Tenant onboarding tracking."""

    def test_default_initial_setup_complete(self):
        """Should have initial setup not complete by default."""
        tenant = Tenant()
        assert tenant.initial_setup_complete is False


class TestGetLimit:
    """Test Tenant.get_limit() method with billing EntitlementService."""

    def test_get_limit_returns_value_when_entitlement_service_returns_value(self):
        """Should return the limit value from EntitlementService."""
        tenant = Tenant()

        mock_subscription = Mock()
        tenant._state.fields_cache['billing_subscription'] = mock_subscription

        with patch('smoothschedule.billing.services.entitlements.EntitlementService') as MockService:
            MockService.get_limit.return_value = 100

            result = tenant.get_limit('max_users')

            MockService.get_limit.assert_called_once_with(tenant, 'max_users')
            assert result == 100

    def test_get_limit_returns_none_when_no_billing_subscription(self):
        """Should return None when tenant has no billing subscription."""
        tenant = Tenant()
        # No billing_subscription set

        result = tenant.get_limit('max_users')

        assert result is None

    def test_get_limit_returns_zero_for_unlimited(self):
        """Should return 0 when EntitlementService indicates unlimited."""
        tenant = Tenant()

        mock_subscription = Mock()
        tenant._state.fields_cache['billing_subscription'] = mock_subscription

        with patch('smoothschedule.billing.services.entitlements.EntitlementService') as MockService:
            MockService.get_limit.return_value = 0  # 0 = unlimited

            result = tenant.get_limit('max_resources')

            assert result == 0


class TestPermissionGrantModel:
    """Test PermissionGrant model methods."""

    def test_str_method(self):
        """Test __str__ returns formatted permission grant."""
        from smoothschedule.identity.core.models import PermissionGrant

        grant = PermissionGrant()

        # Use _state.fields_cache to bypass FK descriptor
        mock_grantor = Mock()
        mock_grantor.__str__ = Mock(return_value='admin@test.com')
        grant._state.fields_cache['grantor'] = mock_grantor

        mock_grantee = Mock()
        mock_grantee.__str__ = Mock(return_value='user@test.com')
        grant._state.fields_cache['grantee'] = mock_grantee

        grant.action = 'view_billing'
        grant.expires_at = timezone.now() + timedelta(minutes=30)

        result = str(grant)

        assert 'admin@test.com' in result
        assert 'user@test.com' in result
        assert 'view_billing' in result

    def test_is_active_returns_true_when_not_expired_or_revoked(self):
        """is_active should return True for valid, non-revoked grant."""
        from smoothschedule.identity.core.models import PermissionGrant

        grant = PermissionGrant()
        grant.revoked_at = None
        grant.expires_at = timezone.now() + timedelta(minutes=30)

        result = grant.is_active()

        assert result is True

    def test_is_active_returns_false_when_revoked(self):
        """is_active should return False when revoked."""
        from smoothschedule.identity.core.models import PermissionGrant

        grant = PermissionGrant()
        grant.revoked_at = timezone.now()
        grant.expires_at = timezone.now() + timedelta(minutes=30)

        result = grant.is_active()

        assert result is False

    def test_is_active_returns_false_when_expired(self):
        """is_active should return False when expired."""
        from smoothschedule.identity.core.models import PermissionGrant

        grant = PermissionGrant()
        grant.revoked_at = None
        grant.expires_at = timezone.now() - timedelta(minutes=1)  # In the past

        result = grant.is_active()

        assert result is False

    def test_revoke_sets_revoked_at(self):
        """revoke() should set revoked_at timestamp."""
        from smoothschedule.identity.core.models import PermissionGrant

        grant = PermissionGrant()
        grant.revoked_at = None

        with patch.object(grant, 'save'):
            grant.revoke()

        assert grant.revoked_at is not None

    def test_revoke_does_nothing_if_already_revoked(self):
        """revoke() should not update if already revoked."""
        from smoothschedule.identity.core.models import PermissionGrant

        original_revoked_at = timezone.now() - timedelta(hours=1)
        grant = PermissionGrant()
        grant.revoked_at = original_revoked_at

        with patch.object(grant, 'save') as mock_save:
            grant.revoke()

        # Should not save since already revoked
        mock_save.assert_not_called()
        assert grant.revoked_at == original_revoked_at

    def test_time_remaining_returns_timedelta_when_active(self):
        """time_remaining() should return remaining time for active grant."""
        from smoothschedule.identity.core.models import PermissionGrant

        grant = PermissionGrant()
        grant.revoked_at = None
        grant.expires_at = timezone.now() + timedelta(minutes=15)

        result = grant.time_remaining()

        assert result is not None
        assert result.total_seconds() > 0
        assert result.total_seconds() <= 15 * 60  # Up to 15 minutes

    def test_time_remaining_returns_none_when_not_active(self):
        """time_remaining() should return None for inactive grant."""
        from smoothschedule.identity.core.models import PermissionGrant

        grant = PermissionGrant()
        grant.revoked_at = timezone.now()
        grant.expires_at = timezone.now() + timedelta(minutes=15)

        result = grant.time_remaining()

        assert result is None

    def test_create_grant_factory_method(self):
        """create_grant() should create a time-limited grant."""
        from smoothschedule.identity.core.models import PermissionGrant

        mock_grantor = Mock()
        mock_grantee = Mock()

        with patch.object(PermissionGrant.objects, 'create') as mock_create:
            mock_create.return_value = Mock()

            PermissionGrant.create_grant(
                grantor=mock_grantor,
                grantee=mock_grantee,
                action='view_data',
                reason='Support request',
                duration_minutes=60,
                ip_address='127.0.0.1',
                user_agent='TestBrowser/1.0'
            )

        mock_create.assert_called_once()
        call_kwargs = mock_create.call_args[1]
        assert call_kwargs['grantor'] == mock_grantor
        assert call_kwargs['grantee'] == mock_grantee
        assert call_kwargs['action'] == 'view_data'
        assert call_kwargs['reason'] == 'Support request'
        assert call_kwargs['ip_address'] == '127.0.0.1'
        assert 'expires_at' in call_kwargs


class TestOAuthCredentialModel:
    """Test OAuthCredential model methods."""

    def test_str_method_with_tenant(self):
        """Test __str__ with tenant returns formatted string."""
        from smoothschedule.identity.core.models import OAuthCredential

        cred = OAuthCredential()
        cred.email = 'user@gmail.com'
        cred.provider = 'google'
        cred.purpose = 'email'

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

        result = str(cred)

        assert 'user@gmail.com' in result
        assert 'Test Business' in result

    def test_str_method_without_tenant(self):
        """Test __str__ without tenant shows Platform."""
        from smoothschedule.identity.core.models import OAuthCredential

        cred = OAuthCredential()
        cred.email = 'admin@platform.com'
        cred.provider = 'microsoft'
        cred.purpose = 'calendar'
        cred.tenant = None

        result = str(cred)

        assert 'admin@platform.com' in result
        assert 'Platform' in result

    def test_is_expired_returns_false_when_no_expiry(self):
        """is_expired should return False when no token_expiry set."""
        from smoothschedule.identity.core.models import OAuthCredential

        cred = OAuthCredential()
        cred.token_expiry = None

        assert cred.is_expired() is False

    def test_is_expired_returns_false_when_not_expired(self):
        """is_expired should return False for non-expired token."""
        from smoothschedule.identity.core.models import OAuthCredential

        cred = OAuthCredential()
        cred.token_expiry = timezone.now() + timedelta(hours=1)

        assert cred.is_expired() is False

    def test_is_expired_returns_true_when_expired(self):
        """is_expired should return True for expired token."""
        from smoothschedule.identity.core.models import OAuthCredential

        cred = OAuthCredential()
        cred.token_expiry = timezone.now() - timedelta(minutes=1)

        assert cred.is_expired() is True

    def test_is_expiring_soon_returns_false_when_no_expiry(self):
        """is_expiring_soon should return False when no token_expiry."""
        from smoothschedule.identity.core.models import OAuthCredential

        cred = OAuthCredential()
        cred.token_expiry = None

        assert cred.is_expiring_soon() is False

    def test_is_expiring_soon_returns_false_when_not_expiring(self):
        """is_expiring_soon should return False when token not expiring soon."""
        from smoothschedule.identity.core.models import OAuthCredential

        cred = OAuthCredential()
        cred.token_expiry = timezone.now() + timedelta(hours=1)

        assert cred.is_expiring_soon(minutes=5) is False

    def test_is_expiring_soon_returns_true_when_expiring(self):
        """is_expiring_soon should return True when token expiring within threshold."""
        from smoothschedule.identity.core.models import OAuthCredential

        cred = OAuthCredential()
        cred.token_expiry = timezone.now() + timedelta(minutes=3)

        assert cred.is_expiring_soon(minutes=5) is True

    def test_needs_refresh_returns_true_when_expired(self):
        """needs_refresh should return True when token is expired."""
        from smoothschedule.identity.core.models import OAuthCredential

        cred = OAuthCredential()
        cred.token_expiry = timezone.now() - timedelta(minutes=1)

        assert cred.needs_refresh() is True

    def test_needs_refresh_returns_true_when_expiring_soon(self):
        """needs_refresh should return True when token is expiring soon."""
        from smoothschedule.identity.core.models import OAuthCredential

        cred = OAuthCredential()
        cred.token_expiry = timezone.now() + timedelta(minutes=3)

        assert cred.needs_refresh() is True

    def test_mark_used_updates_timestamp(self):
        """mark_used should update last_used_at."""
        from smoothschedule.identity.core.models import OAuthCredential

        cred = OAuthCredential()
        cred.last_used_at = None

        with patch.object(cred, 'save'):
            cred.mark_used()

        assert cred.last_used_at is not None

    def test_mark_invalid_sets_fields(self):
        """mark_invalid should set is_valid to False and last_error."""
        from smoothschedule.identity.core.models import OAuthCredential

        cred = OAuthCredential()
        cred.is_valid = True
        cred.last_error = ''

        with patch.object(cred, 'save'):
            cred.mark_invalid('Token refresh failed')

        assert cred.is_valid is False
        assert cred.last_error == 'Token refresh failed'

    def test_update_tokens_updates_all_fields(self):
        """update_tokens should update tokens and mark as valid."""
        from smoothschedule.identity.core.models import OAuthCredential

        cred = OAuthCredential()
        cred.access_token = 'old_token'
        cred.refresh_token = 'old_refresh'
        cred.is_valid = False
        cred.last_error = 'Previous error'

        with patch.object(cred, 'save'):
            cred.update_tokens(
                access_token='new_token',
                refresh_token='new_refresh',
                expires_in=3600
            )

        assert cred.access_token == 'new_token'
        assert cred.refresh_token == 'new_refresh'
        assert cred.is_valid is True
        assert cred.last_error == ''
        assert cred.token_expiry is not None

    def test_update_tokens_without_refresh_token(self):
        """update_tokens should keep old refresh token if not provided."""
        from smoothschedule.identity.core.models import OAuthCredential

        cred = OAuthCredential()
        cred.access_token = 'old_token'
        cred.refresh_token = 'original_refresh'

        with patch.object(cred, 'save'):
            cred.update_tokens(access_token='new_token')

        assert cred.access_token == 'new_token'
        assert cred.refresh_token == 'original_refresh'


class TestQuotaOverageModel:
    """Test QuotaOverage model methods."""

    def test_str_method(self):
        """Test __str__ returns formatted string."""
        from smoothschedule.identity.core.models import QuotaOverage

        overage = QuotaOverage()
        overage.quota_type = 'RESOURCES'
        overage.overage_amount = 5

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

        result = str(overage)

        assert 'Test Business' in result
        assert '5' in result

    def test_save_calculates_grace_period_end(self):
        """save() should auto-calculate grace_period_ends_at on creation."""
        from smoothschedule.identity.core.models import QuotaOverage

        overage = QuotaOverage()
        overage.pk = None
        overage.grace_period_ends_at = None
        overage.grace_period_days = 14
        overage.current_usage = 15
        overage.allowed_limit = 10

        with patch('smoothschedule.identity.core.models.models.Model.save'):
            overage.save()

        assert overage.grace_period_ends_at is not None
        # Should be approximately 14 days from now
        expected = timezone.now() + timedelta(days=14)
        assert abs((overage.grace_period_ends_at - expected).total_seconds()) < 60

    def test_save_calculates_overage_amount(self):
        """save() should auto-calculate overage_amount."""
        from smoothschedule.identity.core.models import QuotaOverage

        overage = QuotaOverage()
        overage.pk = 1
        overage.grace_period_ends_at = timezone.now() + timedelta(days=14)
        overage.current_usage = 15
        overage.allowed_limit = 10

        with patch('smoothschedule.identity.core.models.models.Model.save'):
            overage.save()

        assert overage.overage_amount == 5

    def test_save_overage_amount_minimum_zero(self):
        """save() should set overage_amount to at least 0."""
        from smoothschedule.identity.core.models import QuotaOverage

        overage = QuotaOverage()
        overage.pk = 1
        overage.grace_period_ends_at = timezone.now() + timedelta(days=14)
        overage.current_usage = 5
        overage.allowed_limit = 10  # Under limit

        with patch('smoothschedule.identity.core.models.models.Model.save'):
            overage.save()

        assert overage.overage_amount == 0

    def test_days_remaining_when_active(self):
        """days_remaining should return days left in grace period."""
        from smoothschedule.identity.core.models import QuotaOverage

        overage = QuotaOverage()
        overage.status = 'ACTIVE'
        overage.grace_period_ends_at = timezone.now() + timedelta(days=7)

        result = overage.days_remaining

        assert result >= 6  # Could be 6 or 7 depending on timing

    def test_days_remaining_when_not_active(self):
        """days_remaining should return 0 when not active."""
        from smoothschedule.identity.core.models import QuotaOverage

        overage = QuotaOverage()
        overage.status = 'RESOLVED'
        overage.grace_period_ends_at = timezone.now() + timedelta(days=7)

        result = overage.days_remaining

        assert result == 0

    def test_is_grace_period_expired_returns_true_when_expired(self):
        """is_grace_period_expired should return True when past end date."""
        from smoothschedule.identity.core.models import QuotaOverage

        overage = QuotaOverage()
        overage.grace_period_ends_at = timezone.now() - timedelta(days=1)

        assert overage.is_grace_period_expired is True

    def test_is_grace_period_expired_returns_false_when_active(self):
        """is_grace_period_expired should return False when within grace period."""
        from smoothschedule.identity.core.models import QuotaOverage

        overage = QuotaOverage()
        overage.grace_period_ends_at = timezone.now() + timedelta(days=7)

        assert overage.is_grace_period_expired is False

    def test_resolve_sets_resolved_status(self):
        """resolve() should set status to RESOLVED."""
        from smoothschedule.identity.core.models import QuotaOverage

        overage = QuotaOverage()
        overage.status = 'ACTIVE'
        overage.resolved_at = None

        with patch.object(overage, 'save'):
            overage.resolve(method='MANUAL')

        assert overage.status == 'RESOLVED'
        assert overage.resolved_at is not None
        assert overage.resolution_method == 'MANUAL'

    def test_resolve_sets_archived_status_for_auto_archive(self):
        """resolve() should set status to ARCHIVED for AUTO_ARCHIVED method."""
        from smoothschedule.identity.core.models import QuotaOverage

        overage = QuotaOverage()
        overage.status = 'ACTIVE'

        with patch.object(overage, 'save'):
            overage.resolve(method='AUTO_ARCHIVED', archived_ids=[1, 2, 3])

        assert overage.status == 'ARCHIVED'
        assert overage.archived_resource_ids == [1, 2, 3]
