"""
Comprehensive unit tests for mobile/field app services.

Tests StatusMachine and TwilioFieldCallService with mocks to avoid database hits.
Covers all state transitions, permission checks, Twilio integration, and error handling.
"""
from datetime import datetime, timedelta
from decimal import Decimal
from unittest.mock import Mock, MagicMock, patch, call
import pytest

from django.utils import timezone

from smoothschedule.communication.mobile.services.status_machine import (
    StatusMachine,
    StatusTransitionError,
)
from smoothschedule.communication.mobile.services.twilio_calls import (
    TwilioFieldCallService,
    TwilioFieldCallError,
)
from smoothschedule.scheduling.schedule.models import Event
from smoothschedule.identity.users.models import User


@pytest.mark.django_db
class TestStatusMachine:
    """Test StatusMachine state transitions and validation."""

    def test_init_stores_tenant_and_user(self):
        """Test StatusMachine initialization."""
        mock_tenant = Mock()
        mock_user = Mock()

        machine = StatusMachine(tenant=mock_tenant, user=mock_user)

        assert machine.tenant == mock_tenant
        assert machine.user == mock_user

    def test_can_transition_same_status_allowed(self):
        """Test can_transition returns True for same status."""
        machine = StatusMachine(tenant=Mock(), user=Mock())
        mock_event = Mock()
        mock_event.status = Event.Status.SCHEDULED

        is_valid, reason = machine.can_transition(mock_event, Event.Status.SCHEDULED)

        assert is_valid is True
        assert reason == ""

    def test_can_transition_scheduled_to_en_route(self):
        """Test valid transition from SCHEDULED to EN_ROUTE."""
        machine = StatusMachine(tenant=Mock(), user=Mock())
        mock_event = Mock()
        mock_event.status = Event.Status.SCHEDULED

        is_valid, reason = machine.can_transition(mock_event, Event.Status.EN_ROUTE)

        assert is_valid is True
        assert reason == ""

    def test_can_transition_scheduled_to_in_progress(self):
        """Test valid transition from SCHEDULED to IN_PROGRESS (skip EN_ROUTE)."""
        machine = StatusMachine(tenant=Mock(), user=Mock())
        mock_event = Mock()
        mock_event.status = Event.Status.SCHEDULED

        is_valid, reason = machine.can_transition(mock_event, Event.Status.IN_PROGRESS)

        assert is_valid is True
        assert reason == ""

    def test_can_transition_scheduled_to_canceled(self):
        """Test valid transition from SCHEDULED to CANCELED."""
        machine = StatusMachine(tenant=Mock(), user=Mock())
        mock_event = Mock()
        mock_event.status = Event.Status.SCHEDULED

        is_valid, reason = machine.can_transition(mock_event, Event.Status.CANCELED)

        assert is_valid is True

    def test_can_transition_scheduled_to_completed_invalid(self):
        """Test invalid transition from SCHEDULED to COMPLETED."""
        machine = StatusMachine(tenant=Mock(), user=Mock())
        mock_event = Mock()
        mock_event.status = Event.Status.SCHEDULED

        is_valid, reason = machine.can_transition(mock_event, Event.Status.COMPLETED)

        assert is_valid is False
        assert "Cannot transition from" in reason
        assert "SCHEDULED" in reason
        assert "COMPLETED" in reason

    def test_can_transition_en_route_to_in_progress(self):
        """Test valid transition from EN_ROUTE to IN_PROGRESS."""
        machine = StatusMachine(tenant=Mock(), user=Mock())
        mock_event = Mock()
        mock_event.status = Event.Status.EN_ROUTE

        is_valid, reason = machine.can_transition(mock_event, Event.Status.IN_PROGRESS)

        assert is_valid is True

    def test_can_transition_en_route_to_noshow(self):
        """Test valid transition from EN_ROUTE to NOSHOW."""
        machine = StatusMachine(tenant=Mock(), user=Mock())
        mock_event = Mock()
        mock_event.status = Event.Status.EN_ROUTE

        is_valid, reason = machine.can_transition(mock_event, Event.Status.NOSHOW)

        assert is_valid is True

    def test_can_transition_in_progress_to_completed(self):
        """Test valid transition from IN_PROGRESS to COMPLETED."""
        machine = StatusMachine(tenant=Mock(), user=Mock())
        mock_event = Mock()
        mock_event.status = Event.Status.IN_PROGRESS

        is_valid, reason = machine.can_transition(mock_event, Event.Status.COMPLETED)

        assert is_valid is True

    def test_can_transition_in_progress_to_noshow(self):
        """Test valid transition from IN_PROGRESS to NOSHOW."""
        machine = StatusMachine(tenant=Mock(), user=Mock())
        mock_event = Mock()
        mock_event.status = Event.Status.IN_PROGRESS

        is_valid, reason = machine.can_transition(mock_event, Event.Status.NOSHOW)

        assert is_valid is True

    def test_can_transition_completed_to_awaiting_payment(self):
        """Test valid transition from COMPLETED to AWAITING_PAYMENT."""
        machine = StatusMachine(tenant=Mock(), user=Mock())
        mock_event = Mock()
        mock_event.status = Event.Status.COMPLETED

        is_valid, reason = machine.can_transition(mock_event, Event.Status.AWAITING_PAYMENT)

        assert is_valid is True

    def test_can_transition_awaiting_payment_to_paid(self):
        """Test valid transition from AWAITING_PAYMENT to PAID."""
        machine = StatusMachine(tenant=Mock(), user=Mock())
        mock_event = Mock()
        mock_event.status = Event.Status.AWAITING_PAYMENT

        is_valid, reason = machine.can_transition(mock_event, Event.Status.PAID)

        assert is_valid is True

    def test_can_transition_from_terminal_state(self):
        """Test that terminal states (CANCELED, PAID, NOSHOW) have no valid transitions."""
        machine = StatusMachine(tenant=Mock(), user=Mock())

        for terminal_status in [Event.Status.CANCELED, Event.Status.PAID, Event.Status.NOSHOW]:
            mock_event = Mock()
            mock_event.status = terminal_status

            is_valid, reason = machine.can_transition(mock_event, Event.Status.SCHEDULED)

            assert is_valid is False
            assert "terminal state" in reason or "none" in reason.lower()

    def test_can_transition_backward_not_allowed(self):
        """Test that backward transitions are not allowed."""
        machine = StatusMachine(tenant=Mock(), user=Mock())
        mock_event = Mock()
        mock_event.status = Event.Status.COMPLETED

        is_valid, reason = machine.can_transition(mock_event, Event.Status.SCHEDULED)

        assert is_valid is False
        assert "Cannot transition" in reason

    def test_is_employee_assigned_as_user_participant(self):
        """Test is_employee_assigned returns True when user is direct participant."""
        with patch('django.contrib.contenttypes.models.ContentType') as mock_content_type, \
             patch('smoothschedule.scheduling.schedule.models.Participant') as mock_participant:
            mock_user = Mock()
            mock_user.id = 1

            mock_user_ct = Mock()
            mock_user_ct.id = 10
            mock_content_type.objects.get_for_model.return_value = mock_user_ct

            mock_participant.objects.filter.return_value.exists.return_value = True

            machine = StatusMachine(tenant=Mock(), user=mock_user)
            mock_event = Mock()

            result = machine.is_employee_assigned(mock_event)

            assert result is True
            mock_participant.objects.filter.assert_called_once()

    def test_is_employee_assigned_via_resource(self):
        """Test is_employee_assigned returns True when user linked to Resource participant."""
        with patch('django.contrib.contenttypes.models.ContentType') as mock_content_type, \
             patch('smoothschedule.scheduling.schedule.models.Participant') as mock_participant, \
             patch('smoothschedule.scheduling.schedule.models.Resource') as mock_resource:
            mock_user = Mock()
            mock_user.id = 1

            mock_user_ct = Mock()
            mock_resource_ct = Mock()
            mock_content_type.objects.get_for_model.side_effect = [mock_user_ct, mock_resource_ct]

            # User is not direct participant
            # Resource participant exists
            mock_participant.objects.filter.side_effect = [
                Mock(exists=Mock(return_value=False)),  # User participant check
                Mock(exists=Mock(return_value=True)),   # Resource participant check
            ]

            # User has linked resources
            mock_resource.objects.filter.return_value.values_list.return_value = [101, 102]

            machine = StatusMachine(tenant=Mock(), user=mock_user)
            mock_event = Mock()

            result = machine.is_employee_assigned(mock_event)

            assert result is True

    def test_is_employee_assigned_not_assigned(self):
        """Test is_employee_assigned returns False when user not assigned."""
        with patch('django.contrib.contenttypes.models.ContentType') as mock_content_type, \
             patch('smoothschedule.scheduling.schedule.models.Participant') as mock_participant, \
             patch('smoothschedule.scheduling.schedule.models.Resource') as mock_resource:
            mock_user = Mock()
            mock_user.id = 1

            mock_content_type.objects.get_for_model.return_value = Mock()

            # No user participant
            # No resource participant
            mock_participant.objects.filter.side_effect = [
                Mock(exists=Mock(return_value=False)),
                Mock(exists=Mock(return_value=False)),
            ]

            mock_resource.objects.filter.return_value.values_list.return_value = [101]

            machine = StatusMachine(tenant=Mock(), user=mock_user)
            mock_event = Mock()

            result = machine.is_employee_assigned(mock_event)

            assert result is False

    def test_can_user_change_status_owner_allowed(self):
        """Test can_user_change_status allows TENANT_OWNER."""
        mock_user = Mock()
        mock_user.role = User.Role.TENANT_OWNER

        machine = StatusMachine(tenant=Mock(), user=mock_user)
        mock_event = Mock()

        can_change, reason = machine.can_user_change_status(mock_event)

        assert can_change is True
        assert reason == ""

    def test_can_user_change_status_manager_allowed(self):
        """Test can_user_change_status allows TENANT_MANAGER."""
        mock_user = Mock()
        mock_user.role = User.Role.TENANT_MANAGER

        machine = StatusMachine(tenant=Mock(), user=mock_user)
        mock_event = Mock()

        can_change, reason = machine.can_user_change_status(mock_event)

        assert can_change is True

    def test_can_user_change_status_staff_assigned(self):
        """Test can_user_change_status allows assigned TENANT_STAFF."""
        mock_user = Mock()
        mock_user.role = User.Role.TENANT_STAFF

        machine = StatusMachine(tenant=Mock(), user=mock_user)
        machine.is_employee_assigned = Mock(return_value=True)

        mock_event = Mock()

        can_change, reason = machine.can_user_change_status(mock_event)

        assert can_change is True

    def test_can_user_change_status_staff_not_assigned(self):
        """Test can_user_change_status denies unassigned TENANT_STAFF."""
        mock_user = Mock()
        mock_user.role = User.Role.TENANT_STAFF

        machine = StatusMachine(tenant=Mock(), user=mock_user)
        machine.is_employee_assigned = Mock(return_value=False)

        mock_event = Mock()

        can_change, reason = machine.can_user_change_status(mock_event)

        assert can_change is False
        assert "not assigned" in reason

    def test_can_user_change_status_customer_denied(self):
        """Test can_user_change_status denies CUSTOMER role."""
        mock_user = Mock()
        mock_user.role = User.Role.CUSTOMER

        machine = StatusMachine(tenant=Mock(), user=mock_user)
        mock_event = Mock()

        can_change, reason = machine.can_user_change_status(mock_event)

        assert can_change is False
        assert "do not have permission" in reason

    def test_transition_success(self):
        """Test successful status transition."""
        with patch('smoothschedule.communication.mobile.services.status_machine.EventStatusHistory') as mock_history, \
             patch('smoothschedule.scheduling.schedule.signals.emit_status_change') as mock_emit_signal:

            mock_tenant = Mock()
            mock_user = Mock()
            mock_user.role = User.Role.TENANT_OWNER

            machine = StatusMachine(tenant=mock_tenant, user=mock_user)

            mock_event = Mock()
            mock_event.status = Event.Status.SCHEDULED
            mock_event.id = 1

            # Perform transition
            result = machine.transition(
                event=mock_event,
                new_status=Event.Status.EN_ROUTE,
                notes="Started driving",
                latitude=Decimal("40.7128"),
                longitude=Decimal("-74.0060"),
                source="mobile_app"
            )

            # Verify event status updated
            assert mock_event.status == Event.Status.EN_ROUTE
            mock_event.save.assert_called_once_with(update_fields=['status', 'updated_at'])

            # Verify history created
            mock_history.objects.create.assert_called_once_with(
                tenant=mock_tenant,
                event_id=1,
                old_status=Event.Status.SCHEDULED,
                new_status=Event.Status.EN_ROUTE,
                changed_by=mock_user,
                notes="Started driving",
                latitude=Decimal("40.7128"),
                longitude=Decimal("-74.0060"),
                source="mobile_app"
            )

            # Verify signal emitted
            mock_emit_signal.assert_called_once()
            assert result == mock_event

    def test_transition_permission_denied(self):
        """Test transition raises error when user lacks permission."""
        mock_user = Mock()
        mock_user.role = User.Role.CUSTOMER

        machine = StatusMachine(tenant=Mock(), user=mock_user)
        mock_event = Mock()
        mock_event.status = Event.Status.SCHEDULED

        with pytest.raises(StatusTransitionError) as exc_info:
            machine.transition(mock_event, Event.Status.EN_ROUTE)

        assert "do not have permission" in str(exc_info.value)

    def test_transition_invalid_transition(self):
        """Test transition raises error for invalid transition."""
        mock_user = Mock()
        mock_user.role = User.Role.TENANT_OWNER

        machine = StatusMachine(tenant=Mock(), user=mock_user)
        mock_event = Mock()
        mock_event.status = Event.Status.SCHEDULED

        with pytest.raises(StatusTransitionError) as exc_info:
            machine.transition(mock_event, Event.Status.COMPLETED)

        assert "Cannot transition from" in str(exc_info.value)

    def test_transition_to_non_tracking_status_stops_tracking(self):
        """Test transition to COMPLETED stops location tracking."""
        with patch('smoothschedule.communication.mobile.services.status_machine.EventStatusHistory'), \
             patch('smoothschedule.scheduling.schedule.signals.emit_status_change'):

            mock_user = Mock()
            mock_user.role = User.Role.TENANT_OWNER

            machine = StatusMachine(tenant=Mock(), user=mock_user)
            machine._stop_location_tracking = Mock()

            mock_event = Mock()
            mock_event.status = Event.Status.IN_PROGRESS
            mock_event.id = 1

            machine.transition(mock_event, Event.Status.COMPLETED)

            # Verify stop tracking called
            machine._stop_location_tracking.assert_called_once_with(mock_event)

    def test_transition_to_tracking_status_no_stop(self):
        """Test transition to tracking status doesn't stop tracking."""
        with patch('smoothschedule.communication.mobile.services.status_machine.EventStatusHistory'), \
             patch('smoothschedule.scheduling.schedule.signals.emit_status_change'):

            mock_user = Mock()
            mock_user.role = User.Role.TENANT_OWNER

            machine = StatusMachine(tenant=Mock(), user=mock_user)
            machine._stop_location_tracking = Mock()

            mock_event = Mock()
            mock_event.status = Event.Status.SCHEDULED
            mock_event.id = 1

            machine.transition(mock_event, Event.Status.EN_ROUTE)

            # Verify stop tracking NOT called
            machine._stop_location_tracking.assert_not_called()

    def test_transition_skip_notifications(self):
        """Test transition with skip_notifications=True."""
        with patch('smoothschedule.communication.mobile.services.status_machine.EventStatusHistory'), \
             patch('smoothschedule.scheduling.schedule.signals.emit_status_change') as mock_emit_signal:

            mock_user = Mock()
            mock_user.role = User.Role.TENANT_OWNER

            machine = StatusMachine(tenant=Mock(), user=mock_user)
            mock_event = Mock()
            mock_event.status = Event.Status.SCHEDULED
            mock_event.id = 1

            machine.transition(mock_event, Event.Status.EN_ROUTE, skip_notifications=True)

            # Verify signal called with skip_notifications=True
            call_kwargs = mock_emit_signal.call_args[1]
            assert call_kwargs['skip_notifications'] is True

    def test_get_allowed_transitions_scheduled(self):
        """Test get_allowed_transitions for SCHEDULED status."""
        machine = StatusMachine(tenant=Mock(), user=Mock())
        mock_event = Mock()
        mock_event.status = Event.Status.SCHEDULED

        allowed = machine.get_allowed_transitions(mock_event)

        assert Event.Status.EN_ROUTE in allowed
        assert Event.Status.IN_PROGRESS in allowed
        assert Event.Status.CANCELED in allowed
        assert Event.Status.COMPLETED not in allowed

    def test_get_allowed_transitions_terminal_state(self):
        """Test get_allowed_transitions for terminal state."""
        machine = StatusMachine(tenant=Mock(), user=Mock())
        mock_event = Mock()
        mock_event.status = Event.Status.PAID

        allowed = machine.get_allowed_transitions(mock_event)

        assert allowed == []

    def test_get_status_history(self):
        """Test get_status_history retrieves history records."""
        with patch('smoothschedule.communication.mobile.services.status_machine.EventStatusHistory') as mock_history:
            mock_tenant = Mock()
            mock_tenant.id = 1

            machine = StatusMachine(tenant=mock_tenant, user=Mock())

            mock_history_records = [Mock(), Mock(), Mock()]
            # Mock the queryset chain to return our list when sliced
            mock_qs = Mock()
            mock_qs.select_related.return_value = mock_qs
            mock_qs.__getitem__ = Mock(return_value=mock_history_records)
            mock_qs.__iter__ = Mock(return_value=iter(mock_history_records))
            mock_history.objects.filter.return_value = mock_qs

            result = machine.get_status_history(event_id=123, limit=50)

            # The result should be a list since the code calls list() on the queryset slice
            assert len(result) == 3
            mock_history.objects.filter.assert_called_once_with(
                tenant=mock_tenant,
                event_id=123
            )

    def test_stop_location_tracking_placeholder(self):
        """Test _stop_location_tracking is a placeholder method."""
        machine = StatusMachine(tenant=Mock(), user=Mock())
        mock_event = Mock()

        # Should not raise any errors
        machine._stop_location_tracking(mock_event)


@pytest.mark.django_db
class TestTwilioFieldCallService:
    """Test TwilioFieldCallService call and SMS functionality."""

    def test_init_stores_tenant(self):
        """Test TwilioFieldCallService initialization."""
        mock_tenant = Mock()

        service = TwilioFieldCallService(tenant=mock_tenant)

        assert service.tenant == mock_tenant
        assert service._client is None

    @patch('twilio.rest.Client')
    def test_client_property_uses_tenant_subaccount(self, mock_twilio_client):
        """Test client property uses tenant's Twilio subaccount."""
        mock_tenant = Mock()
        mock_tenant.twilio_subaccount_sid = "AC123"
        mock_tenant.twilio_subaccount_auth_token = "token123"

        service = TwilioFieldCallService(tenant=mock_tenant)

        # Access client property
        client = service.client

        mock_twilio_client.assert_called_once_with("AC123", "token123")
        assert client == mock_twilio_client.return_value

    @patch('twilio.rest.Client')
    @patch('smoothschedule.communication.mobile.services.twilio_calls.settings')
    def test_client_property_falls_back_to_master_account(self, mock_settings, mock_twilio_client):
        """Test client property falls back to master account."""
        mock_tenant = Mock()
        mock_tenant.twilio_subaccount_sid = None
        mock_tenant.twilio_subaccount_auth_token = None

        mock_settings.TWILIO_ACCOUNT_SID = "AC_MASTER"
        mock_settings.TWILIO_AUTH_TOKEN = "master_token"

        service = TwilioFieldCallService(tenant=mock_tenant)

        # Access client property
        client = service.client

        mock_twilio_client.assert_called_once_with("AC_MASTER", "master_token")

    @patch('smoothschedule.communication.mobile.services.twilio_calls.settings')
    def test_client_property_raises_error_when_not_configured(self, mock_settings):
        """Test client property raises error when Twilio not configured."""
        mock_tenant = Mock()
        mock_tenant.twilio_subaccount_sid = None
        mock_tenant.twilio_subaccount_auth_token = None

        mock_settings.TWILIO_ACCOUNT_SID = ''
        mock_settings.TWILIO_AUTH_TOKEN = ''

        service = TwilioFieldCallService(tenant=mock_tenant)

        with pytest.raises(TwilioFieldCallError) as exc_info:
            _ = service.client

        assert "not configured" in str(exc_info.value)

    @patch('smoothschedule.communication.credits.models.MaskedSession')
    @patch('smoothschedule.communication.mobile.services.twilio_calls.timezone')
    def test_get_or_create_session_returns_existing(self, mock_tz, mock_session_model):
        """Test _get_or_create_session returns existing active session."""
        mock_now = datetime(2024, 1, 1, 12, 0)
        mock_tz.now.return_value = mock_now

        mock_tenant = Mock()
        mock_tenant.id = 1

        mock_existing_session = Mock()
        mock_existing_session.staff_phone = "+15551234567"
        mock_existing_session.customer_phone = "+15559876543"

        mock_session_model.objects.filter.return_value.first.return_value = mock_existing_session

        service = TwilioFieldCallService(tenant=mock_tenant)

        result = service._get_or_create_session(
            event_id=123,
            employee_phone="+15551234567",
            customer_phone="+15559876543"
        )

        assert result == mock_existing_session
        # Should not create new session
        mock_session_model.objects.create.assert_not_called()

    @patch('smoothschedule.communication.credits.models.MaskedSession')
    @patch('smoothschedule.communication.credits.models.ProxyPhoneNumber')
    @patch('smoothschedule.communication.mobile.services.twilio_calls.timezone')
    def test_get_or_create_session_creates_new(self, mock_tz, mock_proxy_number_model, mock_session_model):
        """Test _get_or_create_session creates new session when none exists."""
        mock_now = datetime(2024, 1, 1, 12, 0)
        mock_tz.now.return_value = mock_now

        mock_tenant = Mock()

        # No existing session
        mock_session_model.objects.filter.return_value.first.return_value = None

        # Mock proxy number
        mock_proxy = Mock()
        mock_proxy.phone_number = "+15550001111"
        mock_proxy_number_model.Status = Mock(RESERVED='reserved')

        mock_new_session = Mock()
        mock_session_model.objects.create.return_value = mock_new_session
        mock_session_model.Status = Mock(ACTIVE='active')

        service = TwilioFieldCallService(tenant=mock_tenant)
        service._get_available_proxy_number = Mock(return_value=mock_proxy)

        result = service._get_or_create_session(
            event_id=123,
            employee_phone="+15551234567",
            customer_phone="+15559876543"
        )

        assert result == mock_new_session
        mock_session_model.objects.create.assert_called_once()
        # Proxy number should be marked as reserved
        assert mock_proxy.status == mock_proxy_number_model.Status.RESERVED
        mock_proxy.save.assert_called_once()

    @patch('smoothschedule.communication.credits.models.ProxyPhoneNumber')
    def test_get_available_proxy_number_tenant_assigned(self, mock_proxy_number_model):
        """Test _get_available_proxy_number prefers tenant-assigned numbers."""
        mock_tenant = Mock()

        mock_tenant_number = Mock()
        mock_proxy_number_model.objects.filter.return_value.first.return_value = mock_tenant_number
        mock_proxy_number_model.Status = Mock(ASSIGNED='assigned', AVAILABLE='available')

        service = TwilioFieldCallService(tenant=mock_tenant)

        result = service._get_available_proxy_number()

        assert result == mock_tenant_number

    @patch('smoothschedule.communication.credits.models.ProxyPhoneNumber')
    def test_get_available_proxy_number_shared_pool(self, mock_proxy_number_model):
        """Test _get_available_proxy_number falls back to shared pool."""
        mock_tenant = Mock()

        mock_shared_number = Mock()
        # First call (tenant-assigned) returns None, second call (shared) returns number
        mock_proxy_number_model.objects.filter.return_value.first.side_effect = [None, mock_shared_number]
        mock_proxy_number_model.Status = Mock(ASSIGNED='assigned', AVAILABLE='available')

        service = TwilioFieldCallService(tenant=mock_tenant)

        result = service._get_available_proxy_number()

        assert result == mock_shared_number

    def test_check_feature_permission_raises_when_disabled(self):
        """Test _check_feature_permission raises error when feature disabled."""
        mock_tenant = Mock()
        mock_tenant.has_feature.return_value = False

        service = TwilioFieldCallService(tenant=mock_tenant)

        with pytest.raises(TwilioFieldCallError) as exc_info:
            service._check_feature_permission()

        assert "not available on your current plan" in str(exc_info.value)

    def test_check_feature_permission_passes_when_enabled(self):
        """Test _check_feature_permission passes when feature enabled."""
        mock_tenant = Mock()
        mock_tenant.has_feature.return_value = True

        service = TwilioFieldCallService(tenant=mock_tenant)

        # Should not raise
        service._check_feature_permission()

    def test_check_credits_raises_when_insufficient(self):
        """Test _check_credits raises error when credits insufficient."""
        from smoothschedule.communication.credits.models import CommunicationCredits
        mock_tenant = Mock()

        mock_credits = Mock()
        mock_credits.balance_cents = 40  # Less than 50

        with patch.object(CommunicationCredits.objects, 'get', return_value=mock_credits):
            service = TwilioFieldCallService(tenant=mock_tenant)

            with pytest.raises(TwilioFieldCallError) as exc_info:
                service._check_credits(50)

            assert "Insufficient communication credits" in str(exc_info.value)

    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    def test_check_credits_passes_when_sufficient(self, mock_credits_model):
        """Test _check_credits passes when credits sufficient."""
        mock_tenant = Mock()

        mock_credits = Mock()
        mock_credits.balance_cents = 100
        mock_credits_model.objects.get.return_value = mock_credits

        service = TwilioFieldCallService(tenant=mock_tenant)

        # Should not raise
        service._check_credits(50)

    @patch('smoothschedule.communication.credits.models.CommunicationCredits')
    def test_check_credits_raises_when_not_set_up(self, mock_credits_model):
        """Test _check_credits raises error when credits not set up."""
        mock_tenant = Mock()

        mock_credits_model.DoesNotExist = Exception
        mock_credits_model.objects.get.side_effect = mock_credits_model.DoesNotExist()

        service = TwilioFieldCallService(tenant=mock_tenant)

        with pytest.raises(TwilioFieldCallError) as exc_info:
            service._check_credits(50)

        assert "not set up" in str(exc_info.value)

    @patch('django_tenants.utils.schema_context')
    @patch('smoothschedule.scheduling.schedule.models.Event')
    @patch('smoothschedule.scheduling.schedule.models.Participant')
    @patch('django.contrib.contenttypes.models.ContentType')
    def test_get_customer_phone_for_event(self, mock_ct, mock_participant, mock_event_model, mock_schema_context):
        """Test _get_customer_phone_for_event retrieves customer phone."""
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'

        mock_customer = Mock()
        mock_customer.phone = "+15551234567"

        mock_participant_obj = Mock()
        mock_participant_obj.content_object = mock_customer
        mock_participant_obj.Role = Mock(CUSTOMER='customer')

        mock_participant.objects.filter.return_value.first.return_value = mock_participant_obj
        mock_participant.Role = Mock(CUSTOMER='customer')

        mock_event = Mock()
        mock_event_model.objects.get.return_value = mock_event

        service = TwilioFieldCallService(tenant=mock_tenant)

        result = service._get_customer_phone_for_event(event_id=123)

        assert result == "+15551234567"
        mock_schema_context.assert_called_once_with('demo')

    @patch('django_tenants.utils.schema_context')
    @patch('smoothschedule.scheduling.schedule.models.Event')
    def test_get_customer_phone_for_event_not_found(self, mock_event_model, mock_schema_context):
        """Test _get_customer_phone_for_event returns None when event not found."""
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'

        mock_event_model.DoesNotExist = Exception
        mock_event_model.objects.get.side_effect = mock_event_model.DoesNotExist()

        service = TwilioFieldCallService(tenant=mock_tenant)

        result = service._get_customer_phone_for_event(event_id=999)

        assert result is None

    @patch('smoothschedule.communication.mobile.models.FieldCallLog')
    @patch('twilio.rest.Client')
    def test_initiate_call_success(self, mock_twilio_client, mock_call_log):
        """Test initiate_call creates call successfully."""
        mock_tenant = Mock()
        mock_tenant.twilio_subaccount_sid = "AC123"
        mock_tenant.twilio_subaccount_auth_token = "token123"
        mock_tenant.has_feature.return_value = True

        mock_employee = Mock()
        mock_employee.id = 1
        mock_employee.phone = "+15551234567"

        # Mock call response
        mock_call = Mock()
        mock_call.sid = "CA123456789"
        mock_client = Mock()
        mock_client.calls.create.return_value = mock_call
        mock_twilio_client.return_value = mock_client

        # Mock call log
        mock_log = Mock()
        mock_log.id = 10
        mock_call_log.objects.create.return_value = mock_log
        mock_call_log.CallType = Mock(VOICE='voice')
        mock_call_log.Direction = Mock(OUTBOUND='outbound')
        mock_call_log.Status = Mock(INITIATED='initiated')

        service = TwilioFieldCallService(tenant=mock_tenant)
        service._check_feature_permission = Mock()
        service._check_credits = Mock()
        service._get_or_create_session = Mock(return_value=Mock(
            id=1,
            proxy_number=Mock(phone_number="+15550001111")
        ))
        service._get_callback_url = Mock(return_value="https://example.com/callback")
        service._get_status_callback_url = Mock(return_value="https://example.com/status")

        result = service.initiate_call(
            event_id=123,
            employee=mock_employee,
            customer_phone="+15559876543"
        )

        assert result['call_sid'] == "CA123456789"
        assert result['status'] == 'initiated'
        mock_client.calls.create.assert_called_once()
        mock_call_log.objects.create.assert_called_once()

    @patch('twilio.rest.Client')
    def test_initiate_call_no_customer_phone(self, mock_twilio_client):
        """Test initiate_call raises error when customer phone not found."""
        mock_tenant = Mock()
        mock_tenant.has_feature.return_value = True

        mock_employee = Mock()
        mock_employee.phone = "+15551234567"

        service = TwilioFieldCallService(tenant=mock_tenant)
        service._check_feature_permission = Mock()
        service._check_credits = Mock()
        service._get_customer_phone_for_event = Mock(return_value=None)

        with pytest.raises(TwilioFieldCallError) as exc_info:
            service.initiate_call(event_id=123, employee=mock_employee)

        assert "Customer phone number not found" in str(exc_info.value)

    @patch('twilio.rest.Client')
    def test_initiate_call_no_employee_phone(self, mock_twilio_client):
        """Test initiate_call raises error when employee phone not set."""
        mock_tenant = Mock()
        mock_tenant.has_feature.return_value = True

        mock_employee = Mock()
        mock_employee.phone = None

        service = TwilioFieldCallService(tenant=mock_tenant)
        service._check_feature_permission = Mock()
        service._check_credits = Mock()

        with pytest.raises(TwilioFieldCallError) as exc_info:
            service.initiate_call(
                event_id=123,
                employee=mock_employee,
                customer_phone="+15559876543"
            )

        assert "Your phone number is not set" in str(exc_info.value)

    @patch('smoothschedule.communication.mobile.models.FieldCallLog')
    @patch('twilio.rest.Client')
    def test_send_sms_success(self, mock_twilio_client, mock_call_log):
        """Test send_sms sends SMS successfully."""
        mock_tenant = Mock()
        mock_tenant.twilio_subaccount_sid = "AC123"
        mock_tenant.twilio_subaccount_auth_token = "token123"
        mock_tenant.has_feature.return_value = True

        mock_employee = Mock()
        mock_employee.id = 1
        mock_employee.phone = "+15551234567"

        # Mock SMS response
        mock_sms = Mock()
        mock_sms.sid = "SM123456789"
        mock_client = Mock()
        mock_client.messages.create.return_value = mock_sms
        mock_twilio_client.return_value = mock_client

        # Mock session
        mock_session = Mock()
        mock_session.id = 1
        mock_session.proxy_number = Mock(phone_number="+15550001111")
        mock_session.sms_count = 0

        # Mock call log
        mock_log = Mock()
        mock_log.id = 10
        mock_call_log.objects.create.return_value = mock_log
        mock_call_log.CallType = Mock(SMS='sms')
        mock_call_log.Direction = Mock(OUTBOUND='outbound')
        mock_call_log.Status = Mock(COMPLETED='completed')

        service = TwilioFieldCallService(tenant=mock_tenant)
        service._check_feature_permission = Mock()
        service._check_credits = Mock()
        service._get_or_create_session = Mock(return_value=mock_session)
        service._get_sms_status_callback_url = Mock(return_value="https://example.com/sms-status")

        result = service.send_sms(
            event_id=123,
            employee=mock_employee,
            message="On my way!",
            customer_phone="+15559876543"
        )

        assert result['message_sid'] == "SM123456789"
        assert result['status'] == 'sent'
        mock_client.messages.create.assert_called_once()
        mock_call_log.objects.create.assert_called_once()
        assert mock_session.sms_count == 1
        mock_session.save.assert_called_once()

    def test_send_sms_empty_message(self):
        """Test send_sms raises error for empty message."""
        service = TwilioFieldCallService(tenant=Mock())
        service._check_feature_permission = Mock()
        service._check_credits = Mock()

        with pytest.raises(TwilioFieldCallError) as exc_info:
            service.send_sms(
                event_id=123,
                employee=Mock(phone="+15551234567"),
                message="",
                customer_phone="+15559876543"
            )

        assert "Message cannot be empty" in str(exc_info.value)

    def test_send_sms_message_too_long(self):
        """Test send_sms raises error for message exceeding limit."""
        service = TwilioFieldCallService(tenant=Mock())
        service._check_feature_permission = Mock()
        service._check_credits = Mock()

        with pytest.raises(TwilioFieldCallError) as exc_info:
            service.send_sms(
                event_id=123,
                employee=Mock(phone="+15551234567"),
                message="A" * 1601,
                customer_phone="+15559876543"
            )

        assert "Message too long" in str(exc_info.value)

    @patch('smoothschedule.communication.credits.models.MaskedSession')
    @patch('smoothschedule.communication.mobile.services.twilio_calls.timezone')
    def test_get_session_for_event(self, mock_tz, mock_session_model):
        """Test get_session_for_event retrieves active session."""
        mock_now = datetime(2024, 1, 1, 12, 0)
        mock_tz.now.return_value = mock_now

        mock_tenant = Mock()

        mock_session = Mock()
        mock_session_model.objects.filter.return_value.first.return_value = mock_session
        mock_session_model.Status = Mock(ACTIVE='active')

        service = TwilioFieldCallService(tenant=mock_tenant)

        result = service.get_session_for_event(event_id=123)

        assert result == mock_session

    @patch('smoothschedule.communication.credits.models.MaskedSession')
    @patch('smoothschedule.communication.mobile.services.twilio_calls.timezone')
    def test_close_session(self, mock_tz, mock_session_model):
        """Test close_session closes active session."""
        mock_session = Mock()
        mock_session_model.objects.filter.return_value.first.return_value = mock_session
        mock_session_model.Status = Mock(ACTIVE='active')

        service = TwilioFieldCallService(tenant=Mock())

        service.close_session(event_id=123)

        mock_session.close.assert_called_once()

    @patch('smoothschedule.communication.mobile.models.FieldCallLog')
    def test_get_call_history(self, mock_call_log):
        """Test get_call_history retrieves call logs."""
        mock_tenant = Mock()

        mock_logs = [Mock(), Mock()]
        mock_qs = Mock()
        mock_qs.select_related.return_value.__getitem__ = Mock(return_value=mock_logs)
        mock_call_log.objects.filter.return_value = mock_qs

        service = TwilioFieldCallService(tenant=mock_tenant)

        result = service.get_call_history(event_id=123, limit=20)

        assert result == mock_logs

    @patch('smoothschedule.communication.mobile.services.twilio_calls.settings')
    def test_get_callback_url(self, mock_settings):
        """Test _get_callback_url builds correct URL."""
        mock_settings.TWILIO_WEBHOOK_BASE_URL = "https://api.example.com"

        service = TwilioFieldCallService(tenant=Mock())

        result = service._get_callback_url('voice', 123)

        assert result == "https://api.example.com/api/mobile/twilio/voice/123/"

    @patch('smoothschedule.communication.mobile.services.twilio_calls.settings')
    def test_get_status_callback_url(self, mock_settings):
        """Test _get_status_callback_url builds correct URL."""
        mock_settings.TWILIO_WEBHOOK_BASE_URL = "https://api.example.com"

        service = TwilioFieldCallService(tenant=Mock())

        result = service._get_status_callback_url(123)

        assert result == "https://api.example.com/api/mobile/twilio/voice-status/123/"

    @patch('smoothschedule.communication.mobile.services.twilio_calls.settings')
    def test_get_sms_status_callback_url(self, mock_settings):
        """Test _get_sms_status_callback_url builds correct URL."""
        mock_settings.TWILIO_WEBHOOK_BASE_URL = "https://api.example.com"

        service = TwilioFieldCallService(tenant=Mock())

        result = service._get_sms_status_callback_url(123)

        assert result == "https://api.example.com/api/mobile/twilio/sms-status/123/"


class TestTwilioWebhookHandlers:
    """Test standalone Twilio webhook handler functions."""

    @patch('smoothschedule.communication.credits.models.MaskedSession')
    @patch('twilio.twiml.voice_response.VoiceResponse')
    def test_handle_incoming_call_routes_to_customer(self, mock_voice_response, mock_session_model):
        """Test handle_incoming_call routes employee call to customer."""
        from smoothschedule.communication.mobile.services.twilio_calls import handle_incoming_call

        mock_session = Mock()
        mock_session.is_active.return_value = True
        mock_session.get_destination_for_caller.return_value = "+15559876543"
        mock_session.proxy_number = Mock(phone_number="+15550001111")

        mock_session_model.objects.select_related.return_value.get.return_value = mock_session

        mock_response = Mock()
        mock_response_str = '<?xml version="1.0"?><Response><Dial>+15559876543</Dial></Response>'
        mock_response.__str__ = Mock(return_value=mock_response_str)
        mock_voice_response.return_value = mock_response

        result = handle_incoming_call(session_id=123, from_number="+15551234567")

        assert result == mock_response_str
        mock_response.dial.assert_called_once_with(
            "+15559876543",
            caller_id=mock_session.proxy_number.phone_number,
            timeout=30
        )

    @patch('smoothschedule.communication.credits.models.MaskedSession')
    @patch('twilio.twiml.voice_response.VoiceResponse')
    def test_handle_incoming_call_session_inactive(self, mock_voice_response, mock_session_model):
        """Test handle_incoming_call handles inactive session."""
        from smoothschedule.communication.mobile.services.twilio_calls import handle_incoming_call

        mock_session = Mock()
        mock_session.is_active.return_value = False

        mock_session_model.objects.select_related.return_value.get.return_value = mock_session

        mock_response = Mock()
        mock_voice_response.return_value = mock_response

        result = handle_incoming_call(session_id=123, from_number="+15551234567")

        mock_response.say.assert_called_once()
        mock_response.hangup.assert_called_once()

    @patch('smoothschedule.communication.credits.models.MaskedSession')
    @patch('twilio.twiml.voice_response.VoiceResponse')
    def test_handle_incoming_call_session_not_found(self, mock_voice_response, mock_session_model):
        """Test handle_incoming_call handles missing session."""
        from smoothschedule.communication.mobile.services.twilio_calls import handle_incoming_call

        mock_session_model.DoesNotExist = Exception
        mock_session_model.objects.select_related.return_value.get.side_effect = mock_session_model.DoesNotExist()

        mock_response = Mock()
        mock_voice_response.return_value = mock_response

        result = handle_incoming_call(session_id=999, from_number="+15551234567")

        mock_response.say.assert_called_once()
        mock_response.hangup.assert_called_once()

    @patch('smoothschedule.communication.credits.models.MaskedSession')
    @patch('twilio.rest.Client')
    def test_handle_incoming_sms_forwards_message(self, mock_client_class, mock_session_model):
        """Test handle_incoming_sms forwards SMS to destination."""
        from smoothschedule.communication.mobile.services.twilio_calls import handle_incoming_sms

        mock_session = Mock()
        mock_session.is_active.return_value = True
        mock_session.get_destination_for_caller.return_value = "+15559876543"
        mock_session.proxy_number = Mock(phone_number="+15550001111")
        mock_session.tenant = Mock(
            twilio_subaccount_sid="AC123",
            twilio_subaccount_auth_token="token123"
        )
        mock_session.sms_count = 5

        mock_session_model.objects.select_related.return_value.get.return_value = mock_session

        mock_client = Mock()
        mock_client_class.return_value = mock_client

        result = handle_incoming_sms(
            session_id=123,
            from_number="+15551234567",
            body="Running late"
        )

        assert result == ""
        mock_client.messages.create.assert_called_once_with(
            to="+15559876543",
            from_="+15550001111",
            body="Running late"
        )
        assert mock_session.sms_count == 6
        mock_session.save.assert_called_once()

    @patch('smoothschedule.communication.credits.models.MaskedSession')
    def test_handle_incoming_sms_session_inactive(self, mock_session_model):
        """Test handle_incoming_sms ignores inactive session."""
        from smoothschedule.communication.mobile.services.twilio_calls import handle_incoming_sms

        mock_session = Mock()
        mock_session.is_active.return_value = False

        mock_session_model.objects.select_related.return_value.get.return_value = mock_session

        result = handle_incoming_sms(
            session_id=123,
            from_number="+15551234567",
            body="Test"
        )

        assert result == ""

    @patch('smoothschedule.communication.credits.models.MaskedSession')
    def test_handle_incoming_sms_session_not_found(self, mock_session_model):
        """Test handle_incoming_sms handles missing session."""
        from smoothschedule.communication.mobile.services.twilio_calls import handle_incoming_sms

        mock_session_model.DoesNotExist = Exception
        mock_session_model.objects.select_related.return_value.get.side_effect = mock_session_model.DoesNotExist()

        result = handle_incoming_sms(
            session_id=999,
            from_number="+15551234567",
            body="Test"
        )

        assert result == ""
