"""
Tests for Puck-based Email Template System

TDD: These tests are written BEFORE implementation.
Tests cover:
- EmailType enum completeness
- PuckEmailTemplate model CRUD
- Tag allowlist validation
- Tenant isolation
- Email rendering pipeline
- API endpoints
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from django.core.exceptions import ValidationError


# =============================================================================
# Test: EmailType Enum
# =============================================================================

class TestEmailTypeEnum:
    """Test that the EmailType enum contains all required email types."""

    def test_enum_has_appointment_confirmation(self):
        """APPOINTMENT_CONFIRMATION type exists."""
        from ..email_types import EmailType
        assert hasattr(EmailType, 'APPOINTMENT_CONFIRMATION')
        assert EmailType.APPOINTMENT_CONFIRMATION.value == 'appointment_confirmation'

    def test_enum_has_appointment_reminder(self):
        """APPOINTMENT_REMINDER type exists."""
        from ..email_types import EmailType
        assert hasattr(EmailType, 'APPOINTMENT_REMINDER')
        assert EmailType.APPOINTMENT_REMINDER.value == 'appointment_reminder'

    def test_enum_has_appointment_rescheduled(self):
        """APPOINTMENT_RESCHEDULED type exists."""
        from ..email_types import EmailType
        assert hasattr(EmailType, 'APPOINTMENT_RESCHEDULED')
        assert EmailType.APPOINTMENT_RESCHEDULED.value == 'appointment_rescheduled'

    def test_enum_has_appointment_cancelled(self):
        """APPOINTMENT_CANCELLED type exists."""
        from ..email_types import EmailType
        assert hasattr(EmailType, 'APPOINTMENT_CANCELLED')
        assert EmailType.APPOINTMENT_CANCELLED.value == 'appointment_cancelled'

    def test_enum_has_welcome(self):
        """WELCOME type exists."""
        from ..email_types import EmailType
        assert hasattr(EmailType, 'WELCOME')
        assert EmailType.WELCOME.value == 'welcome'

    def test_enum_has_thank_you(self):
        """THANK_YOU type exists."""
        from ..email_types import EmailType
        assert hasattr(EmailType, 'THANK_YOU')
        assert EmailType.THANK_YOU.value == 'thank_you'

    def test_enum_has_payment_receipt(self):
        """PAYMENT_RECEIPT type exists."""
        from ..email_types import EmailType
        assert hasattr(EmailType, 'PAYMENT_RECEIPT')
        assert EmailType.PAYMENT_RECEIPT.value == 'payment_receipt'

    def test_enum_has_contract_signing_request(self):
        """CONTRACT_SIGNING_REQUEST type exists."""
        from ..email_types import EmailType
        assert hasattr(EmailType, 'CONTRACT_SIGNING_REQUEST')
        assert EmailType.CONTRACT_SIGNING_REQUEST.value == 'contract_signing_request'

    def test_enum_has_contract_reminder(self):
        """CONTRACT_REMINDER type exists."""
        from ..email_types import EmailType
        assert hasattr(EmailType, 'CONTRACT_REMINDER')
        assert EmailType.CONTRACT_REMINDER.value == 'contract_reminder'

    def test_enum_has_contract_signed(self):
        """CONTRACT_SIGNED type exists."""
        from ..email_types import EmailType
        assert hasattr(EmailType, 'CONTRACT_SIGNED')
        assert EmailType.CONTRACT_SIGNED.value == 'contract_signed'

    def test_enum_has_ticket_assigned(self):
        """TICKET_ASSIGNED type exists."""
        from ..email_types import EmailType
        assert hasattr(EmailType, 'TICKET_ASSIGNED')
        assert EmailType.TICKET_ASSIGNED.value == 'ticket_assigned'

    def test_enum_has_ticket_reply(self):
        """TICKET_REPLY type exists."""
        from ..email_types import EmailType
        assert hasattr(EmailType, 'TICKET_REPLY')
        assert EmailType.TICKET_REPLY.value == 'ticket_reply'

    def test_enum_has_ticket_resolved(self):
        """TICKET_RESOLVED type exists."""
        from ..email_types import EmailType
        assert hasattr(EmailType, 'TICKET_RESOLVED')
        assert EmailType.TICKET_RESOLVED.value == 'ticket_resolved'

    def test_all_enum_values_are_unique(self):
        """All enum values must be unique."""
        from ..email_types import EmailType
        values = [e.value for e in EmailType]
        assert len(values) == len(set(values)), "Duplicate enum values found"


# =============================================================================
# Test: Tag Allowlist System
# =============================================================================

class TestTagAllowlist:
    """Test the tag allowlist validation system."""

    def test_base_tags_are_defined(self):
        """Base tags available for all email types are defined."""
        from ..email_tags import BASE_TAGS

        expected_tags = [
            'business_name', 'business_email', 'business_phone',
            'business_address', 'business_logo_url', 'business_website_url',
            'customer_name', 'customer_first_name', 'customer_last_name',
            'customer_email', 'customer_phone'
        ]

        for tag in expected_tags:
            assert tag in BASE_TAGS, f"Missing base tag: {tag}"

    def test_appointment_tags_are_defined(self):
        """Appointment-related tags are defined."""
        from ..email_tags import APPOINTMENT_TAGS

        expected_tags = [
            'appointment_date', 'appointment_time', 'appointment_datetime',
            'service_name', 'staff_name', 'location_name',
            'manage_appointment_link', 'reschedule_link', 'cancel_link',
            'add_to_calendar_link'
        ]

        for tag in expected_tags:
            assert tag in APPOINTMENT_TAGS, f"Missing appointment tag: {tag}"

    def test_payment_tags_are_defined(self):
        """Payment-related tags are defined."""
        from ..email_tags import PAYMENT_TAGS

        expected_tags = [
            'amount_due', 'amount_paid', 'currency',
            'invoice_number', 'receipt_link'
        ]

        for tag in expected_tags:
            assert tag in PAYMENT_TAGS, f"Missing payment tag: {tag}"

    def test_get_allowed_tags_for_appointment_confirmation(self):
        """APPOINTMENT_CONFIRMATION has base + appointment tags."""
        from ..email_tags import get_allowed_tags_for_email_type
        from ..email_types import EmailType

        allowed = get_allowed_tags_for_email_type(EmailType.APPOINTMENT_CONFIRMATION)

        # Should include base tags
        assert 'customer_name' in allowed
        assert 'business_name' in allowed

        # Should include appointment tags
        assert 'appointment_date' in allowed
        assert 'service_name' in allowed

        # Should NOT include payment tags
        assert 'amount_paid' not in allowed

    def test_get_allowed_tags_for_payment_receipt(self):
        """PAYMENT_RECEIPT has base + payment tags."""
        from ..email_tags import get_allowed_tags_for_email_type
        from ..email_types import EmailType

        allowed = get_allowed_tags_for_email_type(EmailType.PAYMENT_RECEIPT)

        # Should include base tags
        assert 'customer_name' in allowed

        # Should include payment tags
        assert 'amount_paid' in allowed
        assert 'receipt_link' in allowed

    def test_validate_tags_accepts_valid_tags(self):
        """validate_tags passes for valid tag usage."""
        from ..email_tags import validate_tags
        from ..email_types import EmailType

        subject = "Hello {{ customer_name }}"
        body_content = "Your appointment on {{ appointment_date }}"

        # Should not raise
        errors = validate_tags(subject, body_content, EmailType.APPOINTMENT_CONFIRMATION)
        assert len(errors) == 0

    def test_validate_tags_rejects_unknown_tags(self):
        """validate_tags fails for unknown tags."""
        from ..email_tags import validate_tags
        from ..email_types import EmailType

        subject = "Hello {{ unknown_tag }}"
        body_content = "Your appointment"

        errors = validate_tags(subject, body_content, EmailType.APPOINTMENT_CONFIRMATION)
        assert len(errors) > 0
        assert 'unknown_tag' in str(errors[0])

    def test_validate_tags_rejects_payment_tags_in_appointment_email(self):
        """Payment tags not allowed in appointment emails."""
        from ..email_tags import validate_tags
        from ..email_types import EmailType

        subject = "Hello"
        body_content = "Amount paid: {{ amount_paid }}"

        errors = validate_tags(subject, body_content, EmailType.APPOINTMENT_CONFIRMATION)
        assert len(errors) > 0
        assert 'amount_paid' in str(errors[0])


# =============================================================================
# Test: PuckEmailTemplate Model
# =============================================================================

class TestPuckEmailTemplateModel:
    """Test PuckEmailTemplate model behavior (unit tests with mocks)."""

    def test_model_has_required_fields(self):
        """Model has all required fields."""
        from ..models import PuckEmailTemplate

        # Check field existence
        fields = [f.name for f in PuckEmailTemplate._meta.get_fields()]

        assert 'email_type' in fields
        assert 'subject_template' in fields
        assert 'puck_data' in fields
        assert 'is_active' in fields
        assert 'created_at' in fields
        assert 'updated_at' in fields

    def test_unique_constraint_tenant_email_type(self):
        """Only one active template per tenant per email_type."""
        from ..models import PuckEmailTemplate

        # Check unique_together constraint
        unique_together = PuckEmailTemplate._meta.unique_together
        # Should have (tenant, email_type) as unique constraint
        # Note: exact format depends on implementation
        assert len(unique_together) > 0 or hasattr(PuckEmailTemplate._meta, 'constraints')

    def test_email_type_choices_match_enum(self):
        """email_type field uses EmailType enum values."""
        from ..models import PuckEmailTemplate
        from ..email_types import EmailType

        field = PuckEmailTemplate._meta.get_field('email_type')
        choices = dict(field.choices)

        for email_type in EmailType:
            assert email_type.value in choices, f"Missing choice for {email_type}"

    def test_puck_data_is_json_field(self):
        """puck_data is a JSONField."""
        from ..models import PuckEmailTemplate
        from django.db.models import JSONField

        field = PuckEmailTemplate._meta.get_field('puck_data')
        assert isinstance(field, JSONField)

    def test_default_puck_data_structure(self):
        """Default puck_data has expected structure."""
        from ..models import PuckEmailTemplate

        field = PuckEmailTemplate._meta.get_field('puck_data')
        default = field.default

        if callable(default):
            default = default()

        # Should have content array
        assert 'content' in default
        assert isinstance(default['content'], list)

    def test_clean_validates_tags(self):
        """Model clean() method validates tags."""
        from ..models import PuckEmailTemplate
        from ..email_types import EmailType

        template = PuckEmailTemplate(
            email_type=EmailType.APPOINTMENT_CONFIRMATION.value,
            subject_template="Hello {{ invalid_tag }}",
            puck_data={'content': []},
        )

        with pytest.raises(ValidationError) as exc_info:
            template.clean()

        assert 'invalid_tag' in str(exc_info.value)


# =============================================================================
# Test: Email Rendering Pipeline
# =============================================================================

class TestEmailRenderingPipeline:
    """Test the email rendering functions."""

    def test_render_subject_substitutes_tags(self):
        """Subject rendering substitutes tags correctly."""
        from ..email_renderer import render_subject

        template = "Hello {{ customer_first_name }}, your appointment at {{ business_name }}"
        context = {
            'customer_first_name': 'John',
            'business_name': 'Acme Salon'
        }

        result = render_subject(template, context)
        assert result == "Hello John, your appointment at Acme Salon"

    def test_render_subject_escapes_html(self):
        """Subject rendering escapes HTML in substituted values."""
        from ..email_renderer import render_subject

        template = "Hello {{ customer_name }}"
        context = {
            'customer_name': '<script>alert("xss")</script>'
        }

        result = render_subject(template, context)
        assert '<script>' not in result
        assert '&lt;script&gt;' in result or 'alert' not in result

    def test_render_html_produces_email_safe_output(self):
        """HTML rendering produces email-safe HTML."""
        from ..email_renderer import render_email_html

        puck_data = {
            'content': [
                {
                    'type': 'EmailHeading',
                    'props': {'text': 'Welcome!', 'level': 'h1'}
                },
                {
                    'type': 'EmailText',
                    'props': {'content': 'Hello {{ customer_name }}'}
                }
            ]
        }
        context = {'customer_name': 'Jane'}

        result = render_email_html(puck_data, context)

        # Should contain heading
        assert 'Welcome!' in result
        # Should substitute tag
        assert 'Jane' in result
        # Should be table-based layout (email-safe)
        assert '<table' in result.lower()
        # Should NOT contain dangerous elements
        assert '<script' not in result.lower()
        assert '<iframe' not in result.lower()

    def test_render_html_applies_inline_styles(self):
        """HTML rendering applies inline styles for email clients."""
        from ..email_renderer import render_email_html

        puck_data = {
            'content': [
                {
                    'type': 'EmailButton',
                    'props': {'text': 'Click Me', 'href': 'https://example.com'}
                }
            ]
        }

        result = render_email_html(puck_data, {})

        # Button should have inline styles
        assert 'style=' in result
        # Should be a link (not an actual button element)
        assert 'href="https://example.com"' in result

    def test_render_plaintext_from_puck_data(self):
        """Plain text rendering produces readable text."""
        from ..email_renderer import render_email_plaintext

        puck_data = {
            'content': [
                {
                    'type': 'EmailHeading',
                    'props': {'text': 'Welcome!', 'level': 'h1'}
                },
                {
                    'type': 'EmailText',
                    'props': {'content': 'Hello {{ customer_name }}, thanks for booking.'}
                },
                {
                    'type': 'EmailButton',
                    'props': {'text': 'View Appointment', 'href': 'https://example.com/apt'}
                }
            ]
        }
        context = {'customer_name': 'Bob'}

        result = render_email_plaintext(puck_data, context)

        # Should contain text content
        assert 'Welcome!' in result
        assert 'Bob' in result
        assert 'thanks for booking' in result
        # Button should show as link
        assert 'https://example.com/apt' in result
        # Should NOT contain HTML
        assert '<' not in result

    def test_render_email_returns_all_parts(self):
        """render_email returns subject, html, and text."""
        from ..email_renderer import render_email
        from ..models import PuckEmailTemplate
        from ..email_types import EmailType

        # Mock template
        template = Mock(spec=PuckEmailTemplate)
        template.subject_template = "Appointment on {{ appointment_date }}"
        template.puck_data = {
            'content': [
                {'type': 'EmailText', 'props': {'content': 'Hello {{ customer_name }}'}}
            ]
        }
        template.email_type = EmailType.APPOINTMENT_CONFIRMATION.value

        context = {
            'appointment_date': 'Dec 15',
            'customer_name': 'Alice'
        }

        result = render_email(template, context)

        assert 'subject' in result
        assert 'html' in result
        assert 'text' in result

        assert result['subject'] == 'Appointment on Dec 15'
        assert 'Alice' in result['html']
        assert 'Alice' in result['text']


# =============================================================================
# Test: API Endpoints
# =============================================================================

class TestEmailTemplateAPIEndpoints:
    """Test API endpoint behavior (unit tests with mocks)."""

    def test_list_templates_returns_all_email_types(self):
        """GET /email-templates/ returns template for each email type."""
        from ..views import EmailTemplateViewSet
        from ..email_types import EmailType
        from ..serializers import PuckEmailTemplateListSerializer
        from rest_framework.test import APIRequestFactory

        factory = APIRequestFactory()
        request = factory.get('/api/email-templates/')
        # Mock user with proper role value
        mock_user = Mock(is_authenticated=True, role='TENANT_OWNER')
        request.user = mock_user
        request.tenant = Mock(id=1)

        view = EmailTemplateViewSet.as_view({'get': 'list'})

        # Mock templates for serializer
        mock_templates = []
        for et in EmailType:
            mock_t = Mock()
            mock_t.id = 1
            mock_t.email_type = et.value
            mock_t.subject_template = f"Subject for {et.value}"
            mock_t.is_active = True
            mock_t.is_customized = False
            mock_t.created_at = '2024-01-01T00:00:00Z'
            mock_t.updated_at = '2024-01-01T00:00:00Z'
            mock_t.get_email_type_display_name = Mock(return_value=et.value.replace('_', ' ').title())
            mock_t.get_email_type_description = Mock(return_value=f"Desc for {et.value}")
            mock_templates.append(mock_t)

        with patch.object(EmailTemplateViewSet, 'get_queryset', return_value=mock_templates):
            with patch('smoothschedule.communication.messaging.views.PuckEmailTemplate') as MockModel:
                MockModel.get_or_create_for_type = Mock()
                response = view(request)

        assert response.status_code == 200

    def test_retrieve_template_by_email_type(self):
        """GET /email-templates/{email_type}/ returns specific template."""
        from ..views import EmailTemplateViewSet
        from rest_framework.test import APIRequestFactory

        factory = APIRequestFactory()
        request = factory.get('/api/email-templates/appointment_confirmation/')
        mock_user = Mock(is_authenticated=True, role='TENANT_OWNER')
        request.user = mock_user
        request.tenant = Mock(id=1)

        view = EmailTemplateViewSet.as_view({'get': 'retrieve'})

        # Create a proper mock template
        mock_template = Mock()
        mock_template.id = 1
        mock_template.email_type = 'appointment_confirmation'
        mock_template.subject_template = 'Test Subject'
        mock_template.puck_data = {'content': []}
        mock_template.is_active = True
        mock_template.is_customized = False
        mock_template.created_at = '2024-01-01T00:00:00Z'
        mock_template.updated_at = '2024-01-01T00:00:00Z'
        mock_template.get_email_type_display_name = Mock(return_value='Appointment Confirmation')
        mock_template.get_email_type_description = Mock(return_value='Test description')

        with patch.object(EmailTemplateViewSet, 'get_object', return_value=mock_template):
            response = view(request, email_type='appointment_confirmation')

        assert response.status_code == 200

    def test_update_template_validates_tags(self):
        """PUT /email-templates/{email_type}/ validates tags."""
        from ..serializers import PuckEmailTemplateDetailSerializer
        from ..email_types import EmailType
        from ..models import PuckEmailTemplate

        # Create a mock instance for the serializer
        mock_instance = Mock(spec=PuckEmailTemplate)
        mock_instance.email_type = EmailType.APPOINTMENT_CONFIRMATION.value
        mock_instance.subject_template = 'Old subject'
        mock_instance.puck_data = {'content': []}

        # Test serializer validation with invalid tag
        data = {
            'subject_template': 'Hello {{ invalid_tag }}',
            'puck_data': {'content': []},
        }

        serializer = PuckEmailTemplateDetailSerializer(
            instance=mock_instance,
            data=data,
            partial=True
        )

        # Should be invalid due to tag
        is_valid = serializer.is_valid()
        assert not is_valid
        assert 'subject_template' in serializer.errors

    def test_reset_template_restores_default(self):
        """POST /email-templates/{email_type}/reset/ restores default."""
        from ..views import EmailTemplateViewSet
        from rest_framework.test import APIRequestFactory

        factory = APIRequestFactory()
        request = factory.post('/api/email-templates/appointment_confirmation/reset/')
        mock_user = Mock(is_authenticated=True, role='TENANT_OWNER')
        request.user = mock_user
        request.tenant = Mock(id=1)

        view = EmailTemplateViewSet.as_view({'post': 'reset'})

        # Create a mock template with reset_to_default method
        mock_template = Mock()
        mock_template.id = 1
        mock_template.email_type = 'appointment_confirmation'
        mock_template.subject_template = 'Reset Subject'
        mock_template.puck_data = {'content': []}
        mock_template.is_active = True
        mock_template.is_customized = False
        mock_template.created_at = '2024-01-01T00:00:00Z'
        mock_template.updated_at = '2024-01-01T00:00:00Z'
        mock_template.get_email_type_display_name = Mock(return_value='Appointment Confirmation')
        mock_template.get_email_type_description = Mock(return_value='Test description')
        mock_template.reset_to_default = Mock()

        with patch.object(EmailTemplateViewSet, 'get_object', return_value=mock_template):
            response = view(request, email_type='appointment_confirmation')

        # Template should be reset
        assert response.status_code == 200
        mock_template.reset_to_default.assert_called_once()

    def test_get_tags_endpoint_returns_allowed_tags(self):
        """GET /email-template-tags/ returns all email types with their allowed tags."""
        from ..views import EmailTemplateTagsView
        from rest_framework.test import APIRequestFactory

        factory = APIRequestFactory()
        request = factory.get('/api/email-template-tags/')
        mock_user = Mock(is_authenticated=True, role='TENANT_OWNER')
        request.user = mock_user
        request.tenant = Mock(id=1)

        # ViewSet requires actions mapping
        view = EmailTemplateTagsView.as_view({'get': 'list'})
        response = view(request)

        assert response.status_code == 200
        data = response.data

        # Should return data for each email type
        assert 'appointment_confirmation' in data
        assert 'tags' in data['appointment_confirmation']
        assert isinstance(data['appointment_confirmation']['tags'], list)

        # Each tag should have name and description
        for tag in data['appointment_confirmation']['tags']:
            assert 'name' in tag
            assert 'description' in tag


# =============================================================================
# Test: Tenant Isolation
# =============================================================================

class TestTenantIsolation:
    """Test that templates are properly tenant-scoped."""

    def test_queryset_filters_by_tenant(self):
        """ViewSet queryset filters templates by current tenant."""
        from ..views import EmailTemplateViewSet
        from rest_framework.test import APIRequestFactory

        factory = APIRequestFactory()
        request = factory.get('/api/email-templates/')
        request.user = Mock(is_authenticated=True, role='owner')
        request.tenant = Mock(id=42, schema_name='tenant_42')

        view = EmailTemplateViewSet()
        view.request = request
        view.format_kwarg = None

        # The get_queryset should filter by tenant
        # This tests the view's logic, not actual DB
        with patch('smoothschedule.communication.messaging.views.PuckEmailTemplate') as MockModel:
            mock_manager = MagicMock()
            MockModel.objects = mock_manager

            view.get_queryset()

            # Verify tenant filter was applied
            # The exact call depends on implementation

    def test_create_sets_tenant(self):
        """Creating template associates it with current tenant via django-tenants."""
        from ..models import PuckEmailTemplate
        from ..email_types import EmailType

        # In django-tenants, templates are automatically scoped to the
        # current tenant schema. This test verifies the model can be created
        # with valid data (actual tenant scoping is handled by django-tenants).
        mock_template = Mock(spec=PuckEmailTemplate)
        mock_template.email_type = EmailType.WELCOME.value
        mock_template.subject_template = 'Welcome!'
        mock_template.puck_data = {'content': []}

        # The model should have the expected fields
        assert mock_template.email_type == 'welcome'
        assert mock_template.subject_template == 'Welcome!'
        assert mock_template.puck_data == {'content': []}


# =============================================================================
# Test: Default Template Seeding
# =============================================================================

class TestDefaultTemplateSeeding:
    """Test default template seeding behavior."""

    def test_default_templates_exist_for_all_types(self):
        """Default templates are defined for all email types."""
        from ..default_templates import DEFAULT_TEMPLATES
        from ..email_types import EmailType

        for email_type in EmailType:
            assert email_type.value in DEFAULT_TEMPLATES, \
                f"Missing default template for {email_type.value}"

    def test_default_template_has_required_fields(self):
        """Each default template has subject and puck_data."""
        from ..default_templates import DEFAULT_TEMPLATES

        for email_type, template in DEFAULT_TEMPLATES.items():
            assert 'subject_template' in template, \
                f"Missing subject_template for {email_type}"
            assert 'puck_data' in template, \
                f"Missing puck_data for {email_type}"
            assert 'content' in template['puck_data'], \
                f"Missing content array in puck_data for {email_type}"

    def test_default_template_uses_valid_tags(self):
        """Default templates only use valid tags for their type."""
        from ..default_templates import DEFAULT_TEMPLATES
        from ..email_tags import validate_tags
        from ..email_types import EmailType

        for email_type_str, template in DEFAULT_TEMPLATES.items():
            email_type = EmailType(email_type_str)

            # Extract text content from puck_data for validation
            subject = template['subject_template']

            # Simple text extraction from puck_data
            body_text = extract_text_from_puck_data(template['puck_data'])

            errors = validate_tags(subject, body_text, email_type)
            assert len(errors) == 0, \
                f"Default template for {email_type_str} has invalid tags: {errors}"


def extract_text_from_puck_data(puck_data: dict) -> str:
    """Helper to extract text content from Puck data for tag validation."""
    texts = []
    for item in puck_data.get('content', []):
        props = item.get('props', {})
        if 'text' in props:
            texts.append(props['text'])
        if 'content' in props:
            texts.append(props['content'])
    return ' '.join(texts)
