"""
Unit tests for Communication Credits models.

These tests use mocks to avoid database dependencies and run quickly.
They test model methods, properties, and business logic.
"""
from unittest.mock import Mock, patch, MagicMock, call
from datetime import datetime, timedelta, timezone as dt_timezone
from django.utils import timezone
import pytest


class TestCommunicationCreditsModel:
    """Tests for CommunicationCredits model."""

    def test_str_representation(self):
        """Test string representation shows tenant and balance."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        mock_tenant = Mock()
        mock_tenant.name = "Test Business"

        # Create instance and set attributes directly
        credits = Mock(spec=CommunicationCredits)
        credits.tenant = mock_tenant
        credits.balance_cents = 1500

        # Call the real __str__ method
        result = CommunicationCredits.__str__(credits)

        assert result == "Test Business - $15.00"

    def test_balance_property_converts_cents_to_dollars(self):
        """Test balance property returns dollars."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock(spec=CommunicationCredits)
        credits.balance_cents = 2500

        # Call the real property getter
        result = CommunicationCredits.balance.fget(credits)

        assert result == 25.0

    def test_balance_property_handles_zero(self):
        """Test balance property handles zero balance."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock(spec=CommunicationCredits)
        credits.balance_cents = 0

        result = CommunicationCredits.balance.fget(credits)

        assert result == 0.0

    def test_auto_reload_threshold_property(self):
        """Test auto reload threshold converts cents to dollars."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock(spec=CommunicationCredits)
        credits.auto_reload_threshold_cents = 1000

        result = CommunicationCredits.auto_reload_threshold.fget(credits)

        assert result == 10.0

    def test_auto_reload_amount_property(self):
        """Test auto reload amount converts cents to dollars."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock(spec=CommunicationCredits)
        credits.auto_reload_amount_cents = 2500

        result = CommunicationCredits.auto_reload_amount.fget(credits)

        assert result == 25.0

    def test_deduct_success_returns_transaction(self):
        """Test deduct with sufficient balance returns transaction."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock(spec=CommunicationCredits)
        credits.balance_cents = 5000
        credits.total_spent_cents = 0
        credits.save = Mock()
        credits._check_thresholds = Mock()

        with patch('smoothschedule.communication.credits.models.CreditTransaction') as mock_tx:
            mock_transaction = Mock(id=1)
            mock_tx.objects.create.return_value = mock_transaction

            # Call the real deduct method
            result = CommunicationCredits.deduct(
                credits,
                amount_cents=1000,
                description="Test charge",
                reference_type="sms",
                reference_id="SM123"
            )

            # Verify balance updated
            assert credits.balance_cents == 4000
            assert credits.total_spent_cents == 1000

            # Verify save called
            credits.save.assert_called_once_with(
                update_fields=['balance_cents', 'total_spent_cents', 'updated_at']
            )

            # Verify transaction created
            mock_tx.objects.create.assert_called_once_with(
                credits=credits,
                amount_cents=-1000,
                balance_after_cents=4000,
                transaction_type='usage',
                description="Test charge",
                reference_type="sms",
                reference_id="SM123"
            )

            # Verify thresholds checked
            credits._check_thresholds.assert_called_once()

            assert result == mock_transaction

    def test_deduct_insufficient_balance_returns_none(self):
        """Test deduct with insufficient balance returns None."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock(spec=CommunicationCredits)
        credits.balance_cents = 500
        credits.save = Mock()

        result = CommunicationCredits.deduct(
            credits,
            amount_cents=1000,
            description="Test charge"
        )

        # Verify no changes made
        assert credits.balance_cents == 500
        credits.save.assert_not_called()
        assert result is None

    def test_deduct_handles_none_references(self):
        """Test deduct handles None reference_type and reference_id."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock(spec=CommunicationCredits)
        credits.balance_cents = 5000
        credits.total_spent_cents = 0
        credits.save = Mock()
        credits._check_thresholds = Mock()

        with patch('smoothschedule.communication.credits.models.CreditTransaction') as mock_tx:
            mock_tx.objects.create.return_value = Mock(id=1)

            CommunicationCredits.deduct(
                credits,
                amount_cents=1000,
                description="Test"
            )

            # Verify empty strings used for None values
            call_kwargs = mock_tx.objects.create.call_args[1]
            assert call_kwargs['reference_type'] == ''
            assert call_kwargs['reference_id'] == ''

    def test_add_credits_updates_balance_and_total(self):
        """Test add_credits updates balance and total loaded."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock(spec=CommunicationCredits)
        credits.balance_cents = 1000
        credits.total_loaded_cents = 5000
        credits.low_balance_warning_sent = True
        credits.save = Mock()

        with patch('smoothschedule.communication.credits.models.CreditTransaction') as mock_tx:
            CommunicationCredits.add_credits(
                credits,
                amount_cents=2500,
                transaction_type='manual',
                stripe_charge_id='ch_123',
                description="Top-up"
            )

            # Verify balance and totals updated
            assert credits.balance_cents == 3500
            assert credits.total_loaded_cents == 7500
            assert credits.low_balance_warning_sent is False

            # Verify save called
            credits.save.assert_called_once()

            # Verify transaction created
            mock_tx.objects.create.assert_called_once_with(
                credits=credits,
                amount_cents=2500,
                balance_after_cents=3500,
                transaction_type='manual',
                description="Top-up",
                stripe_charge_id='ch_123'
            )

    def test_add_credits_uses_default_description(self):
        """Test add_credits uses default description when not provided."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock(spec=CommunicationCredits)
        credits.balance_cents = 1000
        credits.total_loaded_cents = 0
        credits.low_balance_warning_sent = False
        credits.save = Mock()

        with patch('smoothschedule.communication.credits.models.CreditTransaction') as mock_tx:
            CommunicationCredits.add_credits(
                credits,
                amount_cents=2500,
                transaction_type='auto_reload'
            )

            call_kwargs = mock_tx.objects.create.call_args[1]
            assert call_kwargs['description'] == "Credits added (auto_reload)"

    def test_check_thresholds_sends_warning_when_below_threshold(self):
        """Test _check_thresholds sends warning when below threshold."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock(spec=CommunicationCredits)
        credits.balance_cents = 400
        credits.low_balance_warning_cents = 500
        credits.low_balance_warning_sent = False
        credits.auto_reload_enabled = False  # Disable auto-reload for this test
        credits._send_low_balance_warning = Mock()
        credits._trigger_auto_reload = Mock()

        CommunicationCredits._check_thresholds(credits)

        credits._send_low_balance_warning.assert_called_once()

    def test_check_thresholds_skips_warning_if_already_sent(self):
        """Test _check_thresholds skips warning if already sent."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock(spec=CommunicationCredits)
        credits.balance_cents = 400
        credits.low_balance_warning_cents = 500
        credits.low_balance_warning_sent = True
        credits.auto_reload_enabled = False  # Disable auto-reload for this test
        credits._send_low_balance_warning = Mock()

        CommunicationCredits._check_thresholds(credits)

        credits._send_low_balance_warning.assert_not_called()

    def test_check_thresholds_triggers_auto_reload_when_enabled(self):
        """Test _check_thresholds triggers auto reload when conditions met."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock(spec=CommunicationCredits)
        credits.balance_cents = 800
        credits.low_balance_warning_cents = 100
        credits.low_balance_warning_sent = False
        credits.auto_reload_enabled = True
        credits.auto_reload_threshold_cents = 1000
        credits.stripe_payment_method_id = 'pm_123'
        credits._send_low_balance_warning = Mock()
        credits._trigger_auto_reload = Mock()

        CommunicationCredits._check_thresholds(credits)

        credits._trigger_auto_reload.assert_called_once()

    def test_check_thresholds_skips_auto_reload_when_disabled(self):
        """Test _check_thresholds skips auto reload when disabled."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock(spec=CommunicationCredits)
        credits.balance_cents = 800
        credits.low_balance_warning_cents = 100
        credits.auto_reload_enabled = False
        credits.auto_reload_threshold_cents = 1000
        credits.stripe_payment_method_id = 'pm_123'
        credits._trigger_auto_reload = Mock()

        CommunicationCredits._check_thresholds(credits)

        credits._trigger_auto_reload.assert_not_called()

    def test_check_thresholds_skips_auto_reload_without_payment_method(self):
        """Test _check_thresholds skips auto reload without payment method."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock(spec=CommunicationCredits)
        credits.balance_cents = 800
        credits.low_balance_warning_cents = 100
        credits.auto_reload_enabled = True
        credits.auto_reload_threshold_cents = 1000
        credits.stripe_payment_method_id = ''
        credits._trigger_auto_reload = Mock()

        CommunicationCredits._check_thresholds(credits)

        credits._trigger_auto_reload.assert_not_called()

    def test_send_low_balance_warning_triggers_task(self):
        """Test _send_low_balance_warning triggers Celery task."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock(spec=CommunicationCredits)
        credits.id = 42
        credits.save = Mock()

        with patch('smoothschedule.communication.credits.tasks.send_low_balance_warning') as mock_task, \
             patch('smoothschedule.communication.credits.models.timezone.now') as mock_now:

            mock_now.return_value = datetime(2024, 1, 1, 12, 0, 0, tzinfo=dt_timezone.utc)

            CommunicationCredits._send_low_balance_warning(credits)

            # Verify task triggered
            mock_task.delay.assert_called_once_with(42)

            # Verify save called with correct update_fields
            credits.save.assert_called_once_with(
                update_fields=['low_balance_warning_sent', 'low_balance_warning_sent_at']
            )

    def test_trigger_auto_reload_triggers_task(self):
        """Test _trigger_auto_reload triggers Celery task."""
        from smoothschedule.communication.credits.models import CommunicationCredits

        credits = Mock()  # Don't use spec here as we only need the id
        credits.id = 42

        with patch('smoothschedule.communication.credits.tasks.process_auto_reload') as mock_task:
            CommunicationCredits._trigger_auto_reload(credits)
            mock_task.delay.assert_called_once_with(42)


class TestCreditTransactionModel:
    """Tests for CreditTransaction model."""

    def test_str_representation_positive_amount(self):
        """Test string representation for credit (positive amount)."""
        from smoothschedule.communication.credits.models import CreditTransaction

        transaction = Mock(spec=CreditTransaction)
        transaction.amount_cents = 2500
        transaction.description = "Manual top-up"

        result = CreditTransaction.__str__(transaction)

        assert result == "+$25.00 - Manual top-up"

    def test_str_representation_negative_amount(self):
        """Test string representation for debit (negative amount)."""
        from smoothschedule.communication.credits.models import CreditTransaction

        transaction = Mock(spec=CreditTransaction)
        transaction.amount_cents = -1500
        transaction.description = "SMS charge"

        result = CreditTransaction.__str__(transaction)

        # Negative amounts display as "$-15.00" because the sign is part of the number
        assert result == "$-15.00 - SMS charge"

    def test_str_representation_zero_amount(self):
        """Test string representation for zero amount."""
        from smoothschedule.communication.credits.models import CreditTransaction

        transaction = Mock(spec=CreditTransaction)
        transaction.amount_cents = 0
        transaction.description = "Test"

        result = CreditTransaction.__str__(transaction)

        # Zero is neither positive nor negative, so no sign
        assert result == "$0.00 - Test"

    def test_amount_property_converts_cents_to_dollars(self):
        """Test amount property returns dollars."""
        from smoothschedule.communication.credits.models import CreditTransaction

        transaction = Mock(spec=CreditTransaction)
        transaction.amount_cents = 3500

        result = CreditTransaction.amount.fget(transaction)

        assert result == 35.0

    def test_amount_property_handles_negative(self):
        """Test amount property handles negative amounts."""
        from smoothschedule.communication.credits.models import CreditTransaction

        transaction = Mock(spec=CreditTransaction)
        transaction.amount_cents = -1250

        result = CreditTransaction.amount.fget(transaction)

        assert result == -12.5

    def test_transaction_type_choices(self):
        """Test TransactionType choices are defined correctly."""
        from smoothschedule.communication.credits.models import CreditTransaction

        assert CreditTransaction.TransactionType.MANUAL == 'manual'
        assert CreditTransaction.TransactionType.AUTO_RELOAD == 'auto_reload'
        assert CreditTransaction.TransactionType.USAGE == 'usage'
        assert CreditTransaction.TransactionType.REFUND == 'refund'
        assert CreditTransaction.TransactionType.ADJUSTMENT == 'adjustment'
        assert CreditTransaction.TransactionType.PROMO == 'promo'

    def test_transaction_type_labels(self):
        """Test TransactionType labels are human-readable."""
        from smoothschedule.communication.credits.models import CreditTransaction

        choices_dict = dict(CreditTransaction.TransactionType.choices)
        assert choices_dict['manual'] == 'Manual Top-up'
        assert choices_dict['auto_reload'] == 'Auto Reload'
        assert choices_dict['usage'] == 'Usage'
        assert choices_dict['refund'] == 'Refund'
        assert choices_dict['adjustment'] == 'Adjustment'
        assert choices_dict['promo'] == 'Promotional Credit'


class TestProxyPhoneNumberModel:
    """Tests for ProxyPhoneNumber model."""

    def test_str_representation_without_tenant(self):
        """Test string representation for unassigned number."""
        from smoothschedule.communication.credits.models import ProxyPhoneNumber

        number = Mock(spec=ProxyPhoneNumber)
        number.phone_number = '+15551234567'
        number.assigned_tenant = None

        result = ProxyPhoneNumber.__str__(number)

        assert result == "+15551234567"

    def test_str_representation_with_tenant(self):
        """Test string representation for assigned number."""
        from smoothschedule.communication.credits.models import ProxyPhoneNumber

        mock_tenant = Mock()
        mock_tenant.name = "Test Business"

        number = Mock(spec=ProxyPhoneNumber)
        number.phone_number = '+15551234567'
        number.assigned_tenant = mock_tenant

        result = ProxyPhoneNumber.__str__(number)

        assert result == "+15551234567 (Test Business)"

    def test_assign_to_tenant_success(self):
        """Test assign_to_tenant with valid permissions."""
        from smoothschedule.communication.credits.models import ProxyPhoneNumber

        mock_tenant = Mock()
        mock_tenant.has_feature.return_value = True

        number = Mock()  # Don't use spec to allow attribute assignment
        number.save = Mock()

        with patch('django.utils.timezone.now') as mock_now:
            mock_now.return_value = datetime(2024, 1, 1, 12, 0, 0, tzinfo=dt_timezone.utc)

            ProxyPhoneNumber.assign_to_tenant(number, mock_tenant)

            # Verify save called with correct update_fields
            number.save.assert_called_once_with(
                update_fields=['assigned_tenant', 'assigned_at', 'status', 'updated_at']
            )

    def test_assign_to_tenant_without_permission(self):
        """Test assign_to_tenant raises PermissionDenied without feature."""
        from rest_framework.exceptions import PermissionDenied
        from smoothschedule.communication.credits.models import ProxyPhoneNumber

        mock_tenant = Mock()
        mock_tenant.has_feature.return_value = False

        number = Mock(spec=ProxyPhoneNumber)

        with pytest.raises(PermissionDenied) as exc_info:
            ProxyPhoneNumber.assign_to_tenant(number, mock_tenant)

        assert "Masked Calling" in str(exc_info.value)
        assert "upgrade" in str(exc_info.value).lower()

    def test_release_clears_assignment(self):
        """Test release returns number to pool."""
        from smoothschedule.communication.credits.models import ProxyPhoneNumber

        number = Mock()  # Don't use spec to allow attribute assignment
        number.save = Mock()

        ProxyPhoneNumber.release(number)

        # Verify save called with correct update_fields
        number.save.assert_called_once_with(
            update_fields=['assigned_tenant', 'assigned_at', 'status', 'updated_at']
        )

    def test_status_choices(self):
        """Test Status choices are defined correctly."""
        from smoothschedule.communication.credits.models import ProxyPhoneNumber

        assert ProxyPhoneNumber.Status.AVAILABLE == 'available'
        assert ProxyPhoneNumber.Status.ASSIGNED == 'assigned'
        assert ProxyPhoneNumber.Status.RESERVED == 'reserved'
        assert ProxyPhoneNumber.Status.INACTIVE == 'inactive'


class TestMaskedSessionModel:
    """Tests for MaskedSession model."""

    def test_str_representation(self):
        """Test string representation shows session info."""
        from smoothschedule.communication.credits.models import MaskedSession

        session = Mock(spec=MaskedSession)
        session.id = 42
        session.customer_phone = '+15551111111'
        session.staff_phone = '+15552222222'

        result = MaskedSession.__str__(session)

        assert result == "Session 42: +15551111111 <-> +15552222222"

    def test_is_active_returns_true_for_active_unexpired_session(self):
        """Test is_active returns True when status active and not expired."""
        from smoothschedule.communication.credits.models import MaskedSession

        now = datetime(2024, 1, 1, 12, 0, 0, tzinfo=dt_timezone.utc)
        future_time = now + timedelta(hours=1)

        session = Mock()  # Don't use spec to allow simpler attribute access
        session.status = MaskedSession.Status.ACTIVE  # Use the actual enum
        session.Status = MaskedSession.Status  # Make Status available on the instance
        session.expires_at = future_time

        with patch('smoothschedule.communication.credits.models.timezone.now') as mock_now:
            mock_now.return_value = now
            result = MaskedSession.is_active(session)

        assert result is True

    def test_is_active_returns_false_for_closed_session(self):
        """Test is_active returns False when status is closed."""
        from smoothschedule.communication.credits.models import MaskedSession

        future_time = timezone.now() + timedelta(hours=1)
        session = Mock(spec=MaskedSession)
        session.status = MaskedSession.Status.CLOSED
        session.expires_at = future_time

        result = MaskedSession.is_active(session)

        assert result is False

    def test_is_active_returns_false_for_expired_session(self):
        """Test is_active returns False when session is expired."""
        from smoothschedule.communication.credits.models import MaskedSession

        now = datetime(2024, 1, 1, 12, 0, 0, tzinfo=dt_timezone.utc)
        past_time = now - timedelta(hours=1)

        session = Mock(spec=MaskedSession)
        session.status = MaskedSession.Status.ACTIVE
        session.expires_at = past_time

        with patch('django.utils.timezone.now') as mock_now:
            mock_now.return_value = now
            result = MaskedSession.is_active(session)

        assert result is False

    def test_close_updates_status_and_timestamp(self):
        """Test close sets status and closed_at."""
        from smoothschedule.communication.credits.models import MaskedSession

        mock_proxy_number = Mock()
        mock_proxy_number.status = 'available'

        session = Mock()  # Don't use spec to allow attribute assignment
        session.proxy_number = mock_proxy_number
        session.save = Mock()

        with patch('smoothschedule.communication.credits.models.timezone.now') as mock_now:
            mock_now.return_value = datetime(2024, 1, 1, 12, 0, 0, tzinfo=dt_timezone.utc)

            MaskedSession.close(session)

            # Verify save called with correct update_fields
            session.save.assert_called_once_with(
                update_fields=['status', 'closed_at', 'updated_at']
            )

    def test_close_releases_reserved_proxy_number(self):
        """Test close releases proxy number if it was reserved."""
        from smoothschedule.communication.credits.models import MaskedSession, ProxyPhoneNumber

        mock_proxy_number = Mock()
        mock_proxy_number.status = ProxyPhoneNumber.Status.RESERVED
        mock_proxy_number.save = Mock()

        session = Mock(spec=MaskedSession)
        session.proxy_number = mock_proxy_number
        session.save = Mock()

        with patch('django.utils.timezone.now'):
            MaskedSession.close(session)

            # Verify proxy number released
            assert mock_proxy_number.status == ProxyPhoneNumber.Status.AVAILABLE
            mock_proxy_number.save.assert_called_once()

    def test_close_does_not_release_assigned_proxy_number(self):
        """Test close does not release assigned proxy number."""
        from smoothschedule.communication.credits.models import MaskedSession, ProxyPhoneNumber

        mock_proxy_number = Mock()
        mock_proxy_number.status = ProxyPhoneNumber.Status.ASSIGNED
        mock_proxy_number.save = Mock()

        session = Mock(spec=MaskedSession)
        session.proxy_number = mock_proxy_number
        session.save = Mock()

        with patch('django.utils.timezone.now'):
            MaskedSession.close(session)

            # Verify proxy number NOT changed
            assert mock_proxy_number.status == ProxyPhoneNumber.Status.ASSIGNED
            mock_proxy_number.save.assert_not_called()

    def test_get_destination_for_caller_customer_to_staff(self):
        """Test get_destination_for_caller routes customer to staff."""
        from smoothschedule.communication.credits.models import MaskedSession

        session = Mock(spec=MaskedSession)
        session.customer_phone = '+15551111111'
        session.staff_phone = '+15552222222'

        result = MaskedSession.get_destination_for_caller(session, '+15551111111')

        assert result == '+15552222222'

    def test_get_destination_for_caller_staff_to_customer(self):
        """Test get_destination_for_caller routes staff to customer."""
        from smoothschedule.communication.credits.models import MaskedSession

        session = Mock(spec=MaskedSession)
        session.customer_phone = '+15551111111'
        session.staff_phone = '+15552222222'

        result = MaskedSession.get_destination_for_caller(session, '+15552222222')

        assert result == '+15551111111'

    def test_get_destination_for_caller_unknown_caller(self):
        """Test get_destination_for_caller returns None for unknown caller."""
        from smoothschedule.communication.credits.models import MaskedSession

        session = Mock(spec=MaskedSession)
        session.customer_phone = '+15551111111'
        session.staff_phone = '+15552222222'

        result = MaskedSession.get_destination_for_caller(session, '+15559999999')

        assert result is None

    def test_get_destination_for_caller_normalizes_phone_numbers(self):
        """Test get_destination_for_caller normalizes phone formats."""
        from smoothschedule.communication.credits.models import MaskedSession

        session = Mock(spec=MaskedSession)
        session.customer_phone = '+15551111111'
        session.staff_phone = '+15552222222'

        # Test with spaces
        result = MaskedSession.get_destination_for_caller(session, '+1 555 111 1111')
        assert result == '+15552222222'

        # Test with dashes
        result = MaskedSession.get_destination_for_caller(session, '+1-555-111-1111')
        assert result == '+15552222222'

        # Test without country code
        result = MaskedSession.get_destination_for_caller(session, '5551111111')
        assert result == '+15552222222'

    def test_get_destination_for_caller_handles_partial_matches(self):
        """Test get_destination_for_caller handles partial number matches."""
        from smoothschedule.communication.credits.models import MaskedSession

        session = Mock(spec=MaskedSession)
        session.customer_phone = '5551111111'
        session.staff_phone = '5552222222'

        # Full number with country code should still match
        result = MaskedSession.get_destination_for_caller(session, '+15551111111')
        assert result == '5552222222'

    def test_status_choices(self):
        """Test Status choices are defined correctly."""
        from smoothschedule.communication.credits.models import MaskedSession

        assert MaskedSession.Status.ACTIVE == 'active'
        assert MaskedSession.Status.CLOSED == 'closed'
        assert MaskedSession.Status.EXPIRED == 'expired'
