"""
Unit tests for QuotaService.

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, call
from django.utils import timezone
from django.core.mail import send_mail
from django.core.exceptions import ObjectDoesNotExist

from smoothschedule.identity.core.quota_service import (
    QuotaService,
    check_tenant_quotas,
    process_expired_grace_periods,
    send_grace_period_reminders,
)


class TestQuotaServiceInit:
    """Test QuotaService initialization."""

    def test_init_stores_tenant(self):
        """Should store tenant reference on initialization."""
        mock_tenant = Mock(id=1, name='Test Tenant')
        service = QuotaService(tenant=mock_tenant)

        assert service.tenant == mock_tenant

    def test_grace_period_constant(self):
        """Should have correct grace period constant."""
        assert QuotaService.GRACE_PERIOD_DAYS == 30

    def test_quota_config_structure(self):
        """Should have properly configured quota types.

        Note: MAX_EMAIL_TEMPLATES removed - email templates are now
        system-wide via PuckEmailTemplate in the messaging app.
        """
        expected_types = [
            'MAX_ADDITIONAL_USERS',
            'MAX_RESOURCES',
            'MAX_SERVICES',
            'MAX_AUTOMATED_TASKS',
        ]

        for quota_type in expected_types:
            assert quota_type in QuotaService.QUOTA_CONFIG
            config = QuotaService.QUOTA_CONFIG[quota_type]
            assert 'model' in config
            assert 'display_name' in config
            assert 'count_method' in config


class TestQuotaServiceCountingMethods:
    """Test resource counting methods."""

    @patch('smoothschedule.identity.core.quota_service.User')
    def test_count_additional_users(self, mock_user_model):
        """Should count users excluding owner and archived."""
        mock_tenant = Mock(id=1)
        mock_queryset = Mock()
        mock_queryset.exclude.return_value.count.return_value = 5

        mock_user_model.objects.filter.return_value = mock_queryset

        service = QuotaService(tenant=mock_tenant)
        count = service.count_additional_users()

        # Verify filtering logic
        mock_user_model.objects.filter.assert_called_once_with(
            tenant=mock_tenant,
            is_archived_by_quota=False
        )
        assert count == 5

    def test_count_resources(self):
        """Should count active resources excluding archived."""
        with patch('smoothschedule.scheduling.schedule.models.Resource') as mock_resource_model:
            mock_queryset = Mock()
            mock_queryset.count.return_value = 10

            mock_resource_model.objects.filter.return_value = mock_queryset

            mock_tenant = Mock(id=1)
            service = QuotaService(tenant=mock_tenant)
            count = service.count_resources()

            mock_resource_model.objects.filter.assert_called_once_with(
                is_archived_by_quota=False
            )
            assert count == 10

    def test_count_services(self):
        """Should count active services excluding archived."""
        with patch('smoothschedule.scheduling.schedule.models.Service') as mock_service_model:
            mock_queryset = Mock()
            mock_queryset.count.return_value = 7

            mock_service_model.objects.filter.return_value = mock_queryset

            mock_tenant = Mock(id=1)
            service = QuotaService(tenant=mock_tenant)
            count = service.count_services()

            mock_service_model.objects.filter.assert_called_once_with(
                is_archived_by_quota=False
            )
            assert count == 7

    # Note: test_count_email_templates removed - email templates are now system-wide
    # using PuckEmailTemplate in the messaging app, not per-tenant quotas

    def test_count_automated_tasks(self):
        """Should count all automated tasks."""
        with patch('smoothschedule.scheduling.schedule.models.ScheduledTask') as mock_task_model:
            mock_queryset = Mock()
            mock_queryset.count.return_value = 12

            mock_task_model.objects = mock_queryset

            mock_tenant = Mock(id=1)
            service = QuotaService(tenant=mock_tenant)
            count = service.count_automated_tasks()

            assert count == 12


class TestQuotaServiceGetCurrentUsage:
    """Test get_current_usage method."""

    def test_get_current_usage_calls_correct_method(self):
        """Should call the appropriate count method for quota type."""
        mock_tenant = Mock(id=1)
        service = QuotaService(tenant=mock_tenant)

        # Mock the count method
        service.count_resources = Mock(return_value=15)

        usage = service.get_current_usage('MAX_RESOURCES')

        service.count_resources.assert_called_once()
        assert usage == 15

    def test_get_current_usage_unknown_quota_type(self):
        """Should return 0 for unknown quota types."""
        mock_tenant = Mock(id=1)
        service = QuotaService(tenant=mock_tenant)

        usage = service.get_current_usage('UNKNOWN_QUOTA')

        assert usage == 0


class TestQuotaServiceGetLimit:
    """Test get_limit method."""

    def test_get_limit_from_billing_entitlements(self):
        """Should get limit from billing entitlements via tenant.get_limit()."""
        mock_tenant = Mock(id=1)
        mock_tenant.get_limit.return_value = 10

        service = QuotaService(tenant=mock_tenant)
        limit = service.get_limit('MAX_ADDITIONAL_USERS')

        mock_tenant.get_limit.assert_called_once_with('max_users')
        assert limit == 10

    def test_get_limit_returns_unlimited_when_none(self):
        """Should return -1 (unlimited) when tenant.get_limit returns None."""
        mock_tenant = Mock(id=1)
        mock_tenant.get_limit.return_value = None  # No limit defined

        service = QuotaService(tenant=mock_tenant)
        limit = service.get_limit('MAX_RESOURCES')

        assert limit == -1

    def test_get_limit_returns_unlimited_when_zero(self):
        """Should return -1 (unlimited) when tenant.get_limit returns 0."""
        mock_tenant = Mock(id=1)
        mock_tenant.get_limit.return_value = 0  # 0 means unlimited in billing

        service = QuotaService(tenant=mock_tenant)
        limit = service.get_limit('MAX_RESOURCES')

        assert limit == -1

    def test_get_limit_maps_quota_types_to_feature_codes(self):
        """Should correctly map quota types to billing feature codes."""
        mock_tenant = Mock(id=1)
        mock_tenant.get_limit.return_value = 5

        service = QuotaService(tenant=mock_tenant)

        # Test each mapping
        service.get_limit('MAX_ADDITIONAL_USERS')
        assert mock_tenant.get_limit.call_args_list[-1][0][0] == 'max_users'

        service.get_limit('MAX_RESOURCES')
        assert mock_tenant.get_limit.call_args_list[-1][0][0] == 'max_resources'

        service.get_limit('MAX_SERVICES')
        assert mock_tenant.get_limit.call_args_list[-1][0][0] == 'max_services'


class TestQuotaServiceCheckQuota:
    """Test check_quota method."""

    @patch('smoothschedule.identity.core.quota_service.QuotaOverage')
    def test_check_quota_returns_none_for_unknown_type(self, mock_overage_model):
        """Should return None and log warning for unknown quota types."""
        mock_tenant = Mock(id=1, name='Test')
        service = QuotaService(tenant=mock_tenant)

        result = service.check_quota('INVALID_QUOTA_TYPE')

        assert result is None

    def test_check_quota_returns_none_for_unlimited(self):
        """Should return None when limit is -1 (unlimited)."""
        mock_tenant = Mock(id=1)
        service = QuotaService(tenant=mock_tenant)

        service.get_limit = Mock(return_value=-1)
        service.count_resources = Mock(return_value=100)

        result = service.check_quota('MAX_RESOURCES')

        assert result is None

    @patch('smoothschedule.identity.core.quota_service.QuotaOverage')
    def test_check_quota_resolves_existing_when_under_limit(self, mock_overage_model):
        """Should resolve existing overage when usage drops below limit."""
        mock_tenant = Mock(id=1)
        service = QuotaService(tenant=mock_tenant)

        # Under limit
        service.get_limit = Mock(return_value=10)
        service.count_resources = Mock(return_value=8)

        # Existing active overage
        mock_existing = Mock(status='ACTIVE')
        mock_queryset = Mock()
        mock_queryset.first.return_value = mock_existing
        mock_overage_model.objects.filter.return_value = mock_queryset

        result = service.check_quota('MAX_RESOURCES')

        # Should resolve the overage
        mock_existing.resolve.assert_called_once_with('USER_DELETED')
        assert result is None

    @patch('smoothschedule.identity.core.quota_service.QuotaOverage')
    def test_check_quota_updates_existing_overage(self, mock_overage_model):
        """Should update existing overage with current counts."""
        mock_tenant = Mock(id=1)
        service = QuotaService(tenant=mock_tenant)

        # Over limit
        service.get_limit = Mock(return_value=10)
        service.count_resources = Mock(return_value=15)

        # Existing active overage
        mock_existing = Mock(
            status='ACTIVE',
            current_usage=12,
            allowed_limit=10
        )
        mock_queryset = Mock()
        mock_queryset.first.return_value = mock_existing
        mock_overage_model.objects.filter.return_value = mock_queryset

        result = service.check_quota('MAX_RESOURCES')

        # Should update counts
        assert mock_existing.current_usage == 15
        assert mock_existing.allowed_limit == 10
        mock_existing.save.assert_called_once()
        assert result == mock_existing

    @patch('smoothschedule.identity.core.quota_service.transaction')
    @patch('smoothschedule.identity.core.quota_service.timezone')
    @patch('smoothschedule.identity.core.quota_service.QuotaOverage')
    def test_check_quota_creates_new_overage(self, mock_overage_model, mock_timezone, mock_transaction):
        """Should create new overage when over limit and none exists."""
        now = timezone.now()
        mock_timezone.now.return_value = now

        # Mock transaction.atomic context manager
        mock_transaction.atomic.return_value.__enter__ = Mock(return_value=None)
        mock_transaction.atomic.return_value.__exit__ = Mock(return_value=None)

        mock_tenant = Mock(id=1)
        service = QuotaService(tenant=mock_tenant)

        # Over limit
        service.get_limit = Mock(return_value=10)
        service.count_resources = Mock(return_value=15)
        service.send_overage_notification = Mock()

        # No existing overage
        mock_queryset = Mock()
        mock_queryset.first.return_value = None
        mock_overage_model.objects.filter.return_value = mock_queryset

        # Mock created overage
        mock_new_overage = Mock(id=1)
        mock_overage_model.objects.create.return_value = mock_new_overage

        result = service.check_quota('MAX_RESOURCES')

        # Should create new overage
        mock_overage_model.objects.create.assert_called_once_with(
            tenant=mock_tenant,
            quota_type='MAX_RESOURCES',
            current_usage=15,
            allowed_limit=10,
            overage_amount=5,
            grace_period_days=30,
            grace_period_ends_at=now + timedelta(days=30)
        )

        # Should send notification
        service.send_overage_notification.assert_called_once_with(
            mock_new_overage, 'initial'
        )

        assert result == mock_new_overage


class TestQuotaServiceCheckAllQuotas:
    """Test check_all_quotas method."""

    def test_check_all_quotas_checks_all_types(self):
        """Should check all configured quota types.

        Note: MAX_EMAIL_TEMPLATES removed - email templates are now
        system-wide via PuckEmailTemplate in the messaging app.
        """
        mock_tenant = Mock(id=1)
        service = QuotaService(tenant=mock_tenant)

        # Mock check_quota to return None (no overages)
        service.check_quota = Mock(return_value=None)

        result = service.check_all_quotas()

        # Should check all quota types (now 4 instead of 5)
        assert service.check_quota.call_count == 4
        quota_types_checked = [call[0][0] for call in service.check_quota.call_args_list]
        assert 'MAX_ADDITIONAL_USERS' in quota_types_checked
        assert 'MAX_RESOURCES' in quota_types_checked
        assert 'MAX_SERVICES' in quota_types_checked
        assert 'MAX_AUTOMATED_TASKS' in quota_types_checked

        assert result == []

    def test_check_all_quotas_returns_new_overages(self):
        """Should return list of newly created overages."""
        mock_tenant = Mock(id=1)
        service = QuotaService(tenant=mock_tenant)

        # Mock some overages
        mock_overage1 = Mock(id=1, quota_type='MAX_RESOURCES')
        mock_overage2 = Mock(id=2, quota_type='MAX_SERVICES')

        def check_quota_side_effect(quota_type):
            if quota_type == 'MAX_RESOURCES':
                return mock_overage1
            elif quota_type == 'MAX_SERVICES':
                return mock_overage2
            return None

        service.check_quota = Mock(side_effect=check_quota_side_effect)

        result = service.check_all_quotas()

        assert len(result) == 2
        assert mock_overage1 in result
        assert mock_overage2 in result


class TestQuotaServiceEmailNotifications:
    """Test email notification methods."""

    @patch('smoothschedule.identity.core.quota_service.send_mail')
    @patch('smoothschedule.identity.core.quota_service.render_to_string')
    @patch('smoothschedule.identity.core.quota_service.User')
    @patch('smoothschedule.identity.core.quota_service.timezone')
    def test_send_overage_notification_initial(
        self, mock_timezone, mock_user_model, mock_render, mock_send_mail
    ):
        """Should send initial overage notification email."""
        now = timezone.now()
        mock_timezone.now.return_value = now

        mock_owner = Mock(email='owner@example.com', role='TENANT_OWNER')
        mock_queryset = Mock()
        mock_queryset.first.return_value = mock_owner
        mock_user_model.objects.filter.return_value = mock_queryset
        mock_user_model.Role.TENANT_OWNER = 'TENANT_OWNER'

        mock_tenant = Mock(id=1, name='Test Tenant')
        mock_tenant.get_primary_domain.return_value = Mock(domain='test.example.com')

        mock_overage = Mock(
            id=1,
            quota_type='MAX_RESOURCES',
            current_usage=15,
            allowed_limit=10,
            overage_amount=5,
            days_remaining=30,
            grace_period_ends_at=now + timedelta(days=30),
            initial_email_sent_at=None,
        )

        mock_render.return_value = '<html>Test</html>'

        service = QuotaService(tenant=mock_tenant)
        service.send_overage_notification(mock_overage, 'initial')

        # Should update overage timestamp
        assert mock_overage.initial_email_sent_at == now
        mock_overage.save.assert_called_once()

        # Should send email
        mock_send_mail.assert_called_once()
        call_kwargs = mock_send_mail.call_args[1]
        assert 'Action Required' in call_kwargs['subject']
        assert call_kwargs['recipient_list'] == ['owner@example.com']

    @patch('smoothschedule.identity.core.quota_service.User')
    def test_send_overage_notification_no_owner_email(self, mock_user_model):
        """Should log warning when owner has no email."""
        mock_queryset = Mock()
        mock_queryset.first.return_value = None
        mock_user_model.objects.filter.return_value = mock_queryset
        mock_user_model.Role.TENANT_OWNER = 'TENANT_OWNER'

        mock_tenant = Mock(id=1, name='Test Tenant')
        mock_overage = Mock(id=1, quota_type='MAX_RESOURCES')

        service = QuotaService(tenant=mock_tenant)
        service.send_overage_notification(mock_overage, 'initial')

        # Should not raise exception, just log warning (not tested here)

    @patch('smoothschedule.identity.core.quota_service.send_mail')
    @patch('smoothschedule.identity.core.quota_service.render_to_string')
    @patch('smoothschedule.identity.core.quota_service.User')
    @patch('smoothschedule.identity.core.quota_service.timezone')
    def test_send_overage_notification_week_reminder(
        self, mock_timezone, mock_user_model, mock_render, mock_send_mail
    ):
        """Should send week reminder notification."""
        now = timezone.now()
        mock_timezone.now.return_value = now

        mock_owner = Mock(email='owner@example.com')
        mock_queryset = Mock()
        mock_queryset.first.return_value = mock_owner
        mock_user_model.objects.filter.return_value = mock_queryset
        mock_user_model.Role.TENANT_OWNER = 'TENANT_OWNER'

        mock_tenant = Mock(id=1, name='Test Tenant')
        mock_tenant.get_primary_domain.return_value = Mock(domain='test.example.com')

        mock_overage = Mock(
            quota_type='MAX_RESOURCES',
            week_reminder_sent_at=None,
        )

        mock_render.return_value = '<html>Test</html>'

        service = QuotaService(tenant=mock_tenant)
        service.send_overage_notification(mock_overage, 'week_reminder')

        # Should update timestamp
        assert mock_overage.week_reminder_sent_at == now

        # Should send email with correct subject
        call_kwargs = mock_send_mail.call_args[1]
        assert '7 days left' in call_kwargs['subject']


class TestQuotaServiceArchiving:
    """Test resource archiving methods."""

    @patch('smoothschedule.identity.core.quota_service.timezone')
    @patch('smoothschedule.identity.core.quota_service.User')
    @patch('smoothschedule.identity.core.quota_service.QuotaOverage')
    def test_archive_resources_users(
        self, mock_overage_model, mock_user_model, mock_timezone
    ):
        """Should archive selected users."""
        now = timezone.now()
        mock_timezone.now.return_value = now

        mock_tenant = Mock(id=1)
        mock_user_model.Role.TENANT_OWNER = 'TENANT_OWNER'

        # Mock user queryset
        mock_queryset = Mock()
        mock_queryset.exclude.return_value.update.return_value = 3
        mock_user_model.objects.filter.return_value = mock_queryset

        # Mock overage
        mock_overage = Mock(allowed_limit=5)
        mock_overage_queryset = Mock()
        mock_overage_queryset.first.return_value = mock_overage
        mock_overage_model.objects.filter.return_value = mock_overage_queryset

        service = QuotaService(tenant=mock_tenant)
        service.count_additional_users = Mock(return_value=4)

        count = service.archive_resources('MAX_ADDITIONAL_USERS', [1, 2, 3])

        assert count == 3

        # Should update users
        mock_user_model.objects.filter.assert_called_once()
        filter_kwargs = mock_user_model.objects.filter.call_args[1]
        assert filter_kwargs['tenant'] == mock_tenant
        assert filter_kwargs['id__in'] == [1, 2, 3]
        assert filter_kwargs['is_archived_by_quota'] is False

        # Should resolve overage if under limit
        mock_overage.resolve.assert_called_once_with('USER_ARCHIVED', [1, 2, 3])

    @patch('smoothschedule.identity.core.quota_service.timezone')
    @patch('smoothschedule.identity.core.quota_service.QuotaOverage')
    def test_archive_resources_resources(
        self, mock_overage_model, mock_timezone
    ):
        """Should archive selected resources."""
        now = timezone.now()
        mock_timezone.now.return_value = now

        mock_tenant = Mock(id=1)

        with patch('smoothschedule.scheduling.schedule.models.Resource') as mock_resource_model:
            # Mock resource queryset
            mock_queryset = Mock()
            mock_queryset.update.return_value = 2
            mock_resource_model.objects.filter.return_value = mock_queryset

            # Mock overage
            mock_overage = Mock(allowed_limit=10)
            mock_overage_queryset = Mock()
            mock_overage_queryset.first.return_value = mock_overage
            mock_overage_model.objects.filter.return_value = mock_overage_queryset

            service = QuotaService(tenant=mock_tenant)
            service.count_resources = Mock(return_value=10)

            count = service.archive_resources('MAX_RESOURCES', [5, 6])

            assert count == 2

            # Should update resources
            mock_resource_model.objects.filter.assert_called_once_with(
                id__in=[5, 6],
                is_archived_by_quota=False
            )

    @patch('smoothschedule.identity.core.quota_service.User')
    def test_unarchive_resource_success(self, mock_user_model):
        """Should unarchive resource when under limit."""
        mock_tenant = Mock(id=1)
        mock_user_model.Role.TENANT_OWNER = 'TENANT_OWNER'

        # Mock queryset
        mock_queryset = Mock()
        mock_queryset.update.return_value = 1
        mock_user_model.objects.filter.return_value = mock_queryset

        service = QuotaService(tenant=mock_tenant)
        service.get_limit = Mock(return_value=10)
        service.count_additional_users = Mock(return_value=8)

        result = service.unarchive_resource('MAX_ADDITIONAL_USERS', 123)

        assert result is True

        # Should update user
        mock_user_model.objects.filter.assert_called_once_with(
            id=123,
            tenant=mock_tenant
        )
        mock_queryset.update.assert_called_once_with(
            is_archived_by_quota=False,
            archived_by_quota_at=None
        )

    def test_unarchive_resource_fails_when_at_limit(self):
        """Should fail to unarchive when at limit."""
        mock_tenant = Mock(id=1)

        service = QuotaService(tenant=mock_tenant)
        service.get_limit = Mock(return_value=10)
        service.count_resources = Mock(return_value=10)

        result = service.unarchive_resource('MAX_RESOURCES', 123)

        assert result is False


class TestQuotaServiceAutoArchive:
    """Test auto-archive functionality."""

    @patch('smoothschedule.identity.core.quota_service.timezone')
    @patch('smoothschedule.identity.core.quota_service.QuotaOverage')
    def test_auto_archive_expired_processes_expired_overages(
        self, mock_overage_model, mock_timezone
    ):
        """Should process all expired overages."""
        now = timezone.now()
        mock_timezone.now.return_value = now

        mock_tenant = Mock(id=1)

        # Mock expired overages
        mock_overage1 = Mock(quota_type='MAX_RESOURCES', overage_amount=3)
        mock_overage2 = Mock(quota_type='MAX_SERVICES', overage_amount=2)

        mock_overage_model.objects.filter.return_value = [
            mock_overage1,
            mock_overage2
        ]

        service = QuotaService(tenant=mock_tenant)
        service._auto_archive_for_overage = Mock(side_effect=[[1, 2, 3], [4, 5]])

        results = service.auto_archive_expired()

        # Should process both overages
        assert results == {'MAX_RESOURCES': 3, 'MAX_SERVICES': 2}

        # Should resolve overages
        mock_overage1.resolve.assert_called_once_with('AUTO_ARCHIVED', [1, 2, 3])
        mock_overage2.resolve.assert_called_once_with('AUTO_ARCHIVED', [4, 5])

    @patch('smoothschedule.identity.core.quota_service.timezone')
    @patch('smoothschedule.identity.core.quota_service.User')
    def test_auto_archive_for_overage_users(self, mock_user_model, mock_timezone):
        """Should auto-archive oldest users."""
        now = timezone.now()
        mock_timezone.now.return_value = now

        mock_tenant = Mock(id=1)
        mock_user_model.Role.TENANT_OWNER = 'TENANT_OWNER'

        # Mock users to archive
        user1 = Mock(id=1)
        user2 = Mock(id=2)
        mock_queryset = Mock()
        mock_queryset.exclude.return_value.order_by.return_value.__getitem__ = Mock(
            return_value=[user1, user2]
        )
        mock_user_model.objects.filter.return_value = mock_queryset

        mock_overage = Mock(quota_type='MAX_ADDITIONAL_USERS', overage_amount=2)

        service = QuotaService(tenant=mock_tenant)
        archived_ids = service._auto_archive_for_overage(mock_overage)

        # Should archive both users
        assert user1.is_archived_by_quota is True
        assert user2.is_archived_by_quota is True
        assert user1.archived_by_quota_at == now
        assert user2.archived_by_quota_at == now
        user1.save.assert_called_once()
        user2.save.assert_called_once()

        assert archived_ids == [1, 2]


class TestQuotaServiceStatusMethods:
    """Test status checking methods."""

    @patch('smoothschedule.identity.core.quota_service.QuotaOverage')
    def test_get_active_overages(self, mock_overage_model):
        """Should return formatted list of active overages."""
        mock_tenant = Mock(id=1)
        now = timezone.now()

        mock_overage1 = Mock(
            id=1,
            quota_type='MAX_RESOURCES',
            current_usage=15,
            allowed_limit=10,
            overage_amount=5,
            days_remaining=25,
            grace_period_ends_at=now + timedelta(days=25),
        )

        mock_overage_model.objects.filter.return_value = [mock_overage1]

        service = QuotaService(tenant=mock_tenant)
        result = service.get_active_overages()

        assert len(result) == 1
        assert result[0]['id'] == 1
        assert result[0]['quota_type'] == 'MAX_RESOURCES'
        assert result[0]['display_name'] == 'resources'
        assert result[0]['current_usage'] == 15
        assert result[0]['allowed_limit'] == 10
        assert result[0]['overage_amount'] == 5
        assert result[0]['days_remaining'] == 25

    @patch('smoothschedule.identity.core.quota_service.QuotaOverage')
    def test_has_active_overages_true(self, mock_overage_model):
        """Should return True when active overages exist."""
        mock_tenant = Mock(id=1)
        mock_queryset = Mock()
        mock_queryset.exists.return_value = True
        mock_overage_model.objects.filter.return_value = mock_queryset

        service = QuotaService(tenant=mock_tenant)
        result = service.has_active_overages()

        assert result is True

    @patch('smoothschedule.identity.core.quota_service.QuotaOverage')
    def test_has_active_overages_false(self, mock_overage_model):
        """Should return False when no active overages."""
        mock_tenant = Mock(id=1)
        mock_queryset = Mock()
        mock_queryset.exists.return_value = False
        mock_overage_model.objects.filter.return_value = mock_queryset

        service = QuotaService(tenant=mock_tenant)
        result = service.has_active_overages()

        assert result is False


class TestHelperFunctions:
    """Test module-level helper functions."""

    @patch('smoothschedule.identity.core.quota_service.QuotaService')
    def test_check_tenant_quotas(self, mock_service_class):
        """Should create QuotaService and check all quotas."""
        mock_tenant = Mock(id=1)
        mock_service = Mock()
        mock_service.check_all_quotas.return_value = [Mock(id=1)]
        mock_service_class.return_value = mock_service

        result = check_tenant_quotas(mock_tenant)

        mock_service_class.assert_called_once_with(mock_tenant)
        mock_service.check_all_quotas.assert_called_once()
        assert len(result) == 1

    @patch('smoothschedule.identity.core.quota_service.QuotaOverage')
    @patch('smoothschedule.identity.core.quota_service.Tenant')
    @patch('smoothschedule.identity.core.quota_service.QuotaService')
    def test_process_expired_grace_periods(
        self, mock_service_class, mock_tenant_model, mock_overage_model
    ):
        """Should process all tenants with expired overages."""
        now = timezone.now()

        # Mock expired overages
        mock_overage_model.objects.filter.return_value.values_list.return_value.distinct.return_value = [
            1, 2
        ]

        # Mock tenants
        mock_tenant1 = Mock(id=1, name='Tenant 1')
        mock_tenant2 = Mock(id=2, name='Tenant 2')

        def get_tenant(id):
            if id == 1:
                return mock_tenant1
            return mock_tenant2

        mock_tenant_model.objects.get.side_effect = get_tenant

        # Mock services
        mock_service1 = Mock()
        mock_service1.auto_archive_expired.return_value = {'MAX_RESOURCES': 3}
        mock_service2 = Mock()
        mock_service2.auto_archive_expired.return_value = {'MAX_SERVICES': 2}

        mock_service_class.side_effect = [mock_service1, mock_service2]

        result = process_expired_grace_periods()

        assert result['overages_processed'] == 2
        assert result['total_archived'] == 5

    @patch('smoothschedule.identity.core.quota_service.timezone')
    @patch('smoothschedule.identity.core.quota_service.QuotaOverage')
    @patch('smoothschedule.identity.core.quota_service.QuotaService')
    def test_send_grace_period_reminders(
        self, mock_service_class, mock_overage_model, mock_timezone
    ):
        """Should send week and day reminders."""
        now = timezone.now()
        mock_timezone.now.return_value = now

        # Mock week reminders
        mock_week_overage = Mock(tenant=Mock(id=1))
        mock_week_queryset = Mock()
        mock_week_queryset.__iter__ = Mock(return_value=iter([mock_week_overage]))

        # Mock day reminders
        mock_day_overage = Mock(tenant=Mock(id=2))
        mock_day_queryset = Mock()
        mock_day_queryset.__iter__ = Mock(return_value=iter([mock_day_overage]))

        # Setup filter to return different querysets
        def filter_side_effect(**kwargs):
            if 'week_reminder_sent_at__isnull' in kwargs:
                return mock_week_queryset
            elif 'day_reminder_sent_at__isnull' in kwargs:
                return mock_day_queryset
            return Mock()

        mock_overage_model.objects.filter.side_effect = filter_side_effect

        # Mock services
        mock_service = Mock()
        mock_service_class.return_value = mock_service

        result = send_grace_period_reminders()

        # Should send both types of reminders
        assert result['week_reminders_sent'] == 1
        assert result['day_reminders_sent'] == 1

        # Should create services for each overage
        assert mock_service_class.call_count == 2
