"""
Unit tests for communication credits Celery tasks.

Tests all task logic with mocked dependencies to ensure fast, isolated tests.
Does NOT use @pytest.mark.django_db for maximum speed.
"""
import pytest
from unittest.mock import Mock, patch, MagicMock, call
from datetime import datetime, timedelta, date
from django.utils import timezone
from django.conf import settings


class TestSyncTwilioUsageAllTenants:
    """Tests for sync_twilio_usage_all_tenants task."""

    @patch('smoothschedule.identity.core.models.Tenant')
    @patch('smoothschedule.communication.credits.tasks.sync_twilio_usage_for_tenant')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_queues_sync_for_all_tenants_with_subaccounts(
        self, mock_logger, mock_sync_task, mock_tenant_model
    ):
        """Should queue sync tasks for all tenants with Twilio subaccounts."""
        # Arrange
        from smoothschedule.communication.credits.tasks import sync_twilio_usage_all_tenants

        tenant1 = Mock(id=1, name='Tenant 1', twilio_subaccount_sid='AC123')
        tenant2 = Mock(id=2, name='Tenant 2', twilio_subaccount_sid='AC456')
        mock_tenant_model.objects.exclude.return_value = [tenant1, tenant2]

        # Act
        result = sync_twilio_usage_all_tenants()

        # Assert
        mock_tenant_model.objects.exclude.assert_called_once_with(twilio_subaccount_sid='')
        assert mock_sync_task.delay.call_count == 2
        mock_sync_task.delay.assert_any_call(1)
        mock_sync_task.delay.assert_any_call(2)
        assert result == {'synced': 2, 'errors': 0}

    @patch('smoothschedule.identity.core.models.Tenant')
    @patch('smoothschedule.communication.credits.tasks.sync_twilio_usage_for_tenant')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_handles_errors_when_queuing_tasks(
        self, mock_logger, mock_sync_task, mock_tenant_model
    ):
        """Should handle errors when queuing sync tasks and continue with other tenants."""
        # Arrange
        from smoothschedule.communication.credits.tasks import sync_twilio_usage_all_tenants

        tenant1 = Mock(id=1, name='Tenant 1')
        tenant2 = Mock(id=2, name='Tenant 2')
        tenant3 = Mock(id=3, name='Tenant 3')
        mock_tenant_model.objects.exclude.return_value = [tenant1, tenant2, tenant3]

        # Make second tenant raise an error
        mock_sync_task.delay.side_effect = [
            None,
            Exception('Celery error'),
            None
        ]

        # Act
        result = sync_twilio_usage_all_tenants()

        # Assert
        assert result == {'synced': 2, 'errors': 1}
        mock_logger.error.assert_called_once()

    @patch('smoothschedule.identity.core.models.Tenant')
    @patch('smoothschedule.communication.credits.tasks.sync_twilio_usage_for_tenant')
    def test_returns_zero_when_no_tenants(self, mock_sync_task, mock_tenant_model):
        """Should return zero counts when no tenants have subaccounts."""
        # Arrange
        from smoothschedule.communication.credits.tasks import sync_twilio_usage_all_tenants

        mock_tenant_model.objects.exclude.return_value = []

        # Act
        result = sync_twilio_usage_all_tenants()

        # Assert
        assert result == {'synced': 0, 'errors': 0}
        mock_sync_task.delay.assert_not_called()


class TestSyncTwilioUsageForTenant:
    """Tests for sync_twilio_usage_for_tenant task."""

    @patch('smoothschedule.identity.core.models.Tenant')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_returns_error_when_tenant_not_found(self, mock_logger, mock_tenant_model):
        """Should return error when tenant doesn't exist."""
        # Arrange
        from smoothschedule.communication.credits.tasks import sync_twilio_usage_for_tenant

        # Create a DoesNotExist exception for the mock
        class MockDoesNotExist(Exception):
            pass

        mock_tenant_model.DoesNotExist = MockDoesNotExist
        mock_tenant_model.objects.get.side_effect = MockDoesNotExist

        # Act
        result = sync_twilio_usage_for_tenant(999)

        # Assert
        assert result == {'error': 'Tenant not found'}
        mock_logger.error.assert_called_once()

    @patch('smoothschedule.identity.core.models.Tenant')
    def test_returns_error_when_no_subaccount(self, mock_tenant_model):
        """Should return error when tenant has no Twilio subaccount configured."""
        # Arrange
        from smoothschedule.communication.credits.tasks import sync_twilio_usage_for_tenant

        tenant = Mock(id=1, name='Test', twilio_subaccount_sid='')
        mock_tenant_model.objects.get.return_value = tenant

        # Act
        result = sync_twilio_usage_for_tenant(1)

        # Assert
        assert result == {'error': 'No Twilio subaccount configured'}

    @patch('smoothschedule.communication.credits.tasks.timezone')
    @patch('twilio.rest.Client')
    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    @patch('smoothschedule.identity.core.models.Tenant')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_syncs_twilio_usage_and_deducts_credits(
        self, mock_logger, mock_tenant_model, mock_credits_model,
        mock_twilio_client, mock_tz
    ):
        """Should fetch Twilio usage, calculate charges, and deduct from credits."""
        # Arrange
        from smoothschedule.communication.credits.tasks import sync_twilio_usage_for_tenant

        # Mock tenant
        tenant = Mock(spec=['id', 'name', 'twilio_subaccount_sid', 'twilio_subaccount_auth_token'])
        tenant.id = 1
        tenant.name = 'Test Tenant'
        tenant.twilio_subaccount_sid = 'AC123'
        tenant.twilio_subaccount_auth_token = 'token123'
        mock_tenant_model.objects.get.return_value = tenant

        # Mock credits
        credits = Mock(
            id=1,
            billed_usage_cents=0,
            deduct=Mock(return_value=True)
        )
        mock_credits_model.objects.get_or_create.return_value = (credits, False)

        # Mock Twilio records
        record1 = Mock(price='0.50', category='sms', usage='100')
        record2 = Mock(price='1.25', category='voice', usage='300')
        mock_client = MagicMock()
        mock_client.usage.records.this_month.list.return_value = [record1, record2]
        mock_twilio_client.return_value = mock_client

        # Mock timezone
        today = date(2024, 3, 15)
        mock_tz.now.return_value = Mock(date=Mock(return_value=today))

        # Act
        # Add COMMS_MARKUP_MULTIPLIER to settings
        settings.COMMS_MARKUP_MULTIPLIER = 1.5
        result = sync_twilio_usage_for_tenant(1)
        del settings.COMMS_MARKUP_MULTIPLIER

        # Assert
        mock_twilio_client.assert_called_once_with('AC123', 'token123')
        mock_client.usage.records.this_month.list.assert_called_once()

        # Verify calculations: (0.50 + 1.25) * 100 = 175 cents raw
        # 175 * 1.5 = 262.5 -> 262 cents with markup
        credits.deduct.assert_called_once()
        deduct_call = credits.deduct.call_args
        assert deduct_call[0][0] == 262  # Amount in cents
        assert 'March 2024' in deduct_call[0][1]  # Description
        assert deduct_call[1]['reference_type'] == 'twilio_sync'

        # Verify result
        assert result['tenant'] == 'Test Tenant'
        assert result['raw_cost_cents'] == 175
        assert result['billed_cents'] == 262
        assert result['new_charges_cents'] == 262
        assert 'usage_breakdown' in result

    @patch('smoothschedule.communication.credits.tasks.timezone')
    @patch('twilio.rest.Client')
    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    @patch('smoothschedule.identity.core.models.Tenant')
    def test_does_not_deduct_when_no_new_charges(
        self, mock_tenant_model, mock_credits_model, mock_twilio_client, mock_tz
    ):
        """Should not deduct credits when already billed for current usage."""
        # Arrange
        from smoothschedule.communication.credits.tasks import sync_twilio_usage_for_tenant

        tenant = Mock(
            id=1, name='Test',
            twilio_subaccount_sid='AC123',
            twilio_subaccount_auth_token='token'
        )
        mock_tenant_model.objects.get.return_value = tenant

        credits = Mock(billed_usage_cents=300)  # Already billed for more than usage
        mock_credits_model.objects.get_or_create.return_value = (credits, False)

        record = Mock(price='1.00', category='sms', usage='50')
        mock_client = MagicMock()
        mock_client.usage.records.this_month.list.return_value = [record]
        mock_twilio_client.return_value = mock_client

        today = date(2024, 3, 15)
        mock_tz.now.return_value = Mock(date=Mock(return_value=today))

        # Act
        settings.COMMS_MARKUP_MULTIPLIER = 1.5
        result = sync_twilio_usage_for_tenant(1)
        del settings.COMMS_MARKUP_MULTIPLIER

        # Assert - should not call deduct since new_charges = 150 - 300 = -150 (negative)
        credits.deduct.assert_not_called()
        assert result['new_charges_cents'] == -150

    @patch('smoothschedule.communication.credits.tasks.timezone')
    @patch('twilio.rest.Client')
    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    @patch('smoothschedule.identity.core.models.Tenant')
    def test_updates_sync_tracking_fields(
        self, mock_tenant_model, mock_credits_model, mock_twilio_client, mock_tz
    ):
        """Should update last sync time and billing period tracking."""
        # Arrange
        from smoothschedule.communication.credits.tasks import sync_twilio_usage_for_tenant

        tenant = Mock(
            id=1, name='Test',
            twilio_subaccount_sid='AC123',
            twilio_subaccount_auth_token='token'
        )
        mock_tenant_model.objects.get.return_value = tenant

        credits = Mock(billed_usage_cents=0, save=Mock())
        mock_credits_model.objects.get_or_create.return_value = (credits, False)

        mock_client = MagicMock()
        mock_client.usage.records.this_month.list.return_value = []
        mock_twilio_client.return_value = mock_client

        now = timezone.make_aware(datetime(2024, 3, 15, 10, 30))
        today = date(2024, 3, 15)
        mock_tz.now.return_value = Mock(date=Mock(return_value=today))
        mock_tz.now.return_value = now

        # Act
        sync_twilio_usage_for_tenant(1)

        # Assert
        assert credits.last_twilio_sync_at == now
        assert credits.twilio_sync_period_start == date(2024, 3, 1)
        assert credits.twilio_raw_usage_cents == 0
        assert credits.billed_usage_cents == 0
        credits.save.assert_called_once()

    @patch('twilio.rest.Client')
    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    @patch('smoothschedule.identity.core.models.Tenant')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_handles_twilio_api_errors(
        self, mock_logger, mock_tenant_model, mock_credits_model, mock_twilio_client
    ):
        """Should handle errors from Twilio API and return error dict."""
        # Arrange
        from smoothschedule.communication.credits.tasks import sync_twilio_usage_for_tenant

        tenant = Mock(
            id=1, name='Test',
            twilio_subaccount_sid='AC123',
            twilio_subaccount_auth_token='token'
        )
        mock_tenant_model.objects.get.return_value = tenant

        credits = Mock()
        mock_credits_model.objects.get_or_create.return_value = (credits, False)

        mock_twilio_client.side_effect = Exception('Twilio API error')

        # Act
        result = sync_twilio_usage_for_tenant(1)

        # Assert
        assert 'error' in result
        assert 'Twilio API error' in result['error']
        mock_logger.error.assert_called_once()


class TestSendLowBalanceWarning:
    """Tests for send_low_balance_warning task."""

    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    def test_returns_error_when_credits_not_found(self, mock_credits_model):
        """Should return error when credits record doesn't exist."""
        # Arrange
        from smoothschedule.communication.credits.tasks import send_low_balance_warning

        # Create a DoesNotExist exception for the mock
        class MockDoesNotExist(Exception):
            pass

        mock_credits_model.DoesNotExist = MockDoesNotExist
        mock_credits_model.objects.select_related.return_value.get.side_effect = MockDoesNotExist

        # Act
        result = send_low_balance_warning(999)

        # Assert
        assert result == {'error': 'Credits not found'}

    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_returns_error_when_no_owner_found(self, mock_logger, mock_credits_model):
        """Should return error when tenant has no owner."""
        # Arrange
        from smoothschedule.communication.credits.tasks import send_low_balance_warning

        tenant = Mock(name='Test Tenant')
        tenant.users.filter.return_value.first.return_value = None

        credits = Mock(id=1, tenant=tenant, balance_cents=100)
        mock_credits_model.objects.select_related.return_value.get.return_value = credits

        # Act
        result = send_low_balance_warning(1)

        # Assert
        assert result == {'error': 'No owner email'}
        mock_logger.warning.assert_called_once()

    @patch('django.core.mail.send_mail')
    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    def test_sends_warning_email_without_auto_reload(
        self, mock_credits_model, mock_send_mail
    ):
        """Should send warning email when auto-reload is disabled."""
        # Arrange
        from smoothschedule.communication.credits.tasks import send_low_balance_warning

        owner = Mock(
            email='owner@example.com',
            first_name='John',
            username='john'
        )
        tenant = Mock(name='Test Business')
        tenant.users.filter.return_value.first.return_value = owner

        credits = Mock(
            id=1,
            tenant=tenant,
            balance_cents=400,
            low_balance_warning_cents=500,
            auto_reload_enabled=False
        )
        mock_credits_model.objects.select_related.return_value.get.return_value = credits

        # Act
        result = send_low_balance_warning(1)

        # Assert
        mock_send_mail.assert_called_once()
        call_args = mock_send_mail.call_args
        subject = call_args[0][0]
        message = call_args[0][1]
        recipient = call_args[0][3]

        assert 'Low Communication Credits Balance' in subject
        assert 'Test Business' in subject
        assert 'John' in message
        assert '$4.00' in message  # Balance
        assert '$5.00' in message  # Threshold
        assert 'Auto-reload is NOT enabled' in message
        assert recipient == ['owner@example.com']
        assert result == {'sent_to': 'owner@example.com'}

    @patch('django.core.mail.send_mail')
    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    def test_sends_warning_email_with_auto_reload_enabled(
        self, mock_credits_model, mock_send_mail
    ):
        """Should include auto-reload info when enabled."""
        # Arrange
        from smoothschedule.communication.credits.tasks import send_low_balance_warning

        owner = Mock(email='owner@example.com', first_name='Jane', username='jane')
        tenant = Mock(name='Business')
        tenant.users.filter.return_value.first.return_value = owner

        credits = Mock(
            id=1,
            tenant=tenant,
            balance_cents=800,
            low_balance_warning_cents=1000,
            auto_reload_enabled=True,
            auto_reload_threshold_cents=500
        )
        mock_credits_model.objects.select_related.return_value.get.return_value = credits

        # Act
        send_low_balance_warning(1)

        # Assert
        message = mock_send_mail.call_args[0][1]
        assert 'Auto-reload is ENABLED' in message
        assert '$5.00' in message  # Auto-reload threshold

    @patch('django.core.mail.send_mail')
    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_handles_email_sending_errors(
        self, mock_logger, mock_credits_model, mock_send_mail
    ):
        """Should handle errors when sending email fails."""
        # Arrange
        from smoothschedule.communication.credits.tasks import send_low_balance_warning

        owner = Mock(email='owner@example.com', first_name='John', username='john')
        tenant = Mock(name='Test')
        tenant.users.filter.return_value.first.return_value = owner

        credits = Mock(
            id=1, tenant=tenant, balance_cents=100,
            low_balance_warning_cents=500, auto_reload_enabled=False
        )
        mock_credits_model.objects.select_related.return_value.get.return_value = credits

        mock_send_mail.side_effect = Exception('SMTP error')

        # Act
        result = send_low_balance_warning(1)

        # Assert
        assert 'error' in result
        assert 'SMTP error' in result['error']
        mock_logger.error.assert_called_once()


class TestProcessAutoReload:
    """Tests for process_auto_reload task."""

    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    def test_returns_error_when_credits_not_found(self, mock_credits_model):
        """Should return error when credits record doesn't exist."""
        # Arrange
        from smoothschedule.communication.credits.tasks import process_auto_reload

        # Create a DoesNotExist exception for the mock
        class MockDoesNotExist(Exception):
            pass

        mock_credits_model.DoesNotExist = MockDoesNotExist
        mock_credits_model.objects.select_related.return_value.get.side_effect = MockDoesNotExist

        # Act
        result = process_auto_reload(999)

        # Assert
        assert result == {'error': 'Credits not found'}

    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    def test_returns_error_when_auto_reload_disabled(self, mock_credits_model):
        """Should return error when auto-reload is not enabled."""
        # Arrange
        from smoothschedule.communication.credits.tasks import process_auto_reload

        credits = Mock(id=1, auto_reload_enabled=False)
        mock_credits_model.objects.select_related.return_value.get.return_value = credits

        # Act
        result = process_auto_reload(1)

        # Assert
        assert result == {'error': 'Auto-reload not enabled'}

    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    def test_returns_error_when_no_payment_method(self, mock_credits_model):
        """Should return error when no payment method configured."""
        # Arrange
        from smoothschedule.communication.credits.tasks import process_auto_reload

        credits = Mock(
            id=1,
            auto_reload_enabled=True,
            stripe_payment_method_id=''
        )
        mock_credits_model.objects.select_related.return_value.get.return_value = credits

        # Act
        result = process_auto_reload(1)

        # Assert
        assert result == {'error': 'No payment method'}

    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    def test_skips_when_balance_above_threshold(self, mock_credits_model):
        """Should skip reload when balance is above threshold."""
        # Arrange
        from smoothschedule.communication.credits.tasks import process_auto_reload

        credits = Mock(
            id=1,
            auto_reload_enabled=True,
            stripe_payment_method_id='pm_123',
            balance_cents=2000,
            auto_reload_threshold_cents=1000
        )
        mock_credits_model.objects.select_related.return_value.get.return_value = credits

        # Act
        result = process_auto_reload(1)

        # Assert
        assert result == {'skipped': 'Balance above threshold'}

    @patch('stripe.PaymentIntent')
    @patch('smoothschedule.platform.admin.models.PlatformSettings')
    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_successfully_processes_auto_reload(
        self, mock_logger, mock_credits_model, mock_platform_settings, mock_payment_intent
    ):
        """Should charge payment method and add credits."""
        # Arrange
        from smoothschedule.communication.credits.tasks import process_auto_reload

        tenant = Mock(id=1, name='Test Tenant')
        credits = Mock(
            id=1,
            tenant=tenant,
            auto_reload_enabled=True,
            stripe_payment_method_id='pm_123',
            balance_cents=500,
            auto_reload_threshold_cents=1000,
            auto_reload_amount_cents=2500,
            add_credits=Mock()
        )
        mock_credits_model.objects.select_related.return_value.get.return_value = credits

        platform_settings = Mock()
        platform_settings.get_stripe_secret_key.return_value = 'sk_test_123'
        mock_platform_settings.get_instance.return_value = platform_settings

        payment_intent_obj = Mock(id='pi_123', status='succeeded')
        mock_payment_intent.create.return_value = payment_intent_obj

        # Act
        result = process_auto_reload(1)

        # Assert
        mock_payment_intent.create.assert_called_once()
        create_args = mock_payment_intent.create.call_args[1]
        assert create_args['amount'] == 2500
        assert create_args['currency'] == 'usd'
        assert create_args['payment_method'] == 'pm_123'
        assert create_args['confirm'] is True
        assert 'Test Tenant' in create_args['description']

        credits.add_credits.assert_called_once_with(
            2500,
            transaction_type='auto_reload',
            stripe_charge_id='pi_123',
            description='Auto-reload: $25.00'
        )

        assert result['success'] is True
        assert result['amount_cents'] == 2500
        assert result['payment_intent_id'] == 'pi_123'

    @patch('stripe.PaymentIntent')
    @patch('smoothschedule.platform.admin.models.PlatformSettings')
    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_handles_failed_payment(
        self, mock_logger, mock_credits_model, mock_platform_settings, mock_payment_intent
    ):
        """Should handle failed payment status."""
        # Arrange
        from smoothschedule.communication.credits.tasks import process_auto_reload

        tenant = Mock(id=1, name='Test')
        credits = Mock(
            id=1, tenant=tenant, auto_reload_enabled=True,
            stripe_payment_method_id='pm_123', balance_cents=500,
            auto_reload_threshold_cents=1000, auto_reload_amount_cents=2500
        )
        mock_credits_model.objects.select_related.return_value.get.return_value = credits

        platform_settings = Mock()
        platform_settings.get_stripe_secret_key.return_value = 'sk_test_123'
        mock_platform_settings.get_instance.return_value = platform_settings

        payment_intent_obj = Mock(id='pi_123', status='requires_action')
        mock_payment_intent.create.return_value = payment_intent_obj

        # Act
        result = process_auto_reload(1)

        # Assert
        assert 'error' in result
        assert 'requires_action' in result['error']
        mock_logger.error.assert_called_once()

    @patch('stripe.error.CardError', new=Exception)
    @patch('stripe.PaymentIntent')
    @patch('smoothschedule.platform.admin.models.PlatformSettings')
    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_handles_card_errors(
        self, mock_logger, mock_credits_model, mock_platform_settings, mock_payment_intent
    ):
        """Should handle Stripe card errors."""
        # Arrange
        from smoothschedule.communication.credits.tasks import process_auto_reload

        tenant = Mock(id=1, name='Test')
        credits = Mock(
            id=1, tenant=tenant, auto_reload_enabled=True,
            stripe_payment_method_id='pm_123', balance_cents=500,
            auto_reload_threshold_cents=1000, auto_reload_amount_cents=2500
        )
        mock_credits_model.objects.select_related.return_value.get.return_value = credits

        platform_settings = Mock()
        platform_settings.get_stripe_secret_key.return_value = 'sk_test_123'
        mock_platform_settings.get_instance.return_value = platform_settings

        card_error = Mock(error=Mock(message='Card declined'))
        mock_payment_intent.create.side_effect = card_error

        # Act
        result = process_auto_reload(1)

        # Assert
        assert 'error' in result
        mock_logger.error.assert_called_once()


class TestBillProxyPhoneNumbers:
    """Tests for bill_proxy_phone_numbers task."""

    @patch('smoothschedule.communication.credits.tasks.timezone')
    @patch('smoothschedule.communication.credits.models.ProxyPhoneNumber')
    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_bills_all_assigned_active_numbers(
        self, mock_logger, mock_credits_model, mock_proxy_model, mock_tz
    ):
        """Should bill monthly fee for all assigned active numbers."""
        # Arrange
        from smoothschedule.communication.credits.tasks import bill_proxy_phone_numbers

        now = timezone.make_aware(datetime(2024, 3, 15, 0, 0))
        first_of_month = timezone.make_aware(datetime(2024, 3, 1, 0, 0))
        mock_tz.now.return_value = now

        tenant1 = Mock(id=1, name='Tenant 1')
        tenant2 = Mock(id=2, name='Tenant 2')

        number1 = Mock(
            phone_number='+15551234567',
            assigned_tenant=tenant1,
            monthly_fee_cents=200,
            save=Mock()
        )
        number2 = Mock(
            phone_number='+15559876543',
            assigned_tenant=tenant2,
            monthly_fee_cents=200,
            save=Mock()
        )

        mock_queryset = Mock()
        mock_queryset.filter.return_value.exclude.return_value.select_related.return_value = [
            number1, number2
        ]
        mock_proxy_model.objects = mock_queryset
        mock_proxy_model.Status.ASSIGNED = 'assigned'

        credits1 = Mock(deduct=Mock(return_value=True))
        credits2 = Mock(deduct=Mock(return_value=True))
        mock_credits_model.objects.get_or_create.side_effect = [
            (credits1, False),
            (credits2, False)
        ]

        # Act
        result = bill_proxy_phone_numbers()

        # Assert
        assert mock_credits_model.objects.get_or_create.call_count == 2
        credits1.deduct.assert_called_once_with(
            200,
            'Proxy number +15551234567 - March 2024',
            reference_type='proxy_number',
            reference_id='+15551234567'
        )
        credits2.deduct.assert_called_once_with(
            200,
            'Proxy number +15559876543 - March 2024',
            reference_type='proxy_number',
            reference_id='+15559876543'
        )

        number1.save.assert_called_once()
        number2.save.assert_called_once()
        assert number1.last_billed_at == now
        assert number2.last_billed_at == now

        assert result == {'billed': 2, 'errors': 0}

    @patch('smoothschedule.communication.credits.tasks.timezone')
    @patch('smoothschedule.communication.credits.models.ProxyPhoneNumber')
    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_skips_numbers_without_tenant(
        self, mock_logger, mock_credits_model, mock_proxy_model, mock_tz
    ):
        """Should skip numbers without assigned tenant."""
        # Arrange
        from smoothschedule.communication.credits.tasks import bill_proxy_phone_numbers

        now = timezone.make_aware(datetime(2024, 3, 15, 0, 0))
        mock_tz.now.return_value = now

        number = Mock(phone_number='+15551234567', assigned_tenant=None)

        mock_queryset = Mock()
        mock_queryset.filter.return_value.exclude.return_value.select_related.return_value = [number]
        mock_proxy_model.objects = mock_queryset
        mock_proxy_model.Status.ASSIGNED = 'assigned'

        # Act
        result = bill_proxy_phone_numbers()

        # Assert
        mock_credits_model.objects.get_or_create.assert_not_called()
        assert result == {'billed': 0, 'errors': 0}

    @patch('smoothschedule.communication.credits.tasks.timezone')
    @patch('smoothschedule.communication.credits.models.ProxyPhoneNumber')
    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_handles_insufficient_credits(
        self, mock_logger, mock_credits_model, mock_proxy_model, mock_tz
    ):
        """Should handle case when tenant has insufficient credits."""
        # Arrange
        from smoothschedule.communication.credits.tasks import bill_proxy_phone_numbers

        now = timezone.make_aware(datetime(2024, 3, 15, 0, 0))
        mock_tz.now.return_value = now

        tenant = Mock(id=1, name='Test')
        number = Mock(
            phone_number='+15551234567',
            assigned_tenant=tenant,
            monthly_fee_cents=200,
            save=Mock()
        )

        mock_queryset = Mock()
        mock_queryset.filter.return_value.exclude.return_value.select_related.return_value = [number]
        mock_proxy_model.objects = mock_queryset
        mock_proxy_model.Status.ASSIGNED = 'assigned'

        credits = Mock(deduct=Mock(return_value=None))  # Insufficient balance
        mock_credits_model.objects.get_or_create.return_value = (credits, False)

        # Act
        result = bill_proxy_phone_numbers()

        # Assert
        number.save.assert_not_called()
        mock_logger.warning.assert_called_once()
        assert result == {'billed': 0, 'errors': 1}

    @patch('smoothschedule.communication.credits.tasks.timezone')
    @patch('smoothschedule.communication.credits.models.ProxyPhoneNumber')
    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_handles_billing_errors(
        self, mock_logger, mock_credits_model, mock_proxy_model, mock_tz
    ):
        """Should handle errors during billing and continue with other numbers."""
        # Arrange
        from smoothschedule.communication.credits.tasks import bill_proxy_phone_numbers

        now = timezone.make_aware(datetime(2024, 3, 15, 0, 0))
        mock_tz.now.return_value = now

        tenant = Mock(id=1, name='Test')
        number = Mock(
            phone_number='+15551234567',
            assigned_tenant=tenant,
            monthly_fee_cents=200
        )

        mock_queryset = Mock()
        mock_queryset.filter.return_value.exclude.return_value.select_related.return_value = [number]
        mock_proxy_model.objects = mock_queryset
        mock_proxy_model.Status.ASSIGNED = 'assigned'

        mock_credits_model.objects.get_or_create.side_effect = Exception('DB error')

        # Act
        result = bill_proxy_phone_numbers()

        # Assert
        mock_logger.error.assert_called_once()
        assert result == {'billed': 0, 'errors': 1}


class TestExpireMaskedSessions:
    """Tests for expire_masked_sessions task."""

    @patch('smoothschedule.communication.credits.tasks.timezone')
    @patch('smoothschedule.communication.credits.models.MaskedSession')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_expires_active_sessions_past_expiry(
        self, mock_logger, mock_session_model, mock_tz
    ):
        """Should expire active sessions past their expiry time."""
        # Arrange
        from smoothschedule.communication.credits.tasks import expire_masked_sessions

        now = timezone.make_aware(datetime(2024, 3, 15, 10, 0))
        mock_tz.now.return_value = now

        session1 = Mock(id=1, proxy_number=None, save=Mock())
        session2 = Mock(id=2, proxy_number=None, save=Mock())

        mock_session_model.objects.filter.return_value = [session1, session2]
        mock_session_model.Status.ACTIVE = 'active'
        mock_session_model.Status.EXPIRED = 'expired'

        # Act
        result = expire_masked_sessions()

        # Assert
        mock_session_model.objects.filter.assert_called_once()
        filter_call = mock_session_model.objects.filter.call_args[1]
        assert filter_call['status'] == 'active'

        assert session1.status == 'expired'
        assert session1.closed_at == now
        session1.save.assert_called_once()

        assert session2.status == 'expired'
        assert session2.closed_at == now
        session2.save.assert_called_once()

        assert result == {'expired': 2}

    @patch('smoothschedule.communication.credits.tasks.timezone')
    @patch('smoothschedule.communication.credits.models.MaskedSession')
    @patch('smoothschedule.communication.credits.models.ProxyPhoneNumber')
    def test_releases_reserved_proxy_numbers(
        self, mock_proxy_model, mock_session_model, mock_tz
    ):
        """Should release proxy numbers that were reserved for expired sessions."""
        # Arrange
        from smoothschedule.communication.credits.tasks import expire_masked_sessions

        now = timezone.make_aware(datetime(2024, 3, 15, 10, 0))
        mock_tz.now.return_value = now

        proxy_number = Mock(status='reserved', save=Mock())
        session = Mock(id=1, proxy_number=proxy_number, save=Mock())

        mock_session_model.objects.filter.return_value = [session]
        mock_session_model.Status.ACTIVE = 'active'
        mock_session_model.Status.EXPIRED = 'expired'
        mock_proxy_model.Status.RESERVED = 'reserved'
        mock_proxy_model.Status.AVAILABLE = 'available'

        # Act
        expire_masked_sessions()

        # Assert
        assert proxy_number.status == 'available'
        proxy_number.save.assert_called_once()

    @patch('smoothschedule.communication.credits.tasks.timezone')
    @patch('smoothschedule.communication.credits.models.MaskedSession')
    @patch('smoothschedule.communication.credits.models.ProxyPhoneNumber')
    def test_does_not_release_assigned_proxy_numbers(
        self, mock_proxy_model, mock_session_model, mock_tz
    ):
        """Should not release proxy numbers that are assigned (not reserved)."""
        # Arrange
        from smoothschedule.communication.credits.tasks import expire_masked_sessions

        now = timezone.make_aware(datetime(2024, 3, 15, 10, 0))
        mock_tz.now.return_value = now

        proxy_number = Mock(status='assigned', save=Mock())
        session = Mock(id=1, proxy_number=proxy_number, save=Mock())

        mock_session_model.objects.filter.return_value = [session]
        mock_session_model.Status.ACTIVE = 'active'
        mock_session_model.Status.EXPIRED = 'expired'
        mock_proxy_model.Status.RESERVED = 'reserved'

        # Act
        expire_masked_sessions()

        # Assert - proxy number status should not change
        assert proxy_number.status == 'assigned'
        proxy_number.save.assert_not_called()

    @patch('smoothschedule.communication.credits.tasks.timezone')
    @patch('smoothschedule.communication.credits.models.MaskedSession')
    def test_returns_zero_when_no_expired_sessions(
        self, mock_session_model, mock_tz
    ):
        """Should return zero count when no sessions to expire."""
        # Arrange
        from smoothschedule.communication.credits.tasks import expire_masked_sessions

        mock_session_model.objects.filter.return_value = []
        mock_session_model.Status.ACTIVE = 'active'

        # Act
        result = expire_masked_sessions()

        # Assert
        assert result == {'expired': 0}


class TestCreateTwilioSubaccount:
    """Tests for create_twilio_subaccount task."""

    @patch('smoothschedule.identity.core.models.Tenant')
    def test_returns_error_when_tenant_not_found(self, mock_tenant_model):
        """Should return error when tenant doesn't exist."""
        # Arrange
        from smoothschedule.communication.credits.tasks import create_twilio_subaccount

        # Create a DoesNotExist exception for the mock
        class MockDoesNotExist(Exception):
            pass

        mock_tenant_model.DoesNotExist = MockDoesNotExist
        mock_tenant_model.objects.get.side_effect = MockDoesNotExist

        # Act
        result = create_twilio_subaccount(999)

        # Assert
        assert result == {'error': 'Tenant not found'}

    @patch('smoothschedule.identity.core.models.Tenant')
    def test_skips_when_subaccount_exists(self, mock_tenant_model):
        """Should skip creation when tenant already has a subaccount."""
        # Arrange
        from smoothschedule.communication.credits.tasks import create_twilio_subaccount

        tenant = Mock(id=1, name='Test', twilio_subaccount_sid='AC123')
        mock_tenant_model.objects.get.return_value = tenant

        # Act
        result = create_twilio_subaccount(1)

        # Assert
        assert result == {'skipped': 'Subaccount already exists'}

    @patch('twilio.rest.Client')
    @patch('smoothschedule.identity.core.models.Tenant')
    def test_returns_error_when_no_master_credentials(
        self, mock_tenant_model, mock_twilio_client
    ):
        """Should return error when Twilio master credentials not configured."""
        # Arrange
        from smoothschedule.communication.credits.tasks import create_twilio_subaccount

        tenant = Mock(id=1, name='Test', twilio_subaccount_sid='')
        mock_tenant_model.objects.get.return_value = tenant

        # Act
        settings.TWILIO_ACCOUNT_SID = ''
        result = create_twilio_subaccount(1)
        if hasattr(settings, 'TWILIO_ACCOUNT_SID'):
            del settings.TWILIO_ACCOUNT_SID

        # Assert
        assert result == {'error': 'Twilio master credentials not configured'}

    @patch('twilio.rest.Client')
    @patch('smoothschedule.identity.core.models.Tenant')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_creates_subaccount_successfully(
        self, mock_logger, mock_tenant_model, mock_twilio_client
    ):
        """Should create Twilio subaccount and save credentials to tenant."""
        # Arrange
        from smoothschedule.communication.credits.tasks import create_twilio_subaccount

        tenant = Mock(spec=['id', 'name', 'twilio_subaccount_sid', 'twilio_subaccount_auth_token', 'save'])
        tenant.id = 1
        tenant.name = 'Test Business'
        tenant.twilio_subaccount_sid = ''
        tenant.save = Mock()
        mock_tenant_model.objects.get.return_value = tenant

        subaccount = Mock(sid='AC987654321', auth_token='token_abc')
        mock_client = MagicMock()
        mock_client.api.accounts.create.return_value = subaccount
        mock_twilio_client.return_value = mock_client

        # Act
        settings.TWILIO_ACCOUNT_SID = 'AC_MASTER'
        settings.TWILIO_AUTH_TOKEN = 'master_token'
        result = create_twilio_subaccount(1)
        if hasattr(settings, 'TWILIO_ACCOUNT_SID'):
            del settings.TWILIO_ACCOUNT_SID
        if hasattr(settings, 'TWILIO_AUTH_TOKEN'):
            del settings.TWILIO_AUTH_TOKEN

        # Assert
        mock_twilio_client.assert_called_once_with('AC_MASTER', 'master_token')
        mock_client.api.accounts.create.assert_called_once_with(
            friendly_name='SmoothSchedule - Test Business'
        )

        assert tenant.twilio_subaccount_sid == 'AC987654321'
        assert tenant.twilio_subaccount_auth_token == 'token_abc'
        tenant.save.assert_called_once()

        assert result == {'success': True, 'subaccount_sid': 'AC987654321'}

    @patch('twilio.rest.Client')
    @patch('smoothschedule.identity.core.models.Tenant')
    @patch('smoothschedule.communication.credits.tasks.logger')
    def test_handles_twilio_api_errors(
        self, mock_logger, mock_tenant_model, mock_twilio_client
    ):
        """Should handle errors from Twilio API."""
        # Arrange
        from smoothschedule.communication.credits.tasks import create_twilio_subaccount

        tenant = Mock(id=1, name='Test', twilio_subaccount_sid='')
        mock_tenant_model.objects.get.return_value = tenant

        mock_twilio_client.side_effect = Exception('Twilio API error')

        # Act
        settings.TWILIO_ACCOUNT_SID = 'AC_MASTER'
        settings.TWILIO_AUTH_TOKEN = 'master_token'
        result = create_twilio_subaccount(1)
        if hasattr(settings, 'TWILIO_ACCOUNT_SID'):
            del settings.TWILIO_ACCOUNT_SID
        if hasattr(settings, 'TWILIO_AUTH_TOKEN'):
            del settings.TWILIO_AUTH_TOKEN

        # Assert
        assert 'error' in result
        assert 'Twilio API error' in result['error']
        mock_logger.error.assert_called_once()
