"""
Unit tests for Communication Credits API Views.

These tests use mocks extensively to avoid database dependencies and run quickly.
They test all API endpoints, permissions, Stripe integration, and Twilio integration.
"""
from unittest.mock import Mock, patch, MagicMock, call
from datetime import datetime, timedelta, timezone as dt_timezone
from django.utils import timezone
from rest_framework.test import APIRequestFactory
from rest_framework import status
import pytest
import stripe
from twilio.base.exceptions import TwilioRestException, TwilioException


class TestGetOrCreateCreditsHelper:
    """Tests for get_or_create_credits helper function."""

    def test_get_or_create_credits_returns_existing(self):
        """Test helper returns existing credits."""
        from smoothschedule.communication.credits.views import get_or_create_credits

        mock_tenant = Mock(id=1)
        mock_credits = Mock(id=42, balance_cents=1000)

        with patch('smoothschedule.communication.credits.models.CommunicationCredits.objects.get_or_create') as mock_get_or_create:
            mock_get_or_create.return_value = (mock_credits, False)

            result = get_or_create_credits(mock_tenant)

            assert result == mock_credits
            mock_get_or_create.assert_called_once_with(tenant=mock_tenant)

    def test_get_or_create_credits_creates_new(self):
        """Test helper creates new credits when none exist."""
        from smoothschedule.communication.credits.views import get_or_create_credits

        mock_tenant = Mock(id=1)
        mock_credits = Mock(id=42, balance_cents=0)

        with patch('smoothschedule.communication.credits.models.CommunicationCredits.objects.get_or_create') as mock_get_or_create:
            mock_get_or_create.return_value = (mock_credits, True)

            result = get_or_create_credits(mock_tenant)

            assert result == mock_credits
            mock_get_or_create.assert_called_once_with(tenant=mock_tenant)


class TestGetCreditsView:
    """Tests for get_credits_view endpoint."""

    def test_get_credits_success(self):
        """Test GET credits returns credit data."""
        from smoothschedule.communication.credits.views import get_credits_view

        factory = APIRequestFactory()
        request = factory.get('/api/credits/')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_credits = Mock(
            id=42,
            balance_cents=5000,
            auto_reload_enabled=True,
            auto_reload_threshold_cents=1000,
            auto_reload_amount_cents=2500,
            low_balance_warning_cents=500,
            low_balance_warning_sent=False,
            stripe_payment_method_id='pm_123',
            last_twilio_sync_at=timezone.now(),
            total_loaded_cents=10000,
            total_spent_cents=5000,
            created_at=timezone.now(),
            updated_at=timezone.now(),
        )

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get:
            mock_get.return_value = mock_credits

            response = get_credits_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert response.data['id'] == 42
            assert response.data['balance_cents'] == 5000
            assert response.data['auto_reload_enabled'] is True
            assert response.data['stripe_payment_method_id'] == 'pm_123'

    def test_get_credits_no_tenant(self):
        """Test GET credits fails without tenant context."""
        from smoothschedule.communication.credits.views import get_credits_view

        factory = APIRequestFactory()
        request = factory.get('/api/credits/')
        request.user = Mock(is_authenticated=True)
        request.tenant = None

        response = get_credits_view(request)

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


class TestUpdateSettingsView:
    """Tests for update_settings_view endpoint."""

    def test_update_settings_success(self):
        """Test PATCH settings updates allowed fields."""
        from smoothschedule.communication.credits.views import update_settings_view

        factory = APIRequestFactory()
        request = factory.patch('/api/credits/settings/', {
            'auto_reload_enabled': True,
            'auto_reload_threshold_cents': 2000,
            'auto_reload_amount_cents': 5000,
            'low_balance_warning_cents': 1000,
            'stripe_payment_method_id': 'pm_new',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_credits = Mock(
            id=42,
            balance_cents=3000,
            auto_reload_enabled=False,
            auto_reload_threshold_cents=1000,
            auto_reload_amount_cents=2500,
            low_balance_warning_cents=500,
            low_balance_warning_sent=False,
            stripe_payment_method_id='pm_old',
            last_twilio_sync_at=None,
            total_loaded_cents=5000,
            total_spent_cents=2000,
            created_at=timezone.now(),
            updated_at=timezone.now(),
        )

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get:
            mock_get.return_value = mock_credits

            response = update_settings_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert mock_credits.auto_reload_enabled is True
            assert mock_credits.auto_reload_threshold_cents == 2000
            assert mock_credits.auto_reload_amount_cents == 5000
            assert mock_credits.low_balance_warning_cents == 1000
            assert mock_credits.stripe_payment_method_id == 'pm_new'
            mock_credits.save.assert_called_once()

    def test_update_settings_partial_update(self):
        """Test PATCH settings with partial data updates only provided fields."""
        from smoothschedule.communication.credits.views import update_settings_view

        factory = APIRequestFactory()
        request = factory.patch('/api/credits/settings/', {
            'auto_reload_enabled': True,
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_credits = Mock(
            id=42,
            balance_cents=3000,
            auto_reload_enabled=False,
            auto_reload_threshold_cents=1000,
            auto_reload_amount_cents=2500,
            low_balance_warning_cents=500,
            low_balance_warning_sent=False,
            stripe_payment_method_id='pm_123',
            last_twilio_sync_at=None,
            total_loaded_cents=5000,
            total_spent_cents=2000,
            created_at=timezone.now(),
            updated_at=timezone.now(),
        )

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get:
            mock_get.return_value = mock_credits

            response = update_settings_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert mock_credits.auto_reload_enabled is True
            # Other fields should remain unchanged
            assert mock_credits.auto_reload_threshold_cents == 1000
            assert mock_credits.stripe_payment_method_id == 'pm_123'

    def test_update_settings_no_tenant(self):
        """Test PATCH settings fails without tenant context."""
        from smoothschedule.communication.credits.views import update_settings_view

        factory = APIRequestFactory()
        request = factory.patch('/api/credits/settings/', {}, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = None

        response = update_settings_view(request)

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


class TestAddCreditsView:
    """Tests for add_credits_view endpoint."""

    def test_add_credits_success(self):
        """Test POST add credits with valid payment succeeds."""
        from smoothschedule.communication.credits.views import add_credits_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/add/', {
            'amount_cents': 5000,
            'payment_method_id': 'pm_123',
            'save_payment_method': True,
        }, format='json')
        request.user = Mock(is_authenticated=True, email='test@example.com')
        request.tenant = Mock(id=1, name='Test Business')

        mock_credits = Mock(
            id=42,
            balance_cents=1000,
            stripe_customer_id='',
        )

        mock_payment_intent = Mock(
            id='pi_123',
            status='succeeded',
        )

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.views._get_or_create_stripe_customer') as mock_get_customer, \
             patch('smoothschedule.communication.credits.views.stripe.PaymentMethod.attach') as mock_attach, \
             patch('smoothschedule.communication.credits.views.stripe.PaymentIntent.create') as mock_create_pi, \
             patch('smoothschedule.communication.credits.views.transaction'):

            mock_get.return_value = mock_credits
            mock_get_customer.return_value = 'cus_123'
            mock_create_pi.return_value = mock_payment_intent

            response = add_credits_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert response.data['success'] is True
            assert response.data['payment_intent_id'] == 'pi_123'

            # Verify Stripe calls
            mock_attach.assert_called_once_with('pm_123', customer='cus_123')
            mock_create_pi.assert_called_once()

            # Verify credits added
            mock_credits.add_credits.assert_called_once_with(
                amount_cents=5000,
                transaction_type='manual',
                stripe_charge_id='pi_123',
                description='Added $50.00 via Stripe'
            )

            # Verify payment method saved
            assert mock_credits.stripe_payment_method_id == 'pm_123'

    def test_add_credits_requires_action(self):
        """Test POST add credits returns client secret when 3D Secure required."""
        from smoothschedule.communication.credits.views import add_credits_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/add/', {
            'amount_cents': 5000,
            'payment_method_id': 'pm_123',
        }, format='json')
        request.user = Mock(is_authenticated=True, email='test@example.com')
        request.tenant = Mock(id=1, name='Test Business')

        mock_credits = Mock(id=42, balance_cents=1000, stripe_customer_id='cus_123')

        mock_payment_intent = Mock(
            id='pi_123',
            status='requires_action',
            client_secret='pi_123_secret_456',
        )

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.views._get_or_create_stripe_customer') as mock_get_customer, \
             patch('smoothschedule.communication.credits.views.stripe.PaymentMethod.attach'), \
             patch('smoothschedule.communication.credits.views.stripe.PaymentIntent.create') as mock_create_pi:

            mock_get.return_value = mock_credits
            mock_get_customer.return_value = 'cus_123'
            mock_create_pi.return_value = mock_payment_intent

            response = add_credits_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert response.data['requires_action'] is True
            assert response.data['payment_intent_client_secret'] == 'pi_123_secret_456'

    def test_add_credits_minimum_amount_validation(self):
        """Test POST add credits validates minimum amount."""
        from smoothschedule.communication.credits.views import add_credits_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/add/', {
            'amount_cents': 400,  # Less than $5 minimum
            'payment_method_id': 'pm_123',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        response = add_credits_view(request)

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

    def test_add_credits_requires_payment_method(self):
        """Test POST add credits requires payment method ID."""
        from smoothschedule.communication.credits.views import add_credits_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/add/', {
            'amount_cents': 5000,
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        response = add_credits_view(request)

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

    def test_add_credits_handles_card_error(self):
        """Test POST add credits handles Stripe card errors."""
        from smoothschedule.communication.credits.views import add_credits_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/add/', {
            'amount_cents': 5000,
            'payment_method_id': 'pm_123',
        }, format='json')
        request.user = Mock(is_authenticated=True, email='test@example.com')
        request.tenant = Mock(id=1, name='Test Business')

        mock_credits = Mock(id=42, stripe_customer_id='cus_123')

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.views._get_or_create_stripe_customer') as mock_get_customer, \
             patch('smoothschedule.communication.credits.views.stripe.PaymentMethod.attach'), \
             patch('smoothschedule.communication.credits.views.stripe.PaymentIntent.create') as mock_create_pi, \
             patch('smoothschedule.communication.credits.views.transaction'):

            mock_get.return_value = mock_credits
            mock_get_customer.return_value = 'cus_123'

            # Simulate card error by creating a real CardError exception
            # We need to patch the exception handler to check the user_message attribute
            def create_card_error(*args, **kwargs):
                err = stripe.error.CardError('Card declined', 'param', 'code')
                # Monkey-patch the user_message onto the instance (readonly property workaround)
                object.__setattr__(err, '_user_message', 'Your card was declined')
                # Override the user_message property getter
                type(err).user_message = property(lambda self: self._user_message)
                raise err

            mock_create_pi.side_effect = create_card_error

            response = add_credits_view(request)

            assert response.status_code == status.HTTP_400_BAD_REQUEST
            assert 'Your card was declined' in response.data['error']

    def test_add_credits_handles_stripe_error(self):
        """Test POST add credits handles generic Stripe errors."""
        from smoothschedule.communication.credits.views import add_credits_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/add/', {
            'amount_cents': 5000,
            'payment_method_id': 'pm_123',
        }, format='json')
        request.user = Mock(is_authenticated=True, email='test@example.com')
        request.tenant = Mock(id=1, name='Test Business')

        mock_credits = Mock(id=42, stripe_customer_id='cus_123')

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.views._get_or_create_stripe_customer') as mock_get_customer, \
             patch('smoothschedule.communication.credits.views.stripe.PaymentMethod.attach'), \
             patch('smoothschedule.communication.credits.views.stripe.PaymentIntent.create') as mock_create_pi:

            mock_get.return_value = mock_credits
            mock_get_customer.return_value = 'cus_123'
            mock_create_pi.side_effect = stripe.error.StripeError('API Error')

            response = add_credits_view(request)

            assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
            assert 'Payment processing error' in response.data['error']

    def test_add_credits_payment_method_already_attached(self):
        """Test POST add credits handles already attached payment method."""
        from smoothschedule.communication.credits.views import add_credits_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/add/', {
            'amount_cents': 5000,
            'payment_method_id': 'pm_123',
        }, format='json')
        request.user = Mock(is_authenticated=True, email='test@example.com')
        request.tenant = Mock(id=1, name='Test Business')

        mock_credits = Mock(id=42, balance_cents=1000, stripe_customer_id='cus_123')
        mock_payment_intent = Mock(id='pi_123', status='succeeded')

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.views._get_or_create_stripe_customer') as mock_get_customer, \
             patch('smoothschedule.communication.credits.views.stripe.PaymentMethod.attach') as mock_attach, \
             patch('smoothschedule.communication.credits.views.stripe.PaymentIntent.create') as mock_create_pi, \
             patch('smoothschedule.communication.credits.views.transaction'):

            mock_get.return_value = mock_credits
            mock_get_customer.return_value = 'cus_123'

            # Simulate payment method already attached
            error = stripe.error.InvalidRequestError(
                'already been attached',
                'param'
            )
            mock_attach.side_effect = error
            mock_create_pi.return_value = mock_payment_intent

            response = add_credits_view(request)

            # Should succeed despite attach error
            assert response.status_code == status.HTTP_200_OK
            assert response.data['success'] is True

    def test_add_credits_no_tenant(self):
        """Test POST add credits fails without tenant context."""
        from smoothschedule.communication.credits.views import add_credits_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/add/', {}, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = None

        response = add_credits_view(request)

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


class TestCreatePaymentIntentView:
    """Tests for create_payment_intent_view endpoint."""

    def test_create_payment_intent_success(self):
        """Test POST create payment intent returns client secret."""
        from smoothschedule.communication.credits.views import create_payment_intent_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/payment-intent/', {
            'amount_cents': 5000,
        }, format='json')
        request.user = Mock(is_authenticated=True, email='test@example.com')
        request.tenant = Mock(id=1, name='Test Business')

        mock_credits = Mock(id=42, stripe_customer_id='')
        mock_payment_intent = Mock(
            id='pi_123',
            client_secret='pi_123_secret_456',
        )

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.views._get_or_create_stripe_customer') as mock_get_customer, \
             patch('smoothschedule.communication.credits.views.stripe.PaymentIntent.create') as mock_create_pi:

            mock_get.return_value = mock_credits
            mock_get_customer.return_value = 'cus_123'
            mock_create_pi.return_value = mock_payment_intent

            response = create_payment_intent_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert response.data['client_secret'] == 'pi_123_secret_456'
            assert response.data['payment_intent_id'] == 'pi_123'

            # Verify payment intent created with correct params
            mock_create_pi.assert_called_once()
            call_kwargs = mock_create_pi.call_args[1]
            assert call_kwargs['amount'] == 5000
            assert call_kwargs['currency'] == 'usd'
            assert call_kwargs['customer'] == 'cus_123'

    def test_create_payment_intent_minimum_validation(self):
        """Test POST create payment intent validates minimum amount."""
        from smoothschedule.communication.credits.views import create_payment_intent_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/payment-intent/', {
            'amount_cents': 400,
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        response = create_payment_intent_view(request)

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

    def test_create_payment_intent_handles_stripe_error(self):
        """Test POST create payment intent handles Stripe errors."""
        from smoothschedule.communication.credits.views import create_payment_intent_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/payment-intent/', {
            'amount_cents': 5000,
        }, format='json')
        request.user = Mock(is_authenticated=True, email='test@example.com')
        request.tenant = Mock(id=1, name='Test Business')

        mock_credits = Mock(id=42, stripe_customer_id='cus_123')

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.views._get_or_create_stripe_customer') as mock_get_customer, \
             patch('smoothschedule.communication.credits.views.stripe.PaymentIntent.create') as mock_create_pi:

            mock_get.return_value = mock_credits
            mock_get_customer.return_value = 'cus_123'
            mock_create_pi.side_effect = stripe.error.StripeError('API Error')

            response = create_payment_intent_view(request)

            assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
            assert 'Failed to create payment' in response.data['error']

    def test_create_payment_intent_no_tenant(self):
        """Test POST create payment intent fails without tenant context."""
        from smoothschedule.communication.credits.views import create_payment_intent_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/payment-intent/', {}, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = None

        response = create_payment_intent_view(request)

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


class TestConfirmPaymentView:
    """Tests for confirm_payment_view endpoint."""

    def test_confirm_payment_success(self):
        """Test POST confirm payment adds credits after successful payment."""
        from smoothschedule.communication.credits.views import confirm_payment_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/confirm-payment/', {
            'payment_intent_id': 'pi_123',
            'save_payment_method': True,
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_credits = Mock(id=42, balance_cents=1000)
        mock_payment_intent = Mock(
            id='pi_123',
            status='succeeded',
            amount=5000,
            payment_method='pm_123',
            metadata={'tenant_id': '1'},
        )

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.views.stripe.PaymentIntent.retrieve') as mock_retrieve, \
             patch('smoothschedule.communication.credits.models.CreditTransaction.objects.filter') as mock_filter, \
             patch('smoothschedule.communication.credits.views.transaction'):

            mock_get.return_value = mock_credits
            mock_retrieve.return_value = mock_payment_intent
            mock_filter.return_value.exists.return_value = False

            response = confirm_payment_view(request)

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

            # Verify credits added
            mock_credits.add_credits.assert_called_once_with(
                amount_cents=5000,
                transaction_type='manual',
                stripe_charge_id='pi_123',
                description='Added $50.00 via Stripe'
            )

            # Verify payment method saved
            assert mock_credits.stripe_payment_method_id == 'pm_123'

    def test_confirm_payment_already_processed(self):
        """Test POST confirm payment handles already processed payment."""
        from smoothschedule.communication.credits.views import confirm_payment_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/confirm-payment/', {
            'payment_intent_id': 'pi_123',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_credits = Mock(id=42, balance_cents=1000)
        mock_payment_intent = Mock(
            id='pi_123',
            status='succeeded',
            amount=5000,
            metadata={'tenant_id': '1'},
        )

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.views.stripe.PaymentIntent.retrieve') as mock_retrieve, \
             patch('smoothschedule.communication.credits.models.CreditTransaction.objects.filter') as mock_filter:

            mock_get.return_value = mock_credits
            mock_retrieve.return_value = mock_payment_intent
            mock_filter.return_value.exists.return_value = True

            response = confirm_payment_view(request)

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

            # Verify credits NOT added again
            mock_credits.add_credits.assert_not_called()

    def test_confirm_payment_wrong_tenant(self):
        """Test POST confirm payment rejects payment for different tenant."""
        from smoothschedule.communication.credits.views import confirm_payment_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/confirm-payment/', {
            'payment_intent_id': 'pi_123',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_payment_intent = Mock(
            id='pi_123',
            status='succeeded',
            metadata={'tenant_id': '999'},  # Different tenant!
        )

        with patch('smoothschedule.communication.credits.views.stripe.PaymentIntent.retrieve') as mock_retrieve:
            mock_retrieve.return_value = mock_payment_intent

            response = confirm_payment_view(request)

            assert response.status_code == status.HTTP_403_FORBIDDEN
            assert 'Invalid payment' in response.data['error']

    def test_confirm_payment_not_succeeded(self):
        """Test POST confirm payment rejects incomplete payment."""
        from smoothschedule.communication.credits.views import confirm_payment_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/confirm-payment/', {
            'payment_intent_id': 'pi_123',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_payment_intent = Mock(
            id='pi_123',
            status='processing',
            metadata={'tenant_id': '1'},
        )

        with patch('smoothschedule.communication.credits.views.stripe.PaymentIntent.retrieve') as mock_retrieve:
            mock_retrieve.return_value = mock_payment_intent

            response = confirm_payment_view(request)

            assert response.status_code == status.HTTP_400_BAD_REQUEST
            assert 'Payment not completed' in response.data['error']

    def test_confirm_payment_requires_payment_intent_id(self):
        """Test POST confirm payment requires payment intent ID."""
        from smoothschedule.communication.credits.views import confirm_payment_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/confirm-payment/', {}, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        response = confirm_payment_view(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'Payment intent ID is required' in response.data['error']

    def test_confirm_payment_handles_stripe_error(self):
        """Test POST confirm payment handles Stripe errors."""
        from smoothschedule.communication.credits.views import confirm_payment_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/confirm-payment/', {
            'payment_intent_id': 'pi_123',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        with patch('smoothschedule.communication.credits.views.stripe.PaymentIntent.retrieve') as mock_retrieve:
            mock_retrieve.side_effect = stripe.error.StripeError('API Error')

            response = confirm_payment_view(request)

            assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
            assert 'Failed to confirm payment' in response.data['error']

    def test_confirm_payment_no_tenant(self):
        """Test POST confirm payment fails without tenant context."""
        from smoothschedule.communication.credits.views import confirm_payment_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/confirm-payment/', {}, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = None

        response = confirm_payment_view(request)

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


class TestSetupPaymentMethodView:
    """Tests for setup_payment_method_view endpoint."""

    def test_setup_payment_method_success(self):
        """Test POST setup payment method returns client secret."""
        from smoothschedule.communication.credits.views import setup_payment_method_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/setup-payment-method/')
        request.user = Mock(is_authenticated=True, email='test@example.com')
        request.tenant = Mock(id=1, name='Test Business')

        mock_credits = Mock(id=42, stripe_customer_id='')
        mock_setup_intent = Mock(client_secret='seti_123_secret_456')

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.views._get_or_create_stripe_customer') as mock_get_customer, \
             patch('smoothschedule.communication.credits.views.stripe.SetupIntent.create') as mock_create_si:

            mock_get.return_value = mock_credits
            mock_get_customer.return_value = 'cus_123'
            mock_create_si.return_value = mock_setup_intent

            response = setup_payment_method_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert response.data['client_secret'] == 'seti_123_secret_456'

            # Verify setup intent created
            mock_create_si.assert_called_once()
            call_kwargs = mock_create_si.call_args[1]
            assert call_kwargs['customer'] == 'cus_123'

    def test_setup_payment_method_handles_stripe_error(self):
        """Test POST setup payment method handles Stripe errors."""
        from smoothschedule.communication.credits.views import setup_payment_method_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/setup-payment-method/')
        request.user = Mock(is_authenticated=True, email='test@example.com')
        request.tenant = Mock(id=1, name='Test Business')

        mock_credits = Mock(id=42, stripe_customer_id='cus_123')

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.views._get_or_create_stripe_customer') as mock_get_customer, \
             patch('smoothschedule.communication.credits.views.stripe.SetupIntent.create') as mock_create_si:

            mock_get.return_value = mock_credits
            mock_get_customer.return_value = 'cus_123'
            mock_create_si.side_effect = stripe.error.StripeError('API Error')

            response = setup_payment_method_view(request)

            assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
            assert 'Failed to set up payment method' in response.data['error']

    def test_setup_payment_method_no_tenant(self):
        """Test POST setup payment method fails without tenant context."""
        from smoothschedule.communication.credits.views import setup_payment_method_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/setup-payment-method/')
        request.user = Mock(is_authenticated=True)
        request.tenant = None

        response = setup_payment_method_view(request)

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


class TestSavePaymentMethodView:
    """Tests for save_payment_method_view endpoint."""

    def test_save_payment_method_success(self):
        """Test POST save payment method updates credits."""
        from smoothschedule.communication.credits.views import save_payment_method_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/save-payment-method/', {
            'payment_method_id': 'pm_123',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_credits = Mock(id=42, stripe_payment_method_id='')

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get:
            mock_get.return_value = mock_credits

            response = save_payment_method_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert response.data['success'] is True
            assert response.data['payment_method_id'] == 'pm_123'
            assert mock_credits.stripe_payment_method_id == 'pm_123'
            mock_credits.save.assert_called_once()

    def test_save_payment_method_requires_id(self):
        """Test POST save payment method requires payment method ID."""
        from smoothschedule.communication.credits.views import save_payment_method_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/save-payment-method/', {}, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        response = save_payment_method_view(request)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'Payment method ID is required' in response.data['error']

    def test_save_payment_method_no_tenant(self):
        """Test POST save payment method fails without tenant context."""
        from smoothschedule.communication.credits.views import save_payment_method_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/save-payment-method/', {}, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = None

        response = save_payment_method_view(request)

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


class TestGetTransactionsView:
    """Tests for get_transactions_view endpoint."""

    def test_get_transactions_returns_paginated_results(self):
        """Test GET transactions returns paginated transaction list."""
        from smoothschedule.communication.credits.views import get_transactions_view

        factory = APIRequestFactory()
        request = factory.get('/api/credits/transactions/')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_credits = Mock(id=42)

        # Create mock transactions
        mock_tx1 = Mock(
            id=1,
            amount_cents=5000,
            balance_after_cents=5000,
            transaction_type='manual',
            description='Top-up',
            reference_type='',
            reference_id='',
            stripe_charge_id='ch_123',
            created_at=timezone.now(),
        )
        mock_tx2 = Mock(
            id=2,
            amount_cents=-100,
            balance_after_cents=4900,
            transaction_type='usage',
            description='SMS sent',
            reference_type='sms',
            reference_id='SM123',
            stripe_charge_id='',
            created_at=timezone.now(),
        )

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.models.CreditTransaction.objects.filter') as mock_filter:

            mock_get.return_value = mock_credits

            # Create a more complete queryset mock that supports pagination
            mock_queryset = Mock()
            tx_list = [mock_tx1, mock_tx2]
            mock_queryset.__iter__ = Mock(return_value=iter(tx_list))
            mock_queryset.count.return_value = 2
            mock_queryset.__len__ = Mock(return_value=2)
            mock_queryset.__getitem__ = Mock(side_effect=lambda s: tx_list[s] if isinstance(s, int) else tx_list)
            mock_filter.return_value = mock_queryset

            response = get_transactions_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert len(response.data['results']) == 2
            assert response.data['results'][0]['id'] == 1
            assert response.data['results'][0]['amount_cents'] == 5000
            assert response.data['results'][1]['id'] == 2
            assert response.data['results'][1]['amount_cents'] == -100

    def test_get_transactions_no_tenant(self):
        """Test GET transactions fails without tenant context."""
        from smoothschedule.communication.credits.views import get_transactions_view

        factory = APIRequestFactory()
        request = factory.get('/api/credits/transactions/')
        request.user = Mock(is_authenticated=True)
        request.tenant = None

        response = get_transactions_view(request)

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


class TestGetUsageStatsView:
    """Tests for get_usage_stats_view endpoint."""

    def test_get_usage_stats_returns_stats(self):
        """Test GET usage stats returns current month statistics."""
        from smoothschedule.communication.credits.views import get_usage_stats_view

        factory = APIRequestFactory()
        request = factory.get('/api/credits/usage-stats/')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_credits = Mock(id=42)

        # Mock SMS transactions
        mock_sms_tx1 = Mock(amount_cents=-10)
        mock_sms_tx2 = Mock(amount_cents=-15)

        # Mock voice transactions
        mock_voice_tx1 = Mock(amount_cents=-50)
        mock_voice_tx2 = Mock(amount_cents=-80)

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.models.CreditTransaction.objects.filter') as mock_filter, \
             patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.filter') as mock_phone_filter:

            mock_get.return_value = mock_credits

            # Setup transaction filter chain
            mock_month_txs = Mock()
            mock_sms_queryset = Mock()
            mock_sms_queryset.count.return_value = 2
            mock_sms_queryset.__iter__ = Mock(return_value=iter([]))

            mock_voice_queryset = Mock()
            mock_voice_queryset.__iter__ = Mock(return_value=iter([mock_voice_tx1, mock_voice_tx2]))
            mock_voice_queryset.count.return_value = 2

            mock_month_txs.filter = Mock(side_effect=[mock_sms_queryset, mock_voice_queryset])
            mock_month_txs.__iter__ = Mock(return_value=iter([mock_sms_tx1, mock_sms_tx2, mock_voice_tx1, mock_voice_tx2]))

            mock_filter.return_value = mock_month_txs

            # Setup phone number filter
            mock_phone_queryset = Mock()
            mock_phone_queryset.count.return_value = 3
            mock_phone_filter.return_value = mock_phone_queryset

            response = get_usage_stats_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert response.data['sms_sent_this_month'] == 2
            assert response.data['proxy_numbers_active'] == 3
            assert 'voice_minutes_this_month' in response.data
            assert 'estimated_cost_cents' in response.data

    def test_get_usage_stats_no_tenant(self):
        """Test GET usage stats fails without tenant context."""
        from smoothschedule.communication.credits.views import get_usage_stats_view

        factory = APIRequestFactory()
        request = factory.get('/api/credits/usage-stats/')
        request.user = Mock(is_authenticated=True)
        request.tenant = None

        response = get_usage_stats_view(request)

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


class TestGetOrCreateStripeCustomer:
    """Tests for _get_or_create_stripe_customer helper."""

    def test_get_existing_stripe_customer(self):
        """Test helper returns existing Stripe customer ID."""
        from smoothschedule.communication.credits.views import _get_or_create_stripe_customer

        mock_credits = Mock(stripe_customer_id='cus_existing')
        mock_tenant = Mock(id=1, name='Test Business')
        mock_user = Mock(email='test@example.com')

        result = _get_or_create_stripe_customer(mock_credits, mock_tenant, mock_user)

        assert result == 'cus_existing'
        mock_credits.save.assert_not_called()

    def test_create_new_stripe_customer(self):
        """Test helper creates new Stripe customer when none exists."""
        from smoothschedule.communication.credits.views import _get_or_create_stripe_customer

        mock_credits = Mock(stripe_customer_id='')
        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_tenant.name = 'Test Business'
        mock_user = Mock()
        mock_user.email = 'test@example.com'

        mock_customer = Mock(id='cus_new123')

        with patch('smoothschedule.communication.credits.views.stripe.Customer.create') as mock_create:
            mock_create.return_value = mock_customer

            result = _get_or_create_stripe_customer(mock_credits, mock_tenant, mock_user)

            assert result == 'cus_new123'
            assert mock_credits.stripe_customer_id == 'cus_new123'
            mock_credits.save.assert_called_once()

            # Verify customer created with correct data
            mock_create.assert_called_once()
            call_kwargs = mock_create.call_args[1]
            assert call_kwargs['email'] == 'test@example.com'
            assert call_kwargs['name'] == 'Test Business'
            assert call_kwargs['metadata']['tenant_id'] == '1'


class TestGetTwilioClient:
    """Tests for _get_twilio_client helper."""

    def test_get_twilio_client_with_credentials(self):
        """Test helper returns Twilio client when credentials configured."""
        from smoothschedule.communication.credits.views import _get_twilio_client

        with patch('smoothschedule.communication.credits.views.settings') as mock_settings, \
             patch('smoothschedule.communication.credits.views.TwilioClient') as mock_client_class:

            mock_settings.TWILIO_ACCOUNT_SID = 'AC123'
            mock_settings.TWILIO_AUTH_TOKEN = 'token123'

            result = _get_twilio_client()

            mock_client_class.assert_called_once_with('AC123', 'token123')

    def test_get_twilio_client_missing_credentials(self):
        """Test helper raises error when credentials missing."""
        from smoothschedule.communication.credits.views import _get_twilio_client

        with patch('smoothschedule.communication.credits.views.settings') as mock_settings:
            mock_settings.TWILIO_ACCOUNT_SID = None
            mock_settings.TWILIO_AUTH_TOKEN = None

            with pytest.raises(ValueError, match='Twilio credentials not configured'):
                _get_twilio_client()


class TestSearchAvailableNumbersView:
    """Tests for search_available_numbers_view endpoint."""

    def test_search_available_numbers_success(self):
        """Test GET search available numbers returns Twilio results."""
        from smoothschedule.communication.credits.views import search_available_numbers_view

        factory = APIRequestFactory()
        request = factory.get('/api/credits/phone-numbers/search/?area_code=415&limit=10')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)
        request.tenant.has_feature.return_value = True

        # Mock Twilio numbers
        mock_number = Mock(
            phone_number='+14155551234',
            friendly_name='(415) 555-1234',
            locality='San Francisco',
            region='CA',
            postal_code='94102',
            capabilities={'voice': True, 'SMS': True, 'MMS': False},
        )

        mock_client = Mock()
        mock_client.available_phone_numbers.return_value.local.list.return_value = [mock_number]

        with patch('smoothschedule.communication.credits.views._get_twilio_client') as mock_get_client:
            mock_get_client.return_value = mock_client

            response = search_available_numbers_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert response.data['count'] == 1
            assert len(response.data['numbers']) == 1
            assert response.data['numbers'][0]['phone_number'] == '+14155551234'
            assert response.data['numbers'][0]['locality'] == 'San Francisco'
            assert response.data['numbers'][0]['capabilities']['voice'] is True

    def test_search_available_numbers_without_feature(self):
        """Test GET search available numbers fails without feature permission."""
        from smoothschedule.communication.credits.views import search_available_numbers_view

        factory = APIRequestFactory()
        request = factory.get('/api/credits/phone-numbers/search/')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)
        request.tenant.has_feature.return_value = False

        response = search_available_numbers_view(request)

        assert response.status_code == status.HTTP_403_FORBIDDEN
        assert 'Masked calling feature not available' in response.data['error']

    def test_search_available_numbers_test_credentials_error(self):
        """Test GET search available numbers handles test credentials error."""
        from smoothschedule.communication.credits.views import search_available_numbers_view

        factory = APIRequestFactory()
        request = factory.get('/api/credits/phone-numbers/search/')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)
        request.tenant.has_feature.return_value = True

        mock_client = Mock()
        error = TwilioRestException(
            status=400,
            uri='/uri',
            msg='Test Account Error 20008',
            code=20008,
        )
        mock_client.available_phone_numbers.return_value.local.list.side_effect = error

        with patch('smoothschedule.communication.credits.views._get_twilio_client') as mock_get_client:
            mock_get_client.return_value = mock_client

            response = search_available_numbers_view(request)

            assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
            assert 'live Twilio credentials' in response.data['error']

    def test_search_available_numbers_twilio_error(self):
        """Test GET search available numbers handles Twilio errors."""
        from smoothschedule.communication.credits.views import search_available_numbers_view

        factory = APIRequestFactory()
        request = factory.get('/api/credits/phone-numbers/search/')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)
        request.tenant.has_feature.return_value = True

        mock_client = Mock()
        error = TwilioRestException(
            status=500,
            uri='/uri',
            msg='Server Error',
            code=500,
        )
        mock_client.available_phone_numbers.return_value.local.list.side_effect = error

        with patch('smoothschedule.communication.credits.views._get_twilio_client') as mock_get_client:
            mock_get_client.return_value = mock_client

            response = search_available_numbers_view(request)

            assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
            assert 'Failed to search phone numbers' in response.data['error']

    def test_search_available_numbers_missing_credentials(self):
        """Test GET search available numbers handles missing Twilio credentials."""
        from smoothschedule.communication.credits.views import search_available_numbers_view

        factory = APIRequestFactory()
        request = factory.get('/api/credits/phone-numbers/search/')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)
        request.tenant.has_feature.return_value = True

        with patch('smoothschedule.communication.credits.views._get_twilio_client') as mock_get_client:
            mock_get_client.side_effect = ValueError('Twilio credentials not configured')

            response = search_available_numbers_view(request)

            assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
            assert 'Twilio credentials not configured' in response.data['error']

    def test_search_available_numbers_no_tenant(self):
        """Test GET search available numbers fails without tenant context."""
        from smoothschedule.communication.credits.views import search_available_numbers_view

        factory = APIRequestFactory()
        request = factory.get('/api/credits/phone-numbers/search/')
        request.user = Mock(is_authenticated=True)
        request.tenant = None

        response = search_available_numbers_view(request)

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


class TestPurchasePhoneNumberView:
    """Tests for purchase_phone_number_view endpoint."""

    def test_purchase_phone_number_success(self):
        """Test POST purchase phone number creates proxy number and charges fee."""
        from smoothschedule.communication.credits.views import purchase_phone_number_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/phone-numbers/purchase/', {
            'phone_number': '+14155551234',
            'friendly_name': 'My Business Line',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1, name='Test Business')
        request.tenant.has_feature.return_value = True

        mock_credits = Mock(id=42, balance_cents=5000)

        # Mock Twilio purchased number
        mock_purchased = Mock(
            sid='PN123',
            capabilities={'voice': True, 'sms': True, 'mms': False},
        )

        mock_proxy_number = Mock(
            id=1,
            phone_number='+14155551234',
            friendly_name='My Business Line',
            status='assigned',
            monthly_fee_cents=200,
            assigned_at=timezone.now(),
        )

        mock_client = Mock()
        mock_client.incoming_phone_numbers.create.return_value = mock_purchased

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.views._get_twilio_client') as mock_get_client, \
             patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.filter') as mock_filter, \
             patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.create') as mock_create, \
             patch('smoothschedule.communication.credits.views.transaction'):

            mock_get.return_value = mock_credits
            mock_get_client.return_value = mock_client
            mock_filter.return_value.first.return_value = None
            mock_create.return_value = mock_proxy_number

            response = purchase_phone_number_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert response.data['success'] is True
            assert response.data['phone_number']['phone_number'] == '+14155551234'

            # Verify purchase fee charged
            mock_credits.deduct.assert_called_once_with(
                amount_cents=200,
                description='Phone number purchase: +14155551234',
                reference_type='phone_purchase',
                reference_id='PN123',
            )

    def test_purchase_phone_number_insufficient_credits(self):
        """Test POST purchase phone number fails with insufficient credits."""
        from smoothschedule.communication.credits.views import purchase_phone_number_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/phone-numbers/purchase/', {
            'phone_number': '+14155551234',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1, name='Test Business')
        request.tenant.has_feature.return_value = True

        mock_credits = Mock(id=42, balance_cents=100)  # Less than $2 fee

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.filter') as mock_filter:

            mock_get.return_value = mock_credits
            mock_filter.return_value.first.return_value = None

            response = purchase_phone_number_view(request)

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

    def test_purchase_phone_number_already_owned(self):
        """Test POST purchase phone number fails if already owned by tenant."""
        from smoothschedule.communication.credits.views import purchase_phone_number_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/phone-numbers/purchase/', {
            'phone_number': '+14155551234',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)
        request.tenant.has_feature.return_value = True

        mock_existing = Mock(assigned_tenant=request.tenant)

        with patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.filter') as mock_filter:
            mock_filter.return_value.first.return_value = mock_existing

            response = purchase_phone_number_view(request)

            assert response.status_code == status.HTTP_400_BAD_REQUEST
            assert 'You already own this number' in response.data['error']

    def test_purchase_phone_number_already_taken(self):
        """Test POST purchase phone number fails if owned by different tenant."""
        from smoothschedule.communication.credits.views import purchase_phone_number_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/phone-numbers/purchase/', {
            'phone_number': '+14155551234',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)
        request.tenant.has_feature.return_value = True

        mock_existing = Mock(assigned_tenant=Mock(id=999))

        with patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.filter') as mock_filter:
            mock_filter.return_value.first.return_value = mock_existing

            response = purchase_phone_number_view(request)

            assert response.status_code == status.HTTP_400_BAD_REQUEST
            assert 'This number is not available' in response.data['error']

    def test_purchase_phone_number_requires_phone_number(self):
        """Test POST purchase phone number requires phone number."""
        from smoothschedule.communication.credits.views import purchase_phone_number_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/phone-numbers/purchase/', {}, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)
        request.tenant.has_feature.return_value = True

        response = purchase_phone_number_view(request)

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

    def test_purchase_phone_number_without_feature(self):
        """Test POST purchase phone number fails without feature permission."""
        from smoothschedule.communication.credits.views import purchase_phone_number_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/phone-numbers/purchase/', {}, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)
        request.tenant.has_feature.return_value = False

        response = purchase_phone_number_view(request)

        assert response.status_code == status.HTTP_403_FORBIDDEN
        assert 'Masked calling feature not available' in response.data['error']

    def test_purchase_phone_number_twilio_error(self):
        """Test POST purchase phone number handles Twilio errors."""
        from smoothschedule.communication.credits.views import purchase_phone_number_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/phone-numbers/purchase/', {
            'phone_number': '+14155551234',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1, name='Test Business')
        request.tenant.has_feature.return_value = True

        mock_credits = Mock(id=42, balance_cents=5000)

        mock_client = Mock()
        error = TwilioRestException(
            status=400,
            uri='/uri',
            msg='Number unavailable',
            code=400,
        )
        mock_client.incoming_phone_numbers.create.side_effect = error

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get, \
             patch('smoothschedule.communication.credits.views._get_twilio_client') as mock_get_client, \
             patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.filter') as mock_filter, \
             patch('smoothschedule.communication.credits.views.transaction'):

            mock_get.return_value = mock_credits
            mock_get_client.return_value = mock_client
            mock_filter.return_value.first.return_value = None

            response = purchase_phone_number_view(request)

            assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
            assert 'Failed to purchase number' in response.data['error']

    def test_purchase_phone_number_no_tenant(self):
        """Test POST purchase phone number fails without tenant context."""
        from smoothschedule.communication.credits.views import purchase_phone_number_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/phone-numbers/purchase/', {}, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = None

        response = purchase_phone_number_view(request)

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


class TestListPhoneNumbersView:
    """Tests for list_phone_numbers_view endpoint."""

    def test_list_phone_numbers_success(self):
        """Test GET list phone numbers returns tenant's numbers."""
        from smoothschedule.communication.credits.views import list_phone_numbers_view

        factory = APIRequestFactory()
        request = factory.get('/api/credits/phone-numbers/')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)
        request.tenant.has_feature.return_value = True

        mock_number = Mock(
            id=1,
            phone_number='+14155551234',
            friendly_name='My Line',
            status='assigned',
            monthly_fee_cents=200,
            capabilities={'voice': True, 'sms': True},
            assigned_at=timezone.now(),
            last_billed_at=timezone.now(),
        )

        with patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.filter') as mock_filter:
            mock_queryset = Mock()
            mock_queryset.order_by.return_value = [mock_number]
            mock_filter.return_value = mock_queryset

            response = list_phone_numbers_view(request)

            assert response.status_code == status.HTTP_200_OK
            assert response.data['count'] == 1
            assert len(response.data['numbers']) == 1
            assert response.data['numbers'][0]['phone_number'] == '+14155551234'

    def test_list_phone_numbers_without_feature(self):
        """Test GET list phone numbers fails without feature permission."""
        from smoothschedule.communication.credits.views import list_phone_numbers_view

        factory = APIRequestFactory()
        request = factory.get('/api/credits/phone-numbers/')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)
        request.tenant.has_feature.return_value = False

        response = list_phone_numbers_view(request)

        assert response.status_code == status.HTTP_403_FORBIDDEN
        assert 'Masked calling feature not available' in response.data['error']

    def test_list_phone_numbers_no_tenant(self):
        """Test GET list phone numbers fails without tenant context."""
        from smoothschedule.communication.credits.views import list_phone_numbers_view

        factory = APIRequestFactory()
        request = factory.get('/api/credits/phone-numbers/')
        request.user = Mock(is_authenticated=True)
        request.tenant = None

        response = list_phone_numbers_view(request)

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


class TestReleasePhoneNumberView:
    """Tests for release_phone_number_view endpoint."""

    def test_release_phone_number_success(self):
        """Test DELETE release phone number marks inactive and deletes from Twilio."""
        from smoothschedule.communication.credits.views import release_phone_number_view

        factory = APIRequestFactory()
        request = factory.delete('/api/credits/phone-numbers/1/')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_proxy_number = Mock(
            id=1,
            phone_number='+14155551234',
            twilio_sid='PN123',
        )

        mock_client = Mock()

        with patch('smoothschedule.communication.credits.views._get_twilio_client') as mock_get_client, \
             patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.get') as mock_get, \
             patch('smoothschedule.communication.credits.views.transaction'):

            mock_get.return_value = mock_proxy_number
            mock_get_client.return_value = mock_client

            response = release_phone_number_view(request, number_id=1)

            assert response.status_code == status.HTTP_200_OK
            assert response.data['success'] is True
            assert 'released' in response.data['message']

            # Verify Twilio deletion
            mock_client.incoming_phone_numbers.assert_called_once_with('PN123')
            mock_client.incoming_phone_numbers.return_value.delete.assert_called_once()

            # Verify status updated
            mock_proxy_number.save.assert_called_once()

    def test_release_phone_number_not_found(self):
        """Test DELETE release phone number fails if not owned by tenant."""
        from smoothschedule.communication.credits.views import release_phone_number_view
        from smoothschedule.communication.credits.models import ProxyPhoneNumber

        factory = APIRequestFactory()
        request = factory.delete('/api/credits/phone-numbers/1/')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        with patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.get') as mock_get:
            mock_get.side_effect = ProxyPhoneNumber.DoesNotExist

            response = release_phone_number_view(request, number_id=1)

            assert response.status_code == status.HTTP_404_NOT_FOUND
            assert 'Phone number not found' in response.data['error']

    def test_release_phone_number_twilio_not_found(self):
        """Test DELETE release phone number continues if already deleted from Twilio."""
        from smoothschedule.communication.credits.views import release_phone_number_view

        factory = APIRequestFactory()
        request = factory.delete('/api/credits/phone-numbers/1/')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_proxy_number = Mock(id=1, phone_number='+14155551234', twilio_sid='PN123')

        mock_client = Mock()
        error = TwilioRestException(status=404, uri='/uri', msg='Not found', code=20404)
        error.code = 20404
        mock_client.incoming_phone_numbers.return_value.delete.side_effect = error

        with patch('smoothschedule.communication.credits.views._get_twilio_client') as mock_get_client, \
             patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.get') as mock_get, \
             patch('smoothschedule.communication.credits.views.transaction'):

            mock_get.return_value = mock_proxy_number
            mock_get_client.return_value = mock_client

            response = release_phone_number_view(request, number_id=1)

            # Should succeed despite Twilio 404
            assert response.status_code == status.HTTP_200_OK
            assert response.data['success'] is True

    def test_release_phone_number_twilio_error(self):
        """Test DELETE release phone number handles Twilio errors."""
        from smoothschedule.communication.credits.views import release_phone_number_view

        factory = APIRequestFactory()
        request = factory.delete('/api/credits/phone-numbers/1/')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_proxy_number = Mock(id=1, twilio_sid='PN123')

        mock_client = Mock()
        error = TwilioRestException(status=500, uri='/uri', msg='Server error', code=500)
        error.code = 500
        mock_client.incoming_phone_numbers.return_value.delete.side_effect = error

        with patch('smoothschedule.communication.credits.views._get_twilio_client') as mock_get_client, \
             patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.get') as mock_get, \
             patch('smoothschedule.communication.credits.views.transaction'):

            mock_get.return_value = mock_proxy_number
            mock_get_client.return_value = mock_client

            response = release_phone_number_view(request, number_id=1)

            assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
            assert 'Failed to release number' in response.data['error']

    def test_release_phone_number_no_tenant(self):
        """Test DELETE release phone number fails without tenant context."""
        from smoothschedule.communication.credits.views import release_phone_number_view

        factory = APIRequestFactory()
        request = factory.delete('/api/credits/phone-numbers/1/')
        request.user = Mock(is_authenticated=True)
        request.tenant = None

        response = release_phone_number_view(request, number_id=1)

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


class TestChangePhoneNumberView:
    """Tests for change_phone_number_view endpoint."""

    def test_change_phone_number_success(self):
        """Test POST change phone number purchases new and releases old."""
        from smoothschedule.communication.credits.views import change_phone_number_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/phone-numbers/1/change/', {
            'new_phone_number': '+14155559999',
            'friendly_name': 'Updated Line',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1, name='Test Business')

        mock_credits = Mock(id=42, balance_cents=5000)

        mock_old_number = Mock(
            id=1,
            phone_number='+14155551234',
            twilio_sid='PN_old',
            friendly_name='Old Line',
        )

        mock_purchased = Mock(
            sid='PN_new',
            capabilities={'voice': True, 'sms': True, 'mms': False},
        )

        mock_client = Mock()
        mock_client.incoming_phone_numbers.create.return_value = mock_purchased

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get_credits, \
             patch('smoothschedule.communication.credits.views._get_twilio_client') as mock_get_client, \
             patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.get') as mock_get_number, \
             patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.filter') as mock_filter, \
             patch('smoothschedule.communication.credits.views.transaction'):

            mock_get_credits.return_value = mock_credits
            mock_get_client.return_value = mock_client
            mock_get_number.return_value = mock_old_number
            mock_filter.return_value.first.return_value = None

            response = change_phone_number_view(request, number_id=1)

            assert response.status_code == status.HTTP_200_OK
            assert response.data['success'] is True
            assert response.data['phone_number']['phone_number'] == '+14155559999'

            # Verify old number updated with new details
            assert mock_old_number.phone_number == '+14155559999'
            assert mock_old_number.twilio_sid == 'PN_new'
            assert mock_old_number.friendly_name == 'Updated Line'

            # Verify change fee charged
            mock_credits.deduct.assert_called_once_with(
                amount_cents=200,
                description='Phone number change to +14155559999',
                reference_type='phone_change',
                reference_id='PN_new',
            )

    def test_change_phone_number_insufficient_credits(self):
        """Test POST change phone number fails with insufficient credits."""
        from smoothschedule.communication.credits.views import change_phone_number_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/phone-numbers/1/change/', {
            'new_phone_number': '+14155559999',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_credits = Mock(id=42, balance_cents=100)
        mock_old_number = Mock(id=1)

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get_credits, \
             patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.get') as mock_get_number, \
             patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.filter') as mock_filter:

            mock_get_credits.return_value = mock_credits
            mock_get_number.return_value = mock_old_number
            mock_filter.return_value.first.return_value = None

            response = change_phone_number_view(request, number_id=1)

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

    def test_change_phone_number_not_found(self):
        """Test POST change phone number fails if number not owned by tenant."""
        from smoothschedule.communication.credits.views import change_phone_number_view
        from smoothschedule.communication.credits.models import ProxyPhoneNumber

        factory = APIRequestFactory()
        request = factory.post('/api/credits/phone-numbers/1/change/', {}, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        with patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.get') as mock_get:
            mock_get.side_effect = ProxyPhoneNumber.DoesNotExist

            response = change_phone_number_view(request, number_id=1)

            assert response.status_code == status.HTTP_404_NOT_FOUND
            assert 'Phone number not found' in response.data['error']

    def test_change_phone_number_requires_new_number(self):
        """Test POST change phone number requires new phone number."""
        from smoothschedule.communication.credits.views import change_phone_number_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/phone-numbers/1/change/', {}, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_old_number = Mock(id=1)

        with patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.get') as mock_get:
            mock_get.return_value = mock_old_number

            response = change_phone_number_view(request, number_id=1)

            assert response.status_code == status.HTTP_400_BAD_REQUEST
            assert 'New phone number is required' in response.data['error']

    def test_change_phone_number_new_number_unavailable(self):
        """Test POST change phone number fails if new number not available."""
        from smoothschedule.communication.credits.views import change_phone_number_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/phone-numbers/1/change/', {
            'new_phone_number': '+14155559999',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1)

        mock_old_number = Mock(id=1)
        mock_existing = Mock()

        with patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.get') as mock_get, \
             patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.filter') as mock_filter:

            mock_get.return_value = mock_old_number
            mock_filter.return_value.first.return_value = mock_existing

            response = change_phone_number_view(request, number_id=1)

            assert response.status_code == status.HTTP_400_BAD_REQUEST
            assert 'This number is not available' in response.data['error']

    def test_change_phone_number_twilio_error(self):
        """Test POST change phone number handles Twilio errors."""
        from smoothschedule.communication.credits.views import change_phone_number_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/phone-numbers/1/change/', {
            'new_phone_number': '+14155559999',
        }, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = Mock(id=1, name='Test Business')

        mock_credits = Mock(id=42, balance_cents=5000)
        mock_old_number = Mock(id=1, friendly_name='Old')

        mock_client = Mock()
        error = TwilioRestException(status=400, uri='/uri', msg='Error', code=400)
        mock_client.incoming_phone_numbers.create.side_effect = error

        with patch('smoothschedule.communication.credits.views.get_or_create_credits') as mock_get_credits, \
             patch('smoothschedule.communication.credits.views._get_twilio_client') as mock_get_client, \
             patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.get') as mock_get_number, \
             patch('smoothschedule.communication.credits.models.ProxyPhoneNumber.objects.filter') as mock_filter, \
             patch('smoothschedule.communication.credits.views.transaction'):

            mock_get_credits.return_value = mock_credits
            mock_get_client.return_value = mock_client
            mock_get_number.return_value = mock_old_number
            mock_filter.return_value.first.return_value = None

            response = change_phone_number_view(request, number_id=1)

            assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
            assert 'Failed to change number' in response.data['error']

    def test_change_phone_number_no_tenant(self):
        """Test POST change phone number fails without tenant context."""
        from smoothschedule.communication.credits.views import change_phone_number_view

        factory = APIRequestFactory()
        request = factory.post('/api/credits/phone-numbers/1/change/', {}, format='json')
        request.user = Mock(is_authenticated=True)
        request.tenant = None

        response = change_phone_number_view(request, number_id=1)

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


class TestTransactionPagination:
    """Tests for TransactionPagination class."""

    def test_pagination_default_page_size(self):
        """Test pagination uses default page size of 20."""
        from smoothschedule.communication.credits.views import TransactionPagination

        pagination = TransactionPagination()
        assert pagination.page_size == 20

    def test_pagination_max_page_size(self):
        """Test pagination enforces max page size of 100."""
        from smoothschedule.communication.credits.views import TransactionPagination

        pagination = TransactionPagination()
        assert pagination.max_page_size == 100

    def test_pagination_allows_custom_limit(self):
        """Test pagination allows custom limit via query param."""
        from smoothschedule.communication.credits.views import TransactionPagination

        pagination = TransactionPagination()
        assert pagination.page_size_query_param == 'limit'
