"""
Comprehensive unit tests for contract views.
Tests all ViewSets, actions, permissions, and business logic using mocks.
Does NOT use @pytest.mark.django_db - uses mocked requests and authentication.
"""
import hashlib
from datetime import datetime, timedelta, timezone as dt_timezone
from decimal import Decimal
from io import BytesIO
from unittest.mock import Mock, patch, MagicMock, call
from django.utils import timezone
from rest_framework import status
import pytest

from smoothschedule.scheduling.contracts.views import (
    HasContractsPermission,
    get_client_ip,
    ContractTemplateViewSet,
    ServiceContractRequirementViewSet,
    ContractViewSet,
    PublicContractSigningView,
)
from smoothschedule.scheduling.contracts.models import (
    ContractTemplate,
    ServiceContractRequirement,
    Contract,
    ContractSignature,
)


def create_mock_request(method='GET', query_params=None, data=None, meta=None, user=None):
    """Helper to create a mock DRF request with proper attributes."""
    request = Mock()
    request.method = method
    request.query_params = query_params or {}
    request.data = data or {}
    request.META = meta or {}
    request.user = user or Mock(id=1, email='user@example.com')
    request.scheme = 'http'
    request.get_host = Mock(return_value='example.com')
    return request


# =============================================================================
# Tests for HasContractsPermission
# =============================================================================

class TestHasContractsPermission:
    """Test the HasContractsPermission class."""

    def test_permission_denied_for_unauthenticated_user(self):
        """Test permission denied when user is not authenticated."""
        permission = HasContractsPermission()
        request = Mock(user=Mock(is_authenticated=False))
        view = Mock()

        assert permission.has_permission(request, view) is False

    def test_permission_denied_for_none_user(self):
        """Test permission denied when user is None."""
        permission = HasContractsPermission()
        request = Mock(user=None)
        view = Mock()

        assert permission.has_permission(request, view) is False

    def test_permission_granted_for_superuser(self):
        """Test permission granted for superuser role."""
        permission = HasContractsPermission()
        request = Mock(user=Mock(is_authenticated=True, role='superuser'))
        view = Mock()

        assert permission.has_permission(request, view) is True

    def test_permission_granted_for_platform_manager(self):
        """Test permission granted for platform_manager role."""
        permission = HasContractsPermission()
        request = Mock(user=Mock(is_authenticated=True, role='platform_manager'))
        view = Mock()

        assert permission.has_permission(request, view) is True

    def test_permission_granted_for_platform_support(self):
        """Test permission granted for platform_support role."""
        permission = HasContractsPermission()
        request = Mock(user=Mock(is_authenticated=True, role='platform_support'))
        view = Mock()

        assert permission.has_permission(request, view) is True

    def test_permission_denied_when_no_tenant(self):
        """Test permission denied when user has no tenant."""
        permission = HasContractsPermission()
        user = Mock(is_authenticated=True, role='owner')
        user.tenant = None
        request = Mock(user=user)
        view = Mock()

        assert permission.has_permission(request, view) is False

    def test_permission_denied_when_contracts_not_enabled(self):
        """Test permission denied when subscription plan does not have contracts enabled."""
        permission = HasContractsPermission()
        subscription_plan = Mock(contracts_enabled=False)
        tenant = Mock(subscription_plan=subscription_plan)
        user = Mock(is_authenticated=True, role='owner', tenant=tenant)
        request = Mock(user=user)
        view = Mock()

        assert permission.has_permission(request, view) is False

    def test_permission_granted_when_contracts_enabled(self):
        """Test permission granted when subscription plan has contracts enabled."""
        permission = HasContractsPermission()
        subscription_plan = Mock(contracts_enabled=True)
        tenant = Mock(subscription_plan=subscription_plan)
        user = Mock(is_authenticated=True, role='owner', tenant=tenant)
        request = Mock(user=user)
        view = Mock()

        assert permission.has_permission(request, view) is True

    def test_permission_denied_when_no_subscription_plan(self):
        """Test permission denied when tenant has no subscription plan."""
        permission = HasContractsPermission()
        tenant = Mock()
        tenant.subscription_plan = None
        user = Mock(is_authenticated=True, role='owner', tenant=tenant)
        request = Mock(user=user)
        view = Mock()

        assert permission.has_permission(request, view) is False

    def test_permission_denied_when_user_has_no_role_attribute(self):
        """Test permission handling when user has no role attribute."""
        permission = HasContractsPermission()
        user = Mock(is_authenticated=True, spec=['is_authenticated'])
        delattr(user, 'role')
        user.tenant = None
        request = Mock(user=user)
        view = Mock()

        assert permission.has_permission(request, view) is False


# =============================================================================
# Tests for get_client_ip utility function
# =============================================================================

class TestGetClientIp:
    """Test the get_client_ip utility function."""

    def test_extracts_ip_from_x_forwarded_for_single(self):
        """Test extracting IP from X-Forwarded-For with single IP."""
        request = Mock(META={'HTTP_X_FORWARDED_FOR': '203.0.113.1'})
        ip = get_client_ip(request)
        assert ip == '203.0.113.1'

    def test_extracts_first_ip_from_x_forwarded_for_multiple(self):
        """Test extracting first IP from X-Forwarded-For with multiple IPs."""
        request = Mock(META={'HTTP_X_FORWARDED_FOR': '203.0.113.1, 198.51.100.2, 192.0.2.3'})
        ip = get_client_ip(request)
        assert ip == '203.0.113.1'

    def test_extracts_ip_from_x_forwarded_for_with_spaces(self):
        """Test extracting IP from X-Forwarded-For with extra spaces."""
        request = Mock(META={'HTTP_X_FORWARDED_FOR': '  203.0.113.1  ,  198.51.100.2  '})
        ip = get_client_ip(request)
        assert ip == '203.0.113.1'

    def test_falls_back_to_remote_addr(self):
        """Test falling back to REMOTE_ADDR when X-Forwarded-For is absent."""
        request = Mock(META={'REMOTE_ADDR': '192.0.2.100'})
        ip = get_client_ip(request)
        assert ip == '192.0.2.100'

    def test_returns_none_when_no_ip_available(self):
        """Test returns None when neither header is present."""
        request = Mock(META={})
        ip = get_client_ip(request)
        assert ip is None


# =============================================================================
# Tests for ContractTemplateViewSet
# =============================================================================

class TestContractTemplateViewSet:
    """Test the ContractTemplateViewSet."""

    def test_get_serializer_class_returns_list_serializer_for_list_action(self):
        """Test get_serializer_class returns list serializer for list action."""
        viewset = ContractTemplateViewSet()
        viewset.action = 'list'

        from smoothschedule.scheduling.contracts.serializers import ContractTemplateListSerializer
        assert viewset.get_serializer_class() == ContractTemplateListSerializer

    def test_get_serializer_class_returns_default_serializer_for_other_actions(self):
        """Test get_serializer_class returns default serializer for non-list actions."""
        viewset = ContractTemplateViewSet()
        viewset.action = 'retrieve'

        from smoothschedule.scheduling.contracts.serializers import ContractTemplateSerializer
        assert viewset.get_serializer_class() == ContractTemplateSerializer

    def test_get_queryset_filters_by_status(self):
        """Test get_queryset filters by status query param."""
        request = create_mock_request(query_params={'status': 'ACTIVE'})
        viewset = ContractTemplateViewSet()
        viewset.request = request

        mock_qs = Mock()
        mock_filtered = Mock()
        mock_ordered = Mock()
        mock_qs.filter.return_value = mock_filtered
        mock_filtered.order_by.return_value = mock_ordered

        with patch('rest_framework.viewsets.ModelViewSet.get_queryset', return_value=mock_qs):
            result = viewset.get_queryset()

        mock_qs.filter.assert_called_once_with(status='ACTIVE')
        mock_filtered.order_by.assert_called_once_with('name')

    def test_get_queryset_no_filter_when_status_not_provided(self):
        """Test get_queryset does not filter when status param is absent."""
        request = create_mock_request()
        viewset = ContractTemplateViewSet()
        viewset.request = request

        mock_qs = Mock()
        mock_ordered = Mock()
        mock_qs.order_by.return_value = mock_ordered

        with patch('rest_framework.viewsets.ModelViewSet.get_queryset', return_value=mock_qs):
            result = viewset.get_queryset()

        mock_qs.filter.assert_not_called()
        mock_qs.order_by.assert_called_once_with('name')

    def test_perform_create_sets_created_by(self):
        """Test perform_create sets created_by to current user."""
        user = Mock(id=1, email='user@example.com')
        request = create_mock_request(user=user)
        viewset = ContractTemplateViewSet()
        viewset.request = request

        serializer = Mock()
        viewset.perform_create(serializer)

        serializer.save.assert_called_once_with(created_by=user)

    def test_duplicate_action_creates_copy(self):
        """Test duplicate action creates a copy of the template."""
        user = Mock(id=1, email='user@example.com')
        request = create_mock_request(method='POST', user=user)

        original_template = Mock(spec=['id', 'name', 'description', 'content', 'scope', 'expires_after_days'])
        original_template.id = 1
        original_template.name = 'Original Template'
        original_template.description = 'Original description'
        original_template.content = 'Original content'
        original_template.scope = ContractTemplate.Scope.CUSTOMER
        original_template.expires_after_days = 30

        new_template = Mock(spec=['id', 'name'])
        new_template.id = 2
        new_template.name = 'Original Template (Copy)'

        viewset = ContractTemplateViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=original_template)

        with patch.object(ContractTemplate.objects, 'create', return_value=new_template) as mock_create:
            with patch('smoothschedule.scheduling.contracts.views.ContractTemplateSerializer') as mock_serializer_class:
                mock_serializer_instance = Mock()
                mock_serializer_instance.data = {'id': 2, 'name': 'Original Template (Copy)'}
                mock_serializer_class.return_value = mock_serializer_instance

                response = viewset.duplicate(request, pk=1)

                mock_create.assert_called_once_with(
                    name='Original Template (Copy)',
                    description='Original description',
                    content='Original content',
                    scope=ContractTemplate.Scope.CUSTOMER,
                    status=ContractTemplate.Status.DRAFT,
                    expires_after_days=30,
                    created_by=user,
                )

        assert response.status_code == status.HTTP_201_CREATED
        assert response.data == {'id': 2, 'name': 'Original Template (Copy)'}

    def test_new_version_action_increments_version(self):
        """Test new_version action increments version and updates fields."""
        request = create_mock_request(
            method='POST',
            data={
                'version_notes': 'Updated terms',
                'content': 'New content',
                'name': 'Updated Name',
                'description': 'Updated description'
            }
        )

        template = Mock(id=1, version=1)

        viewset = ContractTemplateViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=template)

        with patch('smoothschedule.scheduling.contracts.views.ContractTemplateSerializer') as mock_serializer_class:
            mock_serializer_instance = Mock()
            mock_serializer_instance.data = {'id': 1, 'version': 2}
            mock_serializer_class.return_value = mock_serializer_instance

            response = viewset.new_version(request, pk=1)

            assert template.version == 2
            assert template.version_notes == 'Updated terms'
            assert template.content == 'New content'
            assert template.name == 'Updated Name'
            assert template.description == 'Updated description'
            template.save.assert_called_once()
            assert response.status_code == status.HTTP_200_OK

    def test_new_version_action_with_partial_data(self):
        """Test new_version action with only version_notes."""
        request = create_mock_request(
            method='POST',
            data={'version_notes': 'Minor fix'}
        )

        template = Mock(spec=['id', 'version', 'content', 'name', 'description', 'version_notes', 'save'])
        template.id = 1
        template.version = 1
        template.content = 'Original'
        template.name = 'Original'
        template.description = 'Original'

        viewset = ContractTemplateViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=template)

        with patch('smoothschedule.scheduling.contracts.views.ContractTemplateSerializer') as mock_serializer_class:
            mock_serializer_instance = Mock()
            mock_serializer_instance.data = {'id': 1, 'version': 2}
            mock_serializer_class.return_value = mock_serializer_instance

            response = viewset.new_version(request, pk=1)

            assert template.version == 2
            assert template.version_notes == 'Minor fix'
            assert template.content == 'Original'
            assert template.name == 'Original'
            assert template.description == 'Original'

    def test_activate_action_sets_status_to_active(self):
        """Test activate action sets template status to ACTIVE."""
        request = create_mock_request(method='POST')
        template = Mock(id=1, status=ContractTemplate.Status.DRAFT)

        viewset = ContractTemplateViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=template)

        response = viewset.activate(request, pk=1)

        assert template.status == ContractTemplate.Status.ACTIVE
        template.save.assert_called_once_with(update_fields=['status', 'updated_at'])
        assert response.status_code == status.HTTP_200_OK
        assert response.data == {'success': True}

    def test_archive_action_sets_status_to_archived(self):
        """Test archive action sets template status to ARCHIVED."""
        request = create_mock_request(method='POST')
        template = Mock(id=1, status=ContractTemplate.Status.ACTIVE)

        viewset = ContractTemplateViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=template)

        response = viewset.archive(request, pk=1)

        assert template.status == ContractTemplate.Status.ARCHIVED
        template.save.assert_called_once_with(update_fields=['status', 'updated_at'])
        assert response.status_code == status.HTTP_200_OK
        assert response.data == {'success': True}

    def test_preview_pdf_returns_503_when_weasyprint_unavailable(self):
        """Test preview_pdf returns 503 when WeasyPrint is not available."""
        request = create_mock_request()
        template = Mock(id=1, name='Test Template')

        viewset = ContractTemplateViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=template)

        with patch('smoothschedule.scheduling.contracts.pdf_service.WEASYPRINT_AVAILABLE', False):
            response = viewset.preview_pdf(request, pk=1)

        assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
        assert response.data == {'error': 'PDF generation not available'}

    def test_preview_pdf_generates_and_returns_pdf(self):
        """Test preview_pdf generates and returns PDF successfully."""
        user = Mock(id=1, email='user@example.com')
        request = create_mock_request(user=user)
        template = Mock(spec=['id', 'name'])
        template.id = 1
        template.name = 'Test Template'

        pdf_bytes = b'%PDF-1.4 fake pdf content'

        viewset = ContractTemplateViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=template)

        with patch('smoothschedule.scheduling.contracts.pdf_service.WEASYPRINT_AVAILABLE', True):
            with patch('smoothschedule.scheduling.contracts.pdf_service.ContractPDFService') as mock_pdf_service:
                mock_pdf_service.generate_template_preview.return_value = pdf_bytes
                response = viewset.preview_pdf(request, pk=1)

                mock_pdf_service.generate_template_preview.assert_called_once_with(template, user)

        assert response.status_code == 200
        assert response['Content-Type'] == 'application/pdf'
        assert response['Content-Disposition'] == 'inline; filename="Test Template_preview.pdf"'
        assert response.content == pdf_bytes

    def test_preview_pdf_handles_generation_error(self):
        """Test preview_pdf handles PDF generation errors."""
        request = create_mock_request()
        template = Mock(id=1, name='Test Template')

        viewset = ContractTemplateViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=template)

        with patch('smoothschedule.scheduling.contracts.pdf_service.WEASYPRINT_AVAILABLE', True):
            with patch('smoothschedule.scheduling.contracts.pdf_service.ContractPDFService') as mock_pdf_service:
                mock_pdf_service.generate_template_preview.side_effect = Exception('PDF generation failed')
                response = viewset.preview_pdf(request, pk=1)

        assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
        assert 'error' in response.data
        assert response.data['error'] == 'PDF generation failed'


# =============================================================================
# Tests for ServiceContractRequirementViewSet
# =============================================================================

class TestServiceContractRequirementViewSet:
    """Test the ServiceContractRequirementViewSet."""

    def test_get_queryset_filters_by_service(self):
        """Test get_queryset filters by service query param."""
        request = create_mock_request(query_params={'service': '5'})
        viewset = ServiceContractRequirementViewSet()
        viewset.request = request

        mock_qs = Mock()
        mock_selected = Mock()
        mock_filtered = Mock()
        mock_qs.select_related.return_value = mock_selected
        mock_selected.filter.return_value = mock_filtered

        with patch('rest_framework.viewsets.ModelViewSet.get_queryset', return_value=mock_qs):
            result = viewset.get_queryset()

        mock_qs.select_related.assert_called_once_with('service', 'template')
        mock_selected.filter.assert_called_once_with(service_id='5')

    def test_get_queryset_filters_by_template(self):
        """Test get_queryset filters by template query param."""
        request = create_mock_request(query_params={'template': '3'})
        viewset = ServiceContractRequirementViewSet()
        viewset.request = request

        mock_qs = Mock()
        mock_selected = Mock()
        mock_filtered = Mock()
        mock_qs.select_related.return_value = mock_selected
        mock_selected.filter.return_value = mock_filtered

        with patch('rest_framework.viewsets.ModelViewSet.get_queryset', return_value=mock_qs):
            result = viewset.get_queryset()

        mock_qs.select_related.assert_called_once_with('service', 'template')
        mock_selected.filter.assert_called_once_with(template_id='3')

    def test_get_queryset_filters_by_both_service_and_template(self):
        """Test get_queryset filters by both service and template."""
        request = create_mock_request(query_params={'service': '5', 'template': '3'})
        viewset = ServiceContractRequirementViewSet()
        viewset.request = request

        mock_qs = Mock()
        mock_selected = Mock()
        mock_filtered1 = Mock()
        mock_filtered2 = Mock()
        mock_qs.select_related.return_value = mock_selected
        mock_selected.filter.return_value = mock_filtered1
        mock_filtered1.filter.return_value = mock_filtered2

        with patch('rest_framework.viewsets.ModelViewSet.get_queryset', return_value=mock_qs):
            result = viewset.get_queryset()

        mock_qs.select_related.assert_called_once_with('service', 'template')
        # The actual implementation chains filter calls
        assert mock_selected.filter.call_count == 1
        assert mock_filtered1.filter.call_count == 1
        mock_selected.filter.assert_called_with(service_id='5')
        mock_filtered1.filter.assert_called_with(template_id='3')

    def test_get_queryset_no_filters(self):
        """Test get_queryset without any filters."""
        request = create_mock_request()
        viewset = ServiceContractRequirementViewSet()
        viewset.request = request

        mock_qs = Mock()
        mock_selected = Mock()
        mock_qs.select_related.return_value = mock_selected

        with patch('rest_framework.viewsets.ModelViewSet.get_queryset', return_value=mock_qs):
            result = viewset.get_queryset()

        mock_qs.select_related.assert_called_once_with('service', 'template')
        mock_selected.filter.assert_not_called()


# =============================================================================
# Tests for ContractViewSet
# =============================================================================

class TestContractViewSet:
    """Test the ContractViewSet."""

    def test_get_serializer_class_returns_list_serializer_for_list(self):
        """Test get_serializer_class returns list serializer for list action."""
        viewset = ContractViewSet()
        viewset.action = 'list'

        from smoothschedule.scheduling.contracts.serializers import ContractListSerializer
        assert viewset.get_serializer_class() == ContractListSerializer

    def test_get_serializer_class_returns_create_serializer_for_create(self):
        """Test get_serializer_class returns create serializer for create action."""
        viewset = ContractViewSet()
        viewset.action = 'create'

        from smoothschedule.scheduling.contracts.serializers import CreateContractSerializer
        assert viewset.get_serializer_class() == CreateContractSerializer

    def test_get_serializer_class_returns_default_serializer(self):
        """Test get_serializer_class returns default serializer for other actions."""
        viewset = ContractViewSet()
        viewset.action = 'retrieve'

        from smoothschedule.scheduling.contracts.serializers import ContractSerializer
        assert viewset.get_serializer_class() == ContractSerializer

    def test_get_queryset_applies_all_filters(self):
        """Test get_queryset applies customer, status, and template filters."""
        request = create_mock_request(query_params={'customer': '10', 'status': 'SIGNED', 'template': '5'})
        viewset = ContractViewSet()
        viewset.request = request

        mock_qs = Mock()
        mock_selected = Mock()
        mock_filtered1 = Mock()
        mock_filtered2 = Mock()
        mock_filtered3 = Mock()
        mock_ordered = Mock()

        mock_qs.select_related.return_value = mock_selected
        mock_selected.filter.return_value = mock_filtered1
        mock_filtered1.filter.return_value = mock_filtered2
        mock_filtered2.filter.return_value = mock_filtered3
        mock_filtered3.order_by.return_value = mock_ordered

        with patch('rest_framework.viewsets.ModelViewSet.get_queryset', return_value=mock_qs):
            result = viewset.get_queryset()

        mock_qs.select_related.assert_called_once_with('customer', 'template', 'signature', 'event')
        # The actual implementation chains filter calls
        mock_selected.filter.assert_called_once_with(customer_id='10')
        mock_filtered1.filter.assert_called_once_with(status='SIGNED')
        mock_filtered2.filter.assert_called_once_with(template_id='5')
        mock_filtered3.order_by.assert_called_once_with('-created_at')

    def test_get_queryset_without_filters(self):
        """Test get_queryset without any filters."""
        request = create_mock_request()
        viewset = ContractViewSet()
        viewset.request = request

        mock_qs = Mock()
        mock_selected = Mock()
        mock_ordered = Mock()

        mock_qs.select_related.return_value = mock_selected
        mock_selected.order_by.return_value = mock_ordered

        with patch('rest_framework.viewsets.ModelViewSet.get_queryset', return_value=mock_qs):
            result = viewset.get_queryset()

        mock_selected.filter.assert_not_called()
        mock_selected.order_by.assert_called_once_with('-created_at')

    def test_create_contract_with_email(self):
        """Test create action creates contract and sends email."""
        user = Mock(id=1, email='owner@example.com')
        request = create_mock_request(method='POST', user=user)

        template = Mock(
            id=1, name='Service Agreement', version=1,
            content='Contract for {{CUSTOMER_NAME}}',
            scope=ContractTemplate.Scope.CUSTOMER,
            expires_after_days=30
        )

        customer = Mock(
            id=5, email='customer@example.com',
            first_name='John', last_name='Doe', phone='555-1234'
        )
        customer.get_full_name.return_value = 'John Doe'

        validated_data = {
            'template': template,
            'customer': customer,
            'event': None,
            'send_email': True
        }

        contract = Mock(id=100, signing_token='abc123')

        mock_tenant = Mock(name='Test Business', contact_email='business@example.com', phone='555-9999')

        viewset = ContractViewSet()
        viewset.request = request

        with patch('smoothschedule.scheduling.contracts.views.CreateContractSerializer') as mock_create_serializer:
            mock_create_ser_instance = Mock()
            mock_create_ser_instance.is_valid.return_value = True
            mock_create_ser_instance.validated_data = validated_data
            mock_create_serializer.return_value = mock_create_ser_instance

            with patch.object(Contract.objects, 'create', return_value=contract) as mock_contract_create:
                with patch('smoothschedule.scheduling.contracts.tasks.send_contract_email') as mock_send_email:
                    with patch('smoothschedule.scheduling.contracts.views.ContractSerializer') as mock_contract_serializer:
                        mock_response_ser_instance = Mock()
                        mock_response_ser_instance.data = {'id': 100, 'title': 'Service Agreement'}
                        mock_contract_serializer.return_value = mock_response_ser_instance

                        with patch('django.db.connection') as mock_connection:
                            mock_connection.schema_name = 'test_tenant'
                            with patch('smoothschedule.identity.core.models.Tenant.objects.get', return_value=mock_tenant):
                                response = viewset.create(request)

                        assert mock_contract_create.called
                        mock_send_email.delay.assert_called_once_with(100)
                        contract.save.assert_called_once()
                        assert response.status_code == status.HTTP_201_CREATED

    def test_render_template_substitutes_variables(self):
        """Test _render_template substitutes all variables correctly."""
        template = Mock(content='Hello {{CUSTOMER_NAME}}, from {{BUSINESS_NAME}}. Today is {{DATE}}.')

        customer = Mock(email='john@example.com', first_name='John', last_name='Doe', phone='555-1234')
        customer.get_full_name.return_value = 'John Doe'

        mock_tenant = Mock(name='Acme Corp', contact_email='info@acme.com', phone='555-0000')

        viewset = ContractViewSet()
        viewset.request = Mock()

        with patch('smoothschedule.identity.core.models.Tenant.objects.get', return_value=mock_tenant):
            with patch('django.db.connection') as mock_connection:
                mock_connection.schema_name = 'acme'
                with patch('django.utils.timezone.now') as mock_now:
                    mock_now.return_value = datetime(2024, 1, 15, 10, 30)
                    result = viewset._render_template(template, customer, None)

        assert 'John Doe' in result
        assert 'Acme Corp' in result
        assert 'January 15, 2024' in result

    def test_render_template_with_event_variables(self):
        """Test _render_template includes event-specific variables."""
        template = Mock(content='Service: {{SERVICE_NAME}} on {{APPOINTMENT_DATE}} at {{APPOINTMENT_TIME}}')

        customer = Mock(email='customer@example.com', first_name='', last_name='', phone='')
        customer.get_full_name.return_value = ''

        service = Mock(name='Haircut')
        event = Mock(start_time=datetime(2024, 2, 20, 14, 30), service=service)

        mock_tenant = Mock(name='Salon', contact_email='', phone='')

        viewset = ContractViewSet()
        viewset.request = Mock()

        with patch('smoothschedule.identity.core.models.Tenant.objects.get', return_value=mock_tenant):
            with patch('django.db.connection') as mock_connection:
                mock_connection.schema_name = 'salon'
                result = viewset._render_template(template, customer, event)

        assert 'Haircut' in result
        assert 'February 20, 2024' in result
        assert '02:30 PM' in result

    def test_send_action_sends_pending_contract(self):
        """Test send action sends pending contract via email."""
        request = create_mock_request(method='POST')
        contract = Mock(id=1, status=Contract.Status.PENDING)

        viewset = ContractViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=contract)

        with patch('smoothschedule.scheduling.contracts.tasks.send_contract_email') as mock_send_email:
            with patch('django.utils.timezone.now') as mock_now:
                mock_now.return_value = datetime(2024, 1, 15, 10, 0)
                response = viewset.send(request, pk=1)

                mock_send_email.delay.assert_called_once_with(1)
                contract.save.assert_called_once_with(update_fields=['sent_at'])
                assert response.status_code == status.HTTP_200_OK
                assert response.data == {'success': True, 'message': 'Contract sent'}

    def test_send_action_rejects_non_pending_contract(self):
        """Test send action rejects non-pending contracts."""
        request = create_mock_request(method='POST')
        contract = Mock(id=1, status=Contract.Status.SIGNED)

        viewset = ContractViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=contract)

        with patch('smoothschedule.scheduling.contracts.tasks.send_contract_email') as mock_send_email:
            response = viewset.send(request, pk=1)

            assert response.status_code == status.HTTP_400_BAD_REQUEST
            assert 'error' in response.data
            mock_send_email.delay.assert_not_called()

    def test_resend_action_resends_pending_contract(self):
        """Test resend action resends pending contract."""
        request = create_mock_request(method='POST')
        contract = Mock(id=1, status=Contract.Status.PENDING)

        viewset = ContractViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=contract)

        with patch('smoothschedule.scheduling.contracts.tasks.send_contract_email') as mock_send_email:
            response = viewset.resend(request, pk=1)

            mock_send_email.delay.assert_called_once_with(1)
            assert response.status_code == status.HTTP_200_OK
            assert response.data == {'success': True, 'message': 'Contract resent'}

    def test_void_action_voids_pending_contract(self):
        """Test void action voids pending contract."""
        request = create_mock_request(method='POST')
        contract = Mock(id=1, status=Contract.Status.PENDING)

        viewset = ContractViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=contract)

        response = viewset.void(request, pk=1)

        assert contract.status == Contract.Status.VOIDED
        contract.save.assert_called_once_with(update_fields=['status', 'updated_at'])
        assert response.status_code == status.HTTP_200_OK
        assert response.data == {'success': True}

    def test_download_pdf_returns_pdf_file(self):
        """Test download_pdf returns PDF file when available."""
        request = create_mock_request()
        contract = Mock(id=1, title='Service Agreement', pdf_path='contracts/signed_123.pdf')

        mock_file = BytesIO(b'%PDF-1.4 content')

        viewset = ContractViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=contract)

        with patch('django.core.files.storage.default_storage') as mock_storage:
            mock_storage.open.return_value = mock_file
            response = viewset.download_pdf(request, pk=1)

            mock_storage.open.assert_called_once_with('contracts/signed_123.pdf', 'rb')
            assert response.status_code == 200

    def test_download_pdf_returns_404_when_no_pdf(self):
        """Test download_pdf returns 404 when PDF path is empty."""
        request = create_mock_request()
        contract = Mock(id=1, pdf_path='')

        viewset = ContractViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=contract)

        with patch('django.core.files.storage.default_storage') as mock_storage:
            response = viewset.download_pdf(request, pk=1)

            assert response.status_code == status.HTTP_404_NOT_FOUND
            assert response.data == {'error': 'PDF not available'}
            mock_storage.open.assert_not_called()

    def test_export_legal_returns_503_when_weasyprint_unavailable(self):
        """Test export_legal returns 503 when WeasyPrint not available."""
        request = create_mock_request()
        contract = Mock(id=1, status=Contract.Status.SIGNED)
        contract.signature = Mock()

        viewset = ContractViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=contract)

        with patch('smoothschedule.scheduling.contracts.pdf_service.WEASYPRINT_AVAILABLE', False):
            response = viewset.export_legal(request, pk=1)

        assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
        assert 'error' in response.data

    def test_export_legal_rejects_non_signed_contract(self):
        """Test export_legal rejects non-signed contracts."""
        request = create_mock_request()
        contract = Mock(id=1, status=Contract.Status.PENDING)

        viewset = ContractViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=contract)

        response = viewset.export_legal(request, pk=1)

        assert response.status_code == status.HTTP_400_BAD_REQUEST
        assert 'error' in response.data

    def test_export_legal_generates_zip_package(self):
        """Test export_legal generates and returns ZIP package."""
        request = create_mock_request()

        signature = Mock(signed_at=datetime(2024, 1, 15, 10, 30))
        customer = Mock(email='customer@example.com')
        customer.get_full_name.return_value = 'John Doe'

        contract = Mock(
            id=1, title='Service Agreement',
            status=Contract.Status.SIGNED,
            signature=signature, customer=customer
        )

        zip_buffer = BytesIO(b'PK\x03\x04 fake zip')
        zip_buffer.read = Mock(return_value=b'PK\x03\x04 fake zip')

        viewset = ContractViewSet()
        viewset.request = request
        viewset.get_object = Mock(return_value=contract)

        with patch('smoothschedule.scheduling.contracts.pdf_service.WEASYPRINT_AVAILABLE', True):
            with patch('smoothschedule.scheduling.contracts.pdf_service.ContractPDFService') as mock_pdf_service:
                mock_pdf_service.generate_legal_export_package.return_value = zip_buffer
                response = viewset.export_legal(request, pk=1)

                mock_pdf_service.generate_legal_export_package.assert_called_once_with(contract)

        assert response.status_code == 200
        assert response['Content-Type'] == 'application/zip'
        assert 'legal_export_' in response['Content-Disposition']
        assert 'John Doe' in response['Content-Disposition'] or 'John_Doe' in response['Content-Disposition']
        assert '20240115' in response['Content-Disposition']


# =============================================================================
# Tests for PublicContractSigningView
# =============================================================================

class TestPublicContractSigningView:
    """Test the PublicContractSigningView."""

    def test_get_returns_signed_contract(self):
        """Test GET returns signed contract data."""
        request = create_mock_request()
        contract = Mock(id=1, status=Contract.Status.SIGNED)

        with patch('smoothschedule.scheduling.contracts.views.get_object_or_404', return_value=contract):
            with patch('smoothschedule.scheduling.contracts.views.PublicContractSerializer') as mock_serializer_class:
                mock_serializer_instance = Mock()
                mock_serializer_instance.data = {'contract': {'id': 1, 'status': 'SIGNED'}}
                mock_serializer_class.return_value = mock_serializer_instance

                view = PublicContractSigningView()
                response = view.get(request, token='abc123')

                assert response.status_code == status.HTTP_200_OK
                assert response.data == {'contract': {'id': 1, 'status': 'SIGNED'}}

    def test_get_returns_error_for_voided_contract(self):
        """Test GET returns error for voided contract."""
        request = create_mock_request()
        contract = Mock(id=1, status=Contract.Status.VOIDED)

        with patch('smoothschedule.scheduling.contracts.views.get_object_or_404', return_value=contract):
            view = PublicContractSigningView()
            response = view.get(request, token='abc123')

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

    def test_get_expires_contract_when_expired(self):
        """Test GET marks contract as expired when past expiration."""
        request = create_mock_request()

        current_time = datetime(2024, 1, 20, 10, 0, tzinfo=dt_timezone.utc)

        contract = Mock(
            id=1,
            status=Contract.Status.PENDING,
            expires_at=datetime(2024, 1, 19, 10, 0, tzinfo=dt_timezone.utc)
        )

        with patch('smoothschedule.scheduling.contracts.views.get_object_or_404', return_value=contract):
            with patch('django.utils.timezone.now', return_value=current_time):
                view = PublicContractSigningView()
                response = view.get(request, token='abc123')

                assert contract.status == Contract.Status.EXPIRED
                contract.save.assert_called_once_with(update_fields=['status'])
                assert response.status_code == status.HTTP_400_BAD_REQUEST
                assert response.data['status'] == 'expired'

    def test_get_returns_pending_contract_when_not_expired(self):
        """Test GET returns pending contract when not yet expired."""
        request = create_mock_request()

        current_time = datetime(2024, 1, 15, 10, 0, tzinfo=dt_timezone.utc)

        contract = Mock(
            id=1,
            status=Contract.Status.PENDING,
            expires_at=datetime(2024, 1, 20, 10, 0, tzinfo=dt_timezone.utc)
        )

        with patch('smoothschedule.scheduling.contracts.views.get_object_or_404', return_value=contract):
            with patch('django.utils.timezone.now', return_value=current_time):
                with patch('smoothschedule.scheduling.contracts.views.PublicContractSerializer') as mock_serializer_class:
                    mock_serializer_instance = Mock()
                    mock_serializer_instance.data = {'contract': {'id': 1}}
                    mock_serializer_class.return_value = mock_serializer_instance

                    view = PublicContractSigningView()
                    response = view.get(request, token='abc123')

                    assert response.status_code == status.HTTP_200_OK
                    contract.save.assert_not_called()

    def test_post_rejects_non_pending_contract(self):
        """Test POST rejects non-pending contracts."""
        request = create_mock_request(method='POST')
        contract = Mock(id=1, status=Contract.Status.SIGNED)

        with patch('smoothschedule.scheduling.contracts.views.get_object_or_404', return_value=contract):
            view = PublicContractSigningView()
            response = view.post(request, token='abc123')

            assert response.status_code == status.HTTP_400_BAD_REQUEST
            assert 'cannot be signed' in response.data['error']

    def test_post_rejects_expired_contract(self):
        """Test POST rejects expired contracts."""
        request = create_mock_request(method='POST')

        current_time = datetime(2024, 1, 20, 10, 0, tzinfo=dt_timezone.utc)

        contract = Mock(
            id=1,
            status=Contract.Status.PENDING,
            expires_at=datetime(2024, 1, 19, 10, 0, tzinfo=dt_timezone.utc)
        )

        with patch('smoothschedule.scheduling.contracts.views.get_object_or_404', return_value=contract):
            with patch('django.utils.timezone.now', return_value=current_time):
                view = PublicContractSigningView()
                response = view.post(request, token='abc123')

                assert contract.status == Contract.Status.EXPIRED
                assert response.status_code == status.HTTP_400_BAD_REQUEST
                assert 'expired' in response.data['error']

    def test_post_rejects_when_consent_checkbox_not_checked(self):
        """Test POST rejects when consent checkbox is not checked."""
        request = create_mock_request(method='POST')

        contract = Mock(id=1, status=Contract.Status.PENDING, expires_at=None)

        with patch('smoothschedule.scheduling.contracts.views.get_object_or_404', return_value=contract):
            with patch('smoothschedule.scheduling.contracts.views.ContractSignatureInputSerializer') as mock_serializer_class:
                mock_serializer_instance = Mock()
                mock_serializer_instance.is_valid.return_value = True
                mock_serializer_instance.validated_data = {
                    'consent_checkbox_checked': False,
                    'electronic_consent_given': True,
                    'signer_name': 'John Doe'
                }
                mock_serializer_class.return_value = mock_serializer_instance

                view = PublicContractSigningView()
                response = view.post(request, token='abc123')

                assert response.status_code == status.HTTP_400_BAD_REQUEST
                assert 'consent box' in response.data['error']

    def test_post_rejects_when_electronic_consent_not_given(self):
        """Test POST rejects when electronic consent is not given."""
        request = create_mock_request(method='POST')

        contract = Mock(id=1, status=Contract.Status.PENDING, expires_at=None)

        with patch('smoothschedule.scheduling.contracts.views.get_object_or_404', return_value=contract):
            with patch('smoothschedule.scheduling.contracts.views.ContractSignatureInputSerializer') as mock_serializer_class:
                mock_serializer_instance = Mock()
                mock_serializer_instance.is_valid.return_value = True
                mock_serializer_instance.validated_data = {
                    'consent_checkbox_checked': True,
                    'electronic_consent_given': False,
                    'signer_name': 'John Doe'
                }
                mock_serializer_class.return_value = mock_serializer_instance

                view = PublicContractSigningView()
                response = view.post(request, token='abc123')

                assert response.status_code == status.HTTP_400_BAD_REQUEST
                assert 'electronic records' in response.data['error']

    def test_post_creates_signature_and_updates_contract(self):
        """Test POST creates signature and updates contract status."""
        request = create_mock_request(
            method='POST',
            meta={'HTTP_USER_AGENT': 'Mozilla/5.0', 'REMOTE_ADDR': '203.0.113.5'}
        )

        current_time = datetime(2024, 1, 15, 10, 30, tzinfo=dt_timezone.utc)

        customer = Mock(email='customer@example.com')
        contract = Mock(
            id=100,
            status=Contract.Status.PENDING,
            expires_at=None,
            content_hash='abc123hash',
            customer=customer
        )

        signature = Mock(id=1)

        with patch('smoothschedule.scheduling.contracts.views.get_object_or_404', return_value=contract):
            with patch('smoothschedule.scheduling.contracts.views.ContractSignatureInputSerializer') as mock_serializer_class:
                mock_serializer_instance = Mock()
                mock_serializer_instance.is_valid.return_value = True
                mock_serializer_instance.validated_data = {
                    'consent_checkbox_checked': True,
                    'electronic_consent_given': True,
                    'signer_name': 'John Doe',
                    'latitude': Decimal('40.712776'),
                    'longitude': Decimal('-74.005974')
                }
                mock_serializer_class.return_value = mock_serializer_instance

                with patch.object(ContractSignature.objects, 'create', return_value=signature) as mock_signature_create:
                    with patch('smoothschedule.scheduling.contracts.tasks.generate_contract_pdf') as mock_generate_pdf:
                        with patch('smoothschedule.scheduling.contracts.tasks.send_contract_signed_emails') as mock_send_emails:
                            with patch('django.utils.timezone.now', return_value=current_time):
                                view = PublicContractSigningView()
                                response = view.post(request, token='abc123')

                                mock_signature_create.assert_called_once()
                                call_kwargs = mock_signature_create.call_args[1]
                                assert call_kwargs['contract'] == contract
                                assert call_kwargs['signer_name'] == 'John Doe'
                                assert call_kwargs['signer_email'] == 'customer@example.com'
                                assert call_kwargs['ip_address'] == '203.0.113.5'
                                assert call_kwargs['user_agent'] == 'Mozilla/5.0'
                                assert call_kwargs['document_hash_at_signing'] == 'abc123hash'
                                assert call_kwargs['latitude'] == Decimal('40.712776')
                                assert call_kwargs['longitude'] == Decimal('-74.005974')
                                assert call_kwargs['consent_checkbox_checked'] is True
                                assert call_kwargs['electronic_consent_given'] is True

                                assert contract.status == Contract.Status.SIGNED
                                contract.save.assert_called_once_with(update_fields=['status', 'updated_at'])

                                mock_generate_pdf.delay.assert_called_once_with(100)
                                mock_send_emails.delay.assert_called_once_with(100)

                                assert response.status_code == status.HTTP_200_OK
                                assert response.data == {'success': True, 'message': 'Contract signed successfully'}

    def test_post_creates_signature_without_geolocation(self):
        """Test POST creates signature without geolocation data."""
        request = create_mock_request(
            method='POST',
            meta={'REMOTE_ADDR': '192.0.2.1'}
        )

        current_time = datetime(2024, 1, 15, 14, 0, tzinfo=dt_timezone.utc)

        customer = Mock(email='jane@example.com')
        contract = Mock(
            id=200,
            status=Contract.Status.PENDING,
            expires_at=None,
            content_hash='xyz789hash',
            customer=customer
        )

        signature = Mock(id=2)

        with patch('smoothschedule.scheduling.contracts.views.get_object_or_404', return_value=contract):
            with patch('smoothschedule.scheduling.contracts.views.ContractSignatureInputSerializer') as mock_serializer_class:
                mock_serializer_instance = Mock()
                mock_serializer_instance.is_valid.return_value = True
                mock_serializer_instance.validated_data = {
                    'consent_checkbox_checked': True,
                    'electronic_consent_given': True,
                    'signer_name': 'Jane Smith',
                    'latitude': None,
                    'longitude': None
                }
                mock_serializer_class.return_value = mock_serializer_instance

                with patch.object(ContractSignature.objects, 'create', return_value=signature) as mock_signature_create:
                    with patch('smoothschedule.scheduling.contracts.tasks.generate_contract_pdf'):
                        with patch('smoothschedule.scheduling.contracts.tasks.send_contract_signed_emails'):
                            with patch('django.utils.timezone.now', return_value=current_time):
                                view = PublicContractSigningView()
                                response = view.post(request, token='abc123')

                                call_kwargs = mock_signature_create.call_args[1]
                                assert call_kwargs['latitude'] is None
                                assert call_kwargs['longitude'] is None

                                assert response.status_code == status.HTTP_200_OK

    def test_get_handles_none_expires_at(self):
        """Test GET handles contract with no expiration date."""
        request = create_mock_request()

        current_time = datetime(2024, 1, 15, 10, 0, tzinfo=dt_timezone.utc)

        contract = Mock(
            id=1,
            status=Contract.Status.PENDING,
            expires_at=None
        )

        with patch('smoothschedule.scheduling.contracts.views.get_object_or_404', return_value=contract):
            with patch('django.utils.timezone.now', return_value=current_time):
                with patch('smoothschedule.scheduling.contracts.views.PublicContractSerializer') as mock_serializer_class:
                    mock_serializer_instance = Mock()
                    mock_serializer_instance.data = {'contract': {'id': 1}}
                    mock_serializer_class.return_value = mock_serializer_instance

                    view = PublicContractSigningView()
                    response = view.get(request, token='abc123')

                    contract.save.assert_not_called()
                    assert response.status_code == status.HTTP_200_OK
