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

Tests all API views, ViewSets, permissions, business logic, and error handling
using APIRequestFactory with mocked authentication and dependencies.
"""
from datetime import datetime, timedelta, UTC
from decimal import Decimal
from unittest.mock import Mock, MagicMock, patch, call
import pytest

from django.http import HttpResponse
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APIRequestFactory

from smoothschedule.communication.mobile.views import (
    get_tenant_from_user,
    is_field_employee,
    get_employee_jobs_queryset,
    employee_profile_view,
    logout_view,
    job_detail_view,
    set_status_view,
    start_en_route_view,
    reschedule_job_view,
    location_update_view,
    location_route_view,
    call_customer_view,
    send_sms_view,
    call_history_view,
    twilio_voice_webhook,
    twilio_voice_status_webhook,
    twilio_sms_webhook,
    twilio_sms_status_webhook,
)
from smoothschedule.identity.users.models import User
from smoothschedule.scheduling.schedule.models import Event, Participant
from smoothschedule.communication.mobile.models import (
    EventStatusHistory,
    EmployeeLocationUpdate,
    FieldCallLog,
)


class TestHelperFunctions:
    """Test utility helper functions."""

    def test_get_tenant_from_user_with_tenant(self):
        """Test get_tenant_from_user returns tenant when user has one."""
        mock_user = Mock()
        mock_tenant = Mock()
        mock_user.tenant = mock_tenant

        result = get_tenant_from_user(mock_user)
        assert result == mock_tenant

    def test_get_tenant_from_user_without_tenant(self):
        """Test get_tenant_from_user returns None when user has no tenant."""
        mock_user = Mock()
        mock_user.tenant = None

        result = get_tenant_from_user(mock_user)
        assert result is None

    def test_is_field_employee_with_staff_role(self):
        """Test is_field_employee returns True for TENANT_STAFF."""
        mock_user = Mock()
        mock_user.role = User.Role.TENANT_STAFF

        assert is_field_employee(mock_user) is True

    def test_is_field_employee_with_manager_role(self):
        """Test is_field_employee returns True for TENANT_MANAGER."""
        mock_user = Mock()
        mock_user.role = User.Role.TENANT_MANAGER

        assert is_field_employee(mock_user) is True

    def test_is_field_employee_with_owner_role(self):
        """Test is_field_employee returns True for TENANT_OWNER."""
        mock_user = Mock()
        mock_user.role = User.Role.TENANT_OWNER

        assert is_field_employee(mock_user) is True

    def test_is_field_employee_with_customer_role(self):
        """Test is_field_employee returns False for CUSTOMER."""
        mock_user = Mock()
        mock_user.role = User.Role.CUSTOMER

        assert is_field_employee(mock_user) is False

    def test_is_field_employee_with_superuser_role(self):
        """Test is_field_employee returns False for SUPERUSER."""
        mock_user = Mock()
        mock_user.role = User.Role.SUPERUSER

        assert is_field_employee(mock_user) is False

    @patch('smoothschedule.communication.mobile.views.ContentType')
    @patch('smoothschedule.communication.mobile.views.Resource')
    @patch('smoothschedule.communication.mobile.views.Participant')
    @patch('smoothschedule.communication.mobile.views.Event')
    def test_get_employee_jobs_queryset(
        self, mock_event_model, mock_participant_model, mock_resource_model, mock_content_type
    ):
        """Test get_employee_jobs_queryset returns correct events."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 1
        mock_tenant = Mock()
        mock_tenant.id = 1

        # Mock ContentType
        mock_user_ct = Mock()
        mock_user_ct.id = 10
        mock_resource_ct = Mock()
        mock_resource_ct.id = 20
        mock_content_type.objects.get_for_model.side_effect = [mock_user_ct, mock_resource_ct]

        # Mock Resource query
        mock_resource_qs = Mock()
        mock_resource_qs.values_list.return_value = [101, 102]
        mock_resource_model.objects.filter.return_value = mock_resource_qs

        # Mock Participant queries
        mock_user_participant_qs = Mock()
        mock_user_participant_qs.values_list.return_value = [1, 2, 3]

        mock_resource_participant_qs = Mock()
        mock_resource_participant_qs.values_list.return_value = [3, 4, 5]

        mock_participant_model.objects.filter.side_effect = [
            mock_user_participant_qs,
            mock_resource_participant_qs
        ]

        # Mock Event query
        mock_event_qs = Mock()
        mock_event_model.objects.filter.return_value = mock_event_qs

        # Act
        result = get_employee_jobs_queryset(mock_user, mock_tenant)

        # Assert
        assert result == mock_event_qs
        mock_event_model.objects.filter.assert_called_once()
        # Verify event IDs are combined from both participant queries
        call_args = mock_event_model.objects.filter.call_args
        event_ids = call_args[1]['id__in']
        assert 1 in event_ids
        assert 2 in event_ids
        assert 3 in event_ids
        assert 4 in event_ids
        assert 5 in event_ids

    @patch('smoothschedule.communication.mobile.views.ContentType')
    @patch('smoothschedule.communication.mobile.views.Resource')
    @patch('smoothschedule.communication.mobile.views.Participant')
    @patch('smoothschedule.communication.mobile.views.Event')
    def test_get_employee_jobs_queryset_no_resources(
        self, mock_event_model, mock_participant_model, mock_resource_model, mock_content_type
    ):
        """Test get_employee_jobs_queryset when user has no linked resources."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 1
        mock_tenant = Mock()

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

        # No resources for this user
        mock_resource_qs = Mock()
        mock_resource_qs.values_list.return_value = []
        mock_resource_model.objects.filter.return_value = mock_resource_qs

        # Only user participant events
        mock_user_participant_qs = Mock()
        mock_user_participant_qs.values_list.return_value = [1, 2]
        mock_participant_model.objects.filter.return_value = mock_user_participant_qs

        mock_event_qs = Mock()
        mock_event_model.objects.filter.return_value = mock_event_qs

        # Act
        result = get_employee_jobs_queryset(mock_user, mock_tenant)

        # Assert
        assert result == mock_event_qs


class TestEmployeeProfileView:
    """Test employee_profile_view endpoint."""

    def test_employee_profile_success(self):
        """Test successful employee profile retrieval."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.get('/api/mobile/me/')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_user.role = User.Role.TENANT_STAFF
        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_user.tenant = mock_tenant
        request.user = mock_user

        # Act
        with patch('smoothschedule.communication.mobile.views.EmployeeProfileSerializer') as mock_serializer:
            mock_serializer_instance = Mock()
            mock_serializer_instance.data = {'id': 1, 'name': 'Test Employee'}
            mock_serializer.return_value = mock_serializer_instance

            response = employee_profile_view(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data == {'id': 1, 'name': 'Test Employee'}
        mock_serializer.assert_called_once_with(mock_user)

    def test_employee_profile_no_tenant(self):
        """Test employee profile returns error when user has no tenant."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.get('/api/mobile/me/')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_user.tenant = None
        request.user = mock_user

        # Act
        response = employee_profile_view(request)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data
        assert 'No business associated' in response.data['error']

    def test_employee_profile_not_field_employee(self):
        """Test employee profile returns error for non-field employees."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.get('/api/mobile/me/')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_user.role = User.Role.CUSTOMER
        mock_tenant = Mock()
        mock_user.tenant = mock_tenant
        request.user = mock_user

        # Act
        response = employee_profile_view(request)

        # Assert
        assert response.status_code == status.HTTP_403_FORBIDDEN
        assert 'error' in response.data
        assert 'field employees only' in response.data['error']


class TestLogoutView:
    """Test logout_view endpoint."""

    @patch('smoothschedule.communication.mobile.views.logger')
    def test_logout_success(self, mock_logger):
        """Test successful logout."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/logout/')

        mock_user = Mock()
        mock_user.id = 1
        mock_user.is_authenticated = True
        request.user = mock_user

        # Act
        response = logout_view(request)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['success'] is True
        assert 'Logged out successfully' in response.data['message']
        mock_logger.info.assert_called_once()

    def test_logout_preserves_token(self):
        """Test that logout doesn't delete the token."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/logout/')

        mock_user = Mock()
        mock_user.is_authenticated = True
        request.user = mock_user

        # Act
        response = logout_view(request)

        # Assert - just verify it returns success without deleting anything
        assert response.status_code == status.HTTP_200_OK
        # Token should NOT be deleted (shared between web and mobile)


class TestJobDetailView:
    """Test job_detail_view endpoint."""

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    def test_job_detail_success(self, mock_get_queryset, mock_get_object, mock_schema_context):
        """Test successful job detail retrieval."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.get('/api/mobile/jobs/1/')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_job = Mock()
        mock_job.id = 1
        mock_job.title = 'Test Job'

        mock_queryset = Mock()
        mock_get_queryset.return_value = mock_queryset
        mock_get_object.return_value = mock_job

        # Act
        with patch('smoothschedule.communication.mobile.views.JobDetailSerializer') as mock_serializer:
            mock_serializer_instance = Mock()
            mock_serializer_instance.data = {'id': 1, 'title': 'Test Job'}
            mock_serializer.return_value = mock_serializer_instance

            response = job_detail_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data == {'id': 1, 'title': 'Test Job'}
        mock_schema_context.assert_called_once_with('demo')
        mock_get_queryset.assert_called_once_with(mock_user, mock_tenant)
        mock_get_object.assert_called_once_with(mock_queryset, id=1)

    def test_job_detail_no_tenant(self):
        """Test job detail returns error when user has no tenant."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.get('/api/mobile/jobs/1/')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_user.tenant = None
        request.user = mock_user

        # Act
        response = job_detail_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data
        assert 'No business associated' in response.data['error']

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    def test_job_detail_not_found(self, mock_get_queryset, mock_get_object, mock_schema_context):
        """Test job detail raises 404 when job not found."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.get('/api/mobile/jobs/999/')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        from django.http import Http404
        mock_get_object.side_effect = Http404()

        # Act/Assert
        # Http404 is raised internally, but get_object_or_404 is mocked
        # We need to let the side effect propagate through
        try:
            job_detail_view(request, job_id=999)
            assert False, "Expected Http404 to be raised"
        except Exception:
            # The mock side_effect is raised
            pass


class TestSetStatusView:
    """Test set_status_view endpoint."""

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.StatusMachine')
    @patch('smoothschedule.communication.mobile.views.SetStatusSerializer')
    def test_set_status_success(
        self, mock_serializer_class, mock_status_machine, mock_get_queryset,
        mock_get_object, mock_schema_context
    ):
        """Test successful status change."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/set_status/', {
            'status': Event.Status.IN_PROGRESS,
            'notes': 'Starting work',
            'latitude': '40.7128',
            'longitude': '-74.0060'
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        # Mock serializer
        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = True
        mock_serializer.validated_data = {
            'status': Event.Status.IN_PROGRESS,
            'notes': 'Starting work',
            'latitude': Decimal('40.7128'),
            'longitude': Decimal('-74.0060')
        }
        mock_serializer_class.return_value = mock_serializer

        # Mock job
        mock_job = Mock()
        mock_job.id = 1
        mock_job.status = Event.Status.IN_PROGRESS
        mock_get_object.return_value = mock_job

        # Mock status machine
        mock_machine = Mock()
        mock_machine.transition.return_value = mock_job
        mock_status_machine.return_value = mock_machine

        # Act
        with patch('smoothschedule.communication.mobile.views.JobDetailSerializer') as mock_detail_serializer:
            mock_detail_instance = Mock()
            mock_detail_instance.data = {'id': 1, 'status': Event.Status.IN_PROGRESS}
            mock_detail_serializer.return_value = mock_detail_instance

            response = set_status_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['success'] is True
        assert 'job' in response.data
        mock_machine.transition.assert_called_once_with(
            event=mock_job,
            new_status=Event.Status.IN_PROGRESS,
            notes='Starting work',
            latitude=Decimal('40.7128'),
            longitude=Decimal('-74.0060'),
            source='mobile_app'
        )

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.StatusMachine')
    @patch('smoothschedule.communication.mobile.views.SetStatusSerializer')
    @patch('smoothschedule.communication.mobile.views.TwilioFieldCallService')
    @patch('smoothschedule.communication.mobile.views.logger')
    def test_set_status_completed_closes_call_session(
        self, mock_logger, mock_call_service_class, mock_serializer_class,
        mock_status_machine, mock_get_queryset, mock_get_object, mock_schema_context
    ):
        """Test that completing a job closes the call session."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/set_status/', {
            'status': Event.Status.COMPLETED
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = True
        mock_serializer.validated_data = {'status': Event.Status.COMPLETED}
        mock_serializer_class.return_value = mock_serializer

        mock_job = Mock()
        mock_job.id = 1
        mock_job.status = Event.Status.COMPLETED
        mock_get_object.return_value = mock_job

        mock_machine = Mock()
        mock_machine.transition.return_value = mock_job
        mock_status_machine.return_value = mock_machine

        mock_call_service = Mock()
        mock_call_service_class.return_value = mock_call_service

        # Act
        with patch('smoothschedule.communication.mobile.views.JobDetailSerializer'):
            response = set_status_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        mock_call_service.close_session.assert_called_once_with(1)

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.StatusMachine')
    @patch('smoothschedule.communication.mobile.views.SetStatusSerializer')
    @patch('smoothschedule.communication.mobile.views.TwilioFieldCallService')
    @patch('smoothschedule.communication.mobile.views.logger')
    def test_set_status_completed_ignores_call_session_error(
        self, mock_logger, mock_call_service_class, mock_serializer_class,
        mock_status_machine, mock_get_queryset, mock_get_object, mock_schema_context
    ):
        """Test that call session errors are logged but don't fail the request."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/set_status/', {
            'status': Event.Status.COMPLETED
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = True
        mock_serializer.validated_data = {'status': Event.Status.COMPLETED}
        mock_serializer_class.return_value = mock_serializer

        mock_job = Mock()
        mock_job.id = 1
        mock_job.status = Event.Status.COMPLETED
        mock_get_object.return_value = mock_job

        mock_machine = Mock()
        mock_machine.transition.return_value = mock_job
        mock_status_machine.return_value = mock_machine

        # Call service raises error
        mock_call_service = Mock()
        mock_call_service.close_session.side_effect = Exception("Twilio error")
        mock_call_service_class.return_value = mock_call_service

        # Act
        with patch('smoothschedule.communication.mobile.views.JobDetailSerializer'):
            response = set_status_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        mock_logger.warning.assert_called_once()

    @patch('smoothschedule.communication.mobile.views.SetStatusSerializer')
    def test_set_status_invalid_data(self, mock_serializer_class):
        """Test set_status returns error with invalid data."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/set_status/', {
            'status': 'INVALID_STATUS'
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = False
        mock_serializer.errors = {'status': ['Invalid status']}
        mock_serializer_class.return_value = mock_serializer

        # Act
        response = set_status_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'status' in response.data

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.StatusMachine')
    @patch('smoothschedule.communication.mobile.views.SetStatusSerializer')
    def test_set_status_transition_error(
        self, mock_serializer_class, mock_status_machine, mock_get_queryset,
        mock_get_object, mock_schema_context
    ):
        """Test set_status handles StatusTransitionError."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/set_status/', {
            'status': Event.Status.COMPLETED
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = True
        mock_serializer.validated_data = {'status': Event.Status.COMPLETED}
        mock_serializer_class.return_value = mock_serializer

        mock_job = Mock()
        mock_get_object.return_value = mock_job

        # Status machine raises transition error
        from smoothschedule.communication.mobile.services.status_machine import StatusTransitionError
        mock_machine = Mock()
        mock_machine.transition.side_effect = StatusTransitionError("Cannot transition from SCHEDULED to COMPLETED")
        mock_status_machine.return_value = mock_machine

        # Act
        response = set_status_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data
        assert 'Cannot transition' in response.data['error']


class TestStartEnRouteView:
    """Test start_en_route_view endpoint."""

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.StatusMachine')
    @patch('smoothschedule.communication.mobile.views.StartEnRouteSerializer')
    def test_start_en_route_success_with_notification(
        self, mock_serializer_class, mock_status_machine, mock_get_queryset,
        mock_get_object, mock_schema_context
    ):
        """Test successful en-route transition with customer notification."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/start_en_route/', {
            'latitude': '40.7128',
            'longitude': '-74.0060',
            'send_customer_notification': True
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = True
        mock_serializer.validated_data = {
            'latitude': Decimal('40.7128'),
            'longitude': Decimal('-74.0060'),
            'send_customer_notification': True
        }
        mock_serializer_class.return_value = mock_serializer

        mock_job = Mock()
        mock_job.id = 1
        mock_job.status = Event.Status.EN_ROUTE
        mock_get_object.return_value = mock_job

        mock_machine = Mock()
        mock_machine.transition.return_value = mock_job
        mock_status_machine.return_value = mock_machine

        # Act
        with patch('smoothschedule.communication.mobile.views.JobDetailSerializer'):
            response = start_en_route_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['success'] is True
        assert response.data['tracking_enabled'] is True
        mock_machine.transition.assert_called_once_with(
            event=mock_job,
            new_status=Event.Status.EN_ROUTE,
            latitude=Decimal('40.7128'),
            longitude=Decimal('-74.0060'),
            source='mobile_app',
            skip_notifications=False
        )

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.StatusMachine')
    @patch('smoothschedule.communication.mobile.views.StartEnRouteSerializer')
    def test_start_en_route_without_notification(
        self, mock_serializer_class, mock_status_machine, mock_get_queryset,
        mock_get_object, mock_schema_context
    ):
        """Test en-route transition without customer notification."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/start_en_route/', {
            'send_customer_notification': False
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = True
        mock_serializer.validated_data = {'send_customer_notification': False}
        mock_serializer_class.return_value = mock_serializer

        mock_job = Mock()
        mock_get_object.return_value = mock_job

        mock_machine = Mock()
        mock_machine.transition.return_value = mock_job
        mock_status_machine.return_value = mock_machine

        # Act
        with patch('smoothschedule.communication.mobile.views.JobDetailSerializer'):
            response = start_en_route_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        # skip_notifications should be True when send_customer_notification is False
        call_kwargs = mock_machine.transition.call_args[1]
        assert call_kwargs['skip_notifications'] is True

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.StatusMachine')
    @patch('smoothschedule.communication.mobile.views.StartEnRouteSerializer')
    def test_start_en_route_default_notification_enabled(
        self, mock_serializer_class, mock_status_machine, mock_get_queryset,
        mock_get_object, mock_schema_context
    ):
        """Test en-route defaults to sending notification when not specified."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/start_en_route/', {}, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = True
        mock_serializer.validated_data = {}  # No send_customer_notification field
        mock_serializer_class.return_value = mock_serializer

        mock_job = Mock()
        mock_get_object.return_value = mock_job

        mock_machine = Mock()
        mock_machine.transition.return_value = mock_job
        mock_status_machine.return_value = mock_machine

        # Act
        with patch('smoothschedule.communication.mobile.views.JobDetailSerializer'):
            response = start_en_route_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        call_kwargs = mock_machine.transition.call_args[1]
        # Default should be to NOT skip notifications (i.e., send them)
        assert call_kwargs['skip_notifications'] is False


class TestRescheduleJobView:
    """Test reschedule_job_view endpoint."""

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.Resource')
    @patch('smoothschedule.communication.mobile.views.RescheduleJobSerializer')
    @patch('smoothschedule.communication.mobile.views.logger')
    def test_reschedule_job_with_start_and_end_time(
        self, mock_logger, mock_serializer_class, mock_resource_model,
        mock_get_queryset, mock_get_object, mock_schema_context
    ):
        """Test rescheduling with new start and end times."""
        # Arrange
        factory = APIRequestFactory()
        new_start = datetime(2024, 1, 15, 10, 0, tzinfo=UTC)
        new_end = datetime(2024, 1, 15, 11, 0, tzinfo=UTC)

        request = factory.post('/api/mobile/jobs/1/reschedule/', {
            'start_time': new_start.isoformat(),
            'end_time': new_end.isoformat()
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_user.id = 1
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = True
        mock_serializer.validated_data = {
            'start_time': new_start,
            'end_time': new_end
        }
        mock_serializer_class.return_value = mock_serializer

        mock_job = Mock()
        mock_job.id = 1
        mock_get_object.return_value = mock_job

        # Mock resource with edit permission
        mock_resource = Mock()
        mock_resource.user_can_edit_schedule = True
        mock_resource_model.objects.filter.return_value = [mock_resource]

        # Act
        with patch('smoothschedule.communication.mobile.views.JobDetailSerializer'):
            response = reschedule_job_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['success'] is True
        assert mock_job.start_time == new_start
        assert mock_job.end_time == new_end
        mock_job.save.assert_called_once()
        mock_logger.info.assert_called_once()

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.Resource')
    @patch('smoothschedule.communication.mobile.views.RescheduleJobSerializer')
    def test_reschedule_job_with_duration(
        self, mock_serializer_class, mock_resource_model, mock_get_queryset,
        mock_get_object, mock_schema_context
    ):
        """Test rescheduling with duration_minutes."""
        # Arrange
        factory = APIRequestFactory()
        new_start = datetime(2024, 1, 15, 10, 0, tzinfo=UTC)

        request = factory.post('/api/mobile/jobs/1/reschedule/', {
            'start_time': new_start.isoformat(),
            'duration_minutes': 90
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = True
        mock_serializer.validated_data = {
            'start_time': new_start,
            'duration_minutes': 90
        }
        mock_serializer_class.return_value = mock_serializer

        mock_job = Mock()
        mock_job.id = 1
        mock_job.start_time = new_start
        mock_get_object.return_value = mock_job

        mock_resource = Mock()
        mock_resource.user_can_edit_schedule = True
        mock_resource_qs = [mock_resource]
        mock_resource_model.objects.filter.return_value = mock_resource_qs

        # Act
        with patch('smoothschedule.communication.mobile.views.JobDetailSerializer'):
            response = reschedule_job_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        expected_end = new_start + timedelta(minutes=90)
        assert mock_job.end_time == expected_end

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.Resource')
    @patch('smoothschedule.communication.mobile.views.RescheduleJobSerializer')
    def test_reschedule_job_no_permission(
        self, mock_serializer_class, mock_resource_model, mock_get_queryset,
        mock_get_object, mock_schema_context
    ):
        """Test reschedule fails when user lacks permission."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/reschedule/', {
            'start_time': datetime.now().isoformat()
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = True
        mock_serializer.validated_data = {'start_time': datetime.now()}
        mock_serializer_class.return_value = mock_serializer

        mock_job = Mock()
        mock_get_object.return_value = mock_job

        # User's resources don't have edit permission
        mock_resource = Mock()
        mock_resource.user_can_edit_schedule = False
        mock_resource_qs = [mock_resource]
        mock_resource_model.objects.filter.return_value = mock_resource_qs

        # Act
        response = reschedule_job_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_403_FORBIDDEN
        assert 'error' in response.data
        assert 'permission' in response.data['error'].lower()

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.Resource')
    @patch('smoothschedule.communication.mobile.views.RescheduleJobSerializer')
    def test_reschedule_job_no_resources(
        self, mock_serializer_class, mock_resource_model, mock_get_queryset,
        mock_get_object, mock_schema_context
    ):
        """Test reschedule fails when user has no linked resources."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/reschedule/', {
            'start_time': datetime.now().isoformat()
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = True
        mock_serializer.validated_data = {'start_time': datetime.now()}
        mock_serializer_class.return_value = mock_serializer

        mock_job = Mock()
        mock_get_object.return_value = mock_job

        # No resources for this user
        mock_resource_model.objects.filter.return_value = []

        # Act
        response = reschedule_job_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_403_FORBIDDEN


class TestLocationUpdateView:
    """Test location_update_view endpoint."""

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.StatusMachine')
    @patch('smoothschedule.communication.mobile.views.EmployeeLocationUpdate')
    @patch('smoothschedule.communication.mobile.views.LocationUpdateSerializer')
    def test_location_update_success(
        self, mock_serializer_class, mock_location_model,
        mock_status_machine, mock_get_queryset, mock_get_object, mock_schema_context
    ):
        """Test successful location update."""
        # Arrange
        factory = APIRequestFactory()
        update_time = timezone.now()
        request = factory.post('/api/mobile/jobs/1/location_update/', {
            'latitude': '40.7128',
            'longitude': '-74.0060',
            'accuracy': 10.5,
            'timestamp': update_time.isoformat()
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = True
        mock_serializer.validated_data = {
            'latitude': Decimal('40.7128'),
            'longitude': Decimal('-74.0060'),
            'accuracy': 10.5,
            'timestamp': update_time
        }
        mock_serializer_class.return_value = mock_serializer

        mock_job = Mock()
        mock_job.id = 1
        mock_job.title = 'Test Job'
        mock_job.status = Event.Status.EN_ROUTE
        mock_job.get_status_display.return_value = 'En Route'
        mock_get_object.return_value = mock_job

        # Mock status machine
        mock_status_machine.TRACKING_STATUSES = {Event.Status.EN_ROUTE, Event.Status.IN_PROGRESS}

        # Mock location creation
        mock_location = Mock()
        mock_location.latitude = Decimal('40.7128')
        mock_location.longitude = Decimal('-74.0060')
        mock_location.accuracy = 10.5
        mock_location.heading = None
        mock_location.speed = None
        mock_location.timestamp = update_time
        mock_location_model.objects.create.return_value = mock_location

        # Mock the broadcast section entirely by skipping the loop
        with patch('smoothschedule.scheduling.schedule.models.Resource.objects.filter') as mock_filter:
            mock_filter.return_value = []  # No resources, skip broadcasting

            # Act
            response = location_update_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['success'] is True
        assert response.data['should_continue_tracking'] is True
        mock_location_model.objects.create.assert_called_once()

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.StatusMachine')
    @patch('smoothschedule.communication.mobile.views.LocationUpdateSerializer')
    def test_location_update_not_tracking_status(
        self, mock_serializer_class, mock_status_machine, mock_get_queryset,
        mock_get_object, mock_schema_context
    ):
        """Test location update returns false when job not in tracking status."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/location_update/', {
            'latitude': '40.7128',
            'longitude': '-74.0060',
            'timestamp': timezone.now().isoformat()
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = True
        mock_serializer.validated_data = {
            'latitude': Decimal('40.7128'),
            'longitude': Decimal('-74.0060'),
            'timestamp': timezone.now()
        }
        mock_serializer_class.return_value = mock_serializer

        mock_job = Mock()
        mock_job.id = 1
        mock_job.status = Event.Status.COMPLETED
        mock_get_object.return_value = mock_job

        mock_status_machine.TRACKING_STATUSES = {Event.Status.EN_ROUTE, Event.Status.IN_PROGRESS}

        # Act
        response = location_update_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['success'] is False
        assert response.data['should_continue_tracking'] is False
        assert 'message' in response.data

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.StatusMachine')
    @patch('smoothschedule.communication.mobile.views.EmployeeLocationUpdate')
    @patch('smoothschedule.communication.mobile.views.Resource')
    @patch('smoothschedule.communication.mobile.views.LocationUpdateSerializer')
    def test_location_update_invalid_data(
        self, mock_serializer_class, mock_resource_model, mock_location_model,
        mock_status_machine, mock_get_queryset, mock_get_object, mock_schema_context
    ):
        """Test location update with invalid data."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/location_update/', {
            'latitude': 'invalid',
            'longitude': 'invalid'
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = False
        mock_serializer.errors = {'latitude': ['Invalid decimal'], 'longitude': ['Invalid decimal']}
        mock_serializer_class.return_value = mock_serializer

        # Act
        response = location_update_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'latitude' in response.data or 'longitude' in response.data


class TestLocationRouteView:
    """Test location_route_view endpoint."""

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.EmployeeLocationUpdate')
    def test_location_route_success(
        self, mock_location_model, mock_get_queryset, mock_get_object, mock_schema_context
    ):
        """Test successful retrieval of location route."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.get('/api/mobile/jobs/1/route/')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_job = Mock()
        mock_job.id = 1
        mock_get_object.return_value = mock_job

        # Mock route data
        mock_route = [
            {
                'latitude': Decimal('40.7128'),
                'longitude': Decimal('-74.0060'),
                'timestamp': timezone.now(),
                'accuracy': 10.0
            },
            {
                'latitude': Decimal('40.7138'),
                'longitude': Decimal('-74.0070'),
                'timestamp': timezone.now(),
                'accuracy': 12.0
            }
        ]
        mock_location_model.get_route_for_event.return_value = mock_route

        # Act
        response = location_route_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['job_id'] == 1
        assert len(response.data['route']) == 2
        assert response.data['point_count'] == 2
        # Verify Decimals were converted to floats
        assert isinstance(response.data['route'][0]['latitude'], float)
        assert isinstance(response.data['route'][0]['longitude'], float)
        mock_location_model.get_route_for_event.assert_called_once_with(
            tenant_id=1,
            event_id=1,
            limit=200
        )

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.EmployeeLocationUpdate')
    def test_location_route_empty(
        self, mock_location_model, mock_get_queryset, mock_get_object, mock_schema_context
    ):
        """Test location route with no location data."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.get('/api/mobile/jobs/1/route/')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_job = Mock()
        mock_job.id = 1
        mock_get_object.return_value = mock_job

        mock_location_model.get_route_for_event.return_value = []

        # Act
        response = location_route_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['point_count'] == 0
        assert response.data['route'] == []


class TestCallCustomerView:
    """Test call_customer_view endpoint."""

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.TwilioFieldCallService')
    def test_call_customer_success(
        self, mock_call_service_class, mock_get_queryset, mock_get_object, mock_schema_context
    ):
        """Test successful call initiation."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/call_customer/')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_job = Mock()
        mock_job.id = 1
        mock_get_object.return_value = mock_job

        # Mock call service
        mock_call_service = Mock()
        mock_call_result = {
            'call_sid': 'CA123',
            'call_log_id': 1,
            'proxy_number': '+15551234567',
            'status': 'initiated',
            'message': 'Call initiated'
        }
        mock_call_service.initiate_call.return_value = mock_call_result
        mock_call_service_class.return_value = mock_call_service

        # Act
        response = call_customer_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data == mock_call_result
        mock_call_service.initiate_call.assert_called_once_with(
            event_id=1,
            employee=mock_user
        )

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.TwilioFieldCallService')
    def test_call_customer_twilio_error(
        self, mock_call_service_class, mock_get_queryset, mock_get_object, mock_schema_context
    ):
        """Test call handles TwilioFieldCallError."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/call_customer/')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_job = Mock()
        mock_get_object.return_value = mock_job

        from smoothschedule.communication.mobile.services.twilio_calls import TwilioFieldCallError
        mock_call_service = Mock()
        mock_call_service.initiate_call.side_effect = TwilioFieldCallError("Customer has no phone")
        mock_call_service_class.return_value = mock_call_service

        # Act
        response = call_customer_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data
        assert 'Customer has no phone' in response.data['error']

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.TwilioFieldCallService')
    @patch('smoothschedule.communication.mobile.views.logger')
    def test_call_customer_generic_error(
        self, mock_logger, mock_call_service_class, mock_get_queryset,
        mock_get_object, mock_schema_context
    ):
        """Test call handles generic exceptions."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/call_customer/')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_job = Mock()
        mock_get_object.return_value = mock_job

        mock_call_service = Mock()
        mock_call_service.initiate_call.side_effect = Exception("Unexpected error")
        mock_call_service_class.return_value = mock_call_service

        # Act
        response = call_customer_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
        assert 'error' in response.data
        assert 'Failed to initiate call' in response.data['error']
        mock_logger.exception.assert_called_once()


class TestSendSMSView:
    """Test send_sms_view endpoint."""

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.TwilioFieldCallService')
    @patch('smoothschedule.communication.mobile.views.SendSMSSerializer')
    def test_send_sms_success(
        self, mock_serializer_class, mock_call_service_class, mock_get_queryset,
        mock_get_object, mock_schema_context
    ):
        """Test successful SMS sending."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/send_sms/', {
            'message': 'On my way!'
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = True
        mock_serializer.validated_data = {'message': 'On my way!'}
        mock_serializer_class.return_value = mock_serializer

        mock_job = Mock()
        mock_job.id = 1
        mock_get_object.return_value = mock_job

        mock_call_service = Mock()
        mock_sms_result = {
            'message_sid': 'SM123',
            'call_log_id': 1,
            'status': 'sent'
        }
        mock_call_service.send_sms.return_value = mock_sms_result
        mock_call_service_class.return_value = mock_call_service

        # Act
        response = send_sms_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data == mock_sms_result
        mock_call_service.send_sms.assert_called_once_with(
            event_id=1,
            employee=mock_user,
            message='On my way!'
        )

    @patch('smoothschedule.communication.mobile.views.SendSMSSerializer')
    def test_send_sms_invalid_data(self, mock_serializer_class):
        """Test send_sms returns error with invalid data."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/send_sms/', {
            'message': ''
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = False
        mock_serializer.errors = {'message': ['This field may not be blank.']}
        mock_serializer_class.return_value = mock_serializer

        # Act
        response = send_sms_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'message' in response.data

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.TwilioFieldCallService')
    @patch('smoothschedule.communication.mobile.views.SendSMSSerializer')
    @patch('smoothschedule.communication.mobile.views.logger')
    def test_send_sms_generic_error(
        self, mock_logger, mock_serializer_class, mock_call_service_class,
        mock_get_queryset, mock_get_object, mock_schema_context
    ):
        """Test send_sms handles generic exceptions."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/jobs/1/send_sms/', {
            'message': 'Test'
        }, format='json')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_serializer = Mock()
        mock_serializer.is_valid.return_value = True
        mock_serializer.validated_data = {'message': 'Test'}
        mock_serializer_class.return_value = mock_serializer

        mock_job = Mock()
        mock_get_object.return_value = mock_job

        mock_call_service = Mock()
        mock_call_service.send_sms.side_effect = Exception("Network error")
        mock_call_service_class.return_value = mock_call_service

        # Act
        response = send_sms_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
        assert 'Failed to send SMS' in response.data['error']
        mock_logger.exception.assert_called_once()


class TestCallHistoryView:
    """Test call_history_view endpoint."""

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.FieldCallLog')
    def test_call_history_success(
        self, mock_call_log_model, mock_get_queryset, mock_get_object, mock_schema_context
    ):
        """Test successful call history retrieval."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.get('/api/mobile/jobs/1/call_history/')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_job = Mock()
        mock_job.id = 1
        mock_get_object.return_value = mock_job

        # Mock call logs
        mock_log1 = Mock()
        mock_log1.id = 1
        mock_log1.call_type = FieldCallLog.CallType.VOICE
        mock_log2 = Mock()
        mock_log2.id = 2
        mock_log2.call_type = FieldCallLog.CallType.SMS

        # Mock the queryset chain
        mock_logs = [mock_log1, mock_log2]
        mock_order_by = Mock()
        mock_order_by.__getitem__ = Mock(return_value=mock_logs)
        mock_select_related = Mock()
        mock_select_related.order_by.return_value = mock_order_by
        mock_call_log_model.objects.filter.return_value.select_related.return_value = mock_select_related

        # Act
        with patch('smoothschedule.communication.mobile.views.CallHistorySerializer') as mock_serializer:
            mock_serializer_instance = Mock()
            mock_serializer_instance.data = [
                {'id': 1, 'call_type': 'voice'},
                {'id': 2, 'call_type': 'sms'}
            ]
            mock_serializer.return_value = mock_serializer_instance

            response = call_history_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert response.data['job_id'] == 1
        assert len(response.data['history']) == 2
        mock_call_log_model.objects.filter.assert_called_once_with(
            tenant=mock_tenant,
            event_id=1
        )

    @patch('smoothschedule.communication.mobile.views.schema_context')
    @patch('smoothschedule.communication.mobile.views.get_object_or_404')
    @patch('smoothschedule.communication.mobile.views.get_employee_jobs_queryset')
    @patch('smoothschedule.communication.mobile.views.FieldCallLog')
    def test_call_history_empty(
        self, mock_call_log_model, mock_get_queryset, mock_get_object, mock_schema_context
    ):
        """Test call history with no logs."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.get('/api/mobile/jobs/1/call_history/')

        mock_user = Mock()
        mock_user.is_authenticated = True
        mock_tenant = Mock()
        mock_tenant.id = 1
        mock_tenant.schema_name = 'demo'
        mock_user.tenant = mock_tenant
        request.user = mock_user

        mock_job = Mock()
        mock_job.id = 1
        mock_get_object.return_value = mock_job

        # Mock empty queryset chain
        mock_order_by = Mock()
        mock_order_by.__getitem__ = Mock(return_value=[])
        mock_select_related = Mock()
        mock_select_related.order_by.return_value = mock_order_by
        mock_call_log_model.objects.filter.return_value.select_related.return_value = mock_select_related

        # Act
        with patch('smoothschedule.communication.mobile.views.CallHistorySerializer') as mock_serializer:
            mock_serializer_instance = Mock()
            mock_serializer_instance.data = []
            mock_serializer.return_value = mock_serializer_instance

            response = call_history_view(request, job_id=1)

        # Assert
        assert response.status_code == status.HTTP_200_OK
        assert len(response.data['history']) == 0


class TestTwilioWebhooks:
    """Test Twilio webhook endpoints."""

    @patch('smoothschedule.communication.mobile.services.twilio_calls.handle_incoming_call')
    def test_twilio_voice_webhook(self, mock_handle_call):
        """Test Twilio voice webhook."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/twilio/voice/session123/', {
            'From': '+15551234567',
            'To': '+15559876543'
        })

        mock_twiml = '<?xml version="1.0"?><Response><Dial>+15551111111</Dial></Response>'
        mock_handle_call.return_value = mock_twiml

        # Act
        response = twilio_voice_webhook(request, session_id='session123')

        # Assert
        assert response.status_code == 200
        assert response['Content-Type'] == 'application/xml'
        assert mock_twiml in response.content.decode()
        mock_handle_call.assert_called_once_with('session123', '+15551234567')

    def test_twilio_voice_status_webhook_completed(self):
        """Test voice status webhook with completed status."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/twilio/voice-status/session123/', {
            'CallSid': 'CA123',
            'CallStatus': 'completed',
            'CallDuration': '120'
        })

        mock_call_log = Mock()
        mock_call_log.masked_session = None

        with patch('smoothschedule.communication.mobile.views.FieldCallLog') as mock_call_log_model, \
             patch('smoothschedule.communication.mobile.views.timezone') as mock_tz:
            mock_call_log_model.objects.get.return_value = mock_call_log
            mock_call_log_model.DoesNotExist = Exception
            mock_call_log_model.Status = FieldCallLog.Status

            mock_now = Mock()
            mock_tz.now.return_value = mock_now

            # Act
            response = twilio_voice_status_webhook(request, session_id='session123')

        # Assert
        assert response.status_code == 200
        assert mock_call_log.status == FieldCallLog.Status.COMPLETED
        assert mock_call_log.duration_seconds == 120
        assert mock_call_log.ended_at == mock_now
        mock_call_log.save.assert_called_once()

    def test_twilio_voice_status_webhook_in_progress(self):
        """Test voice status webhook with in-progress status."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/twilio/voice-status/session123/', {
            'CallSid': 'CA123',
            'CallStatus': 'in-progress',
            'CallDuration': '0'
        })

        mock_call_log = Mock()
        mock_call_log.masked_session = None

        with patch('smoothschedule.communication.mobile.views.FieldCallLog') as mock_call_log_model, \
             patch('smoothschedule.communication.mobile.views.timezone') as mock_tz:
            mock_call_log_model.objects.get.return_value = mock_call_log
            mock_call_log_model.DoesNotExist = Exception
            mock_call_log_model.Status = FieldCallLog.Status

            mock_now = Mock()
            mock_tz.now.return_value = mock_now

            # Act
            response = twilio_voice_status_webhook(request, session_id='session123')

        # Assert
        assert response.status_code == 200
        assert mock_call_log.status == FieldCallLog.Status.IN_PROGRESS
        assert mock_call_log.answered_at == mock_now
        mock_call_log.save.assert_called_once()

    def test_twilio_voice_status_webhook_updates_session(self):
        """Test voice status webhook updates masked session usage."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/twilio/voice-status/session123/', {
            'CallSid': 'CA123',
            'CallStatus': 'completed',
            'CallDuration': '180'
        })

        with patch('smoothschedule.communication.mobile.views.FieldCallLog') as mock_call_log_model, \
             patch('smoothschedule.communication.mobile.views.timezone'):
            mock_session = Mock()
            mock_session.voice_seconds = 100

            mock_call_log = Mock()
            mock_call_log.masked_session = mock_session
            mock_call_log_model.objects.get.return_value = mock_call_log
            mock_call_log_model.DoesNotExist = Exception
            mock_call_log_model.Status = FieldCallLog.Status

            # Act
            response = twilio_voice_status_webhook(request, session_id='session123')

        # Assert
        assert response.status_code == 200
        assert mock_session.voice_seconds == 280  # 100 + 180
        mock_session.save.assert_called_once_with(update_fields=['voice_seconds', 'updated_at'])

    @patch('smoothschedule.communication.mobile.views.logger')
    def test_twilio_voice_status_webhook_not_found(self, mock_logger):
        """Test voice status webhook handles missing call log."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/twilio/voice-status/session123/', {
            'CallSid': 'CA999',
            'CallStatus': 'completed'
        })

        with patch('smoothschedule.communication.mobile.views.FieldCallLog') as mock_call_log_model:
            # Create a proper DoesNotExist exception class
            class DoesNotExist(Exception):
                pass

            mock_call_log_model.DoesNotExist = DoesNotExist
            mock_call_log_model.objects.get.side_effect = DoesNotExist()

            # Act
            response = twilio_voice_status_webhook(request, session_id='session123')

        # Assert
        assert response.status_code == 200
        mock_logger.warning.assert_called_once()

    @patch('smoothschedule.communication.mobile.services.twilio_calls.handle_incoming_sms')
    def test_twilio_sms_webhook(self, mock_handle_sms):
        """Test Twilio SMS webhook."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/twilio/sms/session123/', {
            'From': '+15551234567',
            'Body': 'Running 5 minutes late'
        })

        # Act
        response = twilio_sms_webhook(request, session_id='session123')

        # Assert
        assert response.status_code == 200
        assert response['Content-Type'] == 'application/xml'
        assert b'<Response></Response>' in response.content
        mock_handle_sms.assert_called_once_with(
            'session123',
            '+15551234567',
            'Running 5 minutes late'
        )

    @patch('smoothschedule.communication.mobile.views.logger')
    def test_twilio_sms_status_webhook(self, mock_logger):
        """Test Twilio SMS status webhook."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/twilio/sms-status/session123/', {
            'MessageSid': 'SM123',
            'MessageStatus': 'delivered'
        })

        # Act
        response = twilio_sms_status_webhook(request, session_id='session123')

        # Assert
        assert response.status_code == 200
        mock_logger.debug.assert_called_once()

    @patch('smoothschedule.communication.mobile.views.logger')
    def test_twilio_sms_status_webhook_no_data(self, mock_logger):
        """Test SMS status webhook with missing data."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/twilio/sms-status/session123/', {})

        # Act
        response = twilio_sms_status_webhook(request, session_id='session123')

        # Assert
        assert response.status_code == 200
        mock_logger.debug.assert_not_called()

    def test_twilio_voice_status_webhook_no_call_sid(self):
        """Test voice status webhook with missing CallSid."""
        # Arrange
        factory = APIRequestFactory()
        request = factory.post('/api/mobile/twilio/voice-status/session123/', {
            'CallStatus': 'completed'
        })

        with patch('smoothschedule.communication.mobile.views.FieldCallLog') as mock_call_log_model:
            mock_call_log_model.DoesNotExist = Exception

            # Act
            response = twilio_voice_status_webhook(request, session_id='session123')

        # Assert
        assert response.status_code == 200
        mock_call_log_model.objects.get.assert_not_called()

    def test_twilio_voice_status_webhook_maps_all_statuses(self):
        """Test voice status webhook correctly maps all Twilio statuses."""
        # Test each status mapping
        status_mappings = [
            ('queued', FieldCallLog.Status.INITIATED),
            ('ringing', FieldCallLog.Status.RINGING),
            ('in-progress', FieldCallLog.Status.IN_PROGRESS),
            ('completed', FieldCallLog.Status.COMPLETED),
            ('busy', FieldCallLog.Status.BUSY),
            ('no-answer', FieldCallLog.Status.NO_ANSWER),
            ('failed', FieldCallLog.Status.FAILED),
            ('canceled', FieldCallLog.Status.CANCELED),
        ]

        for twilio_status, expected_status in status_mappings:
            factory = APIRequestFactory()
            request = factory.post('/api/mobile/twilio/voice-status/session123/', {
                'CallSid': 'CA123',
                'CallStatus': twilio_status,
                'CallDuration': '0'
            })

            with patch('smoothschedule.communication.mobile.views.FieldCallLog') as mock_call_log_model, \
                 patch('smoothschedule.communication.mobile.views.timezone'):
                mock_call_log = Mock()
                mock_call_log.masked_session = None
                mock_call_log_model.objects.get.return_value = mock_call_log
                mock_call_log_model.DoesNotExist = Exception
                # Create Status class with expected attributes
                mock_call_log_model.Status = FieldCallLog.Status

                response = twilio_voice_status_webhook(request, session_id='session123')

                assert response.status_code == 200
                assert mock_call_log.status == expected_status
