"""
Unit tests for Schedule serializers.

Tests serializer validation and transformation logic.
"""
from unittest.mock import Mock, patch
from rest_framework.test import APIRequestFactory
from decimal import Decimal
from datetime import datetime, timedelta, date, time
import pytest

from smoothschedule.scheduling.schedule.serializers import (
    ResourceTypeSerializer,
    CustomerSerializer,
    StaffSerializer,
    ServiceSerializer,
    ResourceSerializer,
    EventSerializer,
    ParticipantSerializer,
    TimeBlockSerializer,
    HolidaySerializer,
    PluginInstallationSerializer,
    EmailTemplateSerializer,
)


class TestResourceTypeSerializer:
    """Test ResourceTypeSerializer validation."""

    def test_read_only_fields(self):
        """Test that correct fields are read-only."""
        serializer = ResourceTypeSerializer()

        assert serializer.fields['id'].read_only
        assert serializer.fields['created_at'].read_only
        assert serializer.fields['updated_at'].read_only
        assert serializer.fields['is_default'].read_only

    def test_writable_fields(self):
        """Test that correct fields are writable."""
        serializer = ResourceTypeSerializer()
        writable = [f for f in serializer.fields if not serializer.fields[f].read_only]

        assert 'name' in writable
        assert 'description' in writable
        assert 'category' in writable
        assert 'icon_name' in writable

    def test_validate_allows_renaming_default_type(self):
        """Test that default types can be renamed."""
        # Arrange
        mock_instance = Mock()
        mock_instance.is_default = True
        mock_instance.name = 'Staff'

        serializer = ResourceTypeSerializer(instance=mock_instance)

        # Act - renaming is allowed
        attrs = {'name': 'Team Members'}
        result = serializer.validate(attrs)

        # Assert
        assert result == attrs

    def test_delete_validation_blocks_default_types(self):
        """Test that default types cannot be deleted."""
        # Arrange
        mock_instance = Mock()
        mock_instance.is_default = True

        serializer = ResourceTypeSerializer()

        # Act & Assert
        with pytest.raises(Exception):  # ValidationError in real usage
            serializer.delete(mock_instance)

    def test_delete_validation_blocks_types_in_use(self):
        """Test that types with resources cannot be deleted."""
        # Arrange
        mock_instance = Mock()
        mock_instance.is_default = False
        mock_instance.name = 'Custom Type'
        mock_instance.resources.exists.return_value = True
        mock_instance.resources.count.return_value = 3

        serializer = ResourceTypeSerializer()

        # Act & Assert
        with pytest.raises(Exception):  # ValidationError in real usage
            serializer.delete(mock_instance)


class TestCustomerSerializer:
    """Test CustomerSerializer."""

    def test_read_only_fields(self):
        """Test that correct fields are read-only."""
        serializer = CustomerSerializer()

        assert serializer.fields['id'].read_only
        assert serializer.fields['name'].read_only
        assert serializer.fields['total_spend'].read_only
        assert serializer.fields['last_visit'].read_only
        assert serializer.fields['status'].read_only

    def test_get_name_returns_full_name(self):
        """Test that get_name returns user's full name."""
        # Arrange
        mock_user = Mock()
        mock_user.full_name = 'John Doe'

        serializer = CustomerSerializer()

        # Act
        name = serializer.get_name(mock_user)

        # Assert
        assert name == 'John Doe'

    def test_get_status_returns_active_for_active_user(self):
        """Test that get_status returns Active for active users."""
        # Arrange
        mock_user = Mock()
        mock_user.is_active = True

        serializer = CustomerSerializer()

        # Act
        status = serializer.get_status(mock_user)

        # Assert
        assert status == 'Active'

    def test_get_status_returns_inactive_for_inactive_user(self):
        """Test that get_status returns Inactive for inactive users."""
        # Arrange
        mock_user = Mock()
        mock_user.is_active = False

        serializer = CustomerSerializer()

        # Act
        status = serializer.get_status(mock_user)

        # Assert
        assert status == 'Inactive'

    def test_get_user_data_returns_masquerade_info(self):
        """Test that get_user_data returns info needed for masquerading."""
        # Arrange
        mock_user = Mock()
        mock_user.id = 42
        mock_user.username = 'johndoe'
        mock_user.full_name = 'John Doe'
        mock_user.email = 'john@example.com'

        serializer = CustomerSerializer()

        # Act
        user_data = serializer.get_user_data(mock_user)

        # Assert
        assert user_data['id'] == 42
        assert user_data['username'] == 'johndoe'
        assert user_data['name'] == 'John Doe'
        assert user_data['email'] == 'john@example.com'
        assert user_data['role'] == 'customer'


class TestStaffSerializer:
    """Test StaffSerializer."""

    def test_read_only_fields(self):
        """Test that correct fields are read-only."""
        serializer = StaffSerializer()

        assert serializer.fields['id'].read_only
        assert serializer.fields['username'].read_only
        assert serializer.fields['email'].read_only
        assert serializer.fields['role'].read_only
        assert serializer.fields['can_invite_staff'].read_only

    def test_get_role_maps_tenant_owner(self):
        """Test that TENANT_OWNER maps to owner."""
        # Arrange
        mock_user = Mock()
        mock_user.role = 'TENANT_OWNER'

        serializer = StaffSerializer()

        # Act
        role = serializer.get_role(mock_user)

        # Assert
        assert role == 'owner'

    def test_get_role_maps_tenant_manager(self):
        """Test that TENANT_MANAGER maps to manager."""
        # Arrange
        mock_user = Mock()
        mock_user.role = 'TENANT_MANAGER'

        serializer = StaffSerializer()

        # Act
        role = serializer.get_role(mock_user)

        # Assert
        assert role == 'manager'

    def test_get_role_maps_tenant_staff(self):
        """Test that TENANT_STAFF maps to staff."""
        # Arrange
        mock_user = Mock()
        mock_user.role = 'TENANT_STAFF'

        serializer = StaffSerializer()

        # Act
        role = serializer.get_role(mock_user)

        # Assert
        assert role == 'staff'

    def test_get_can_invite_staff_calls_model_method(self):
        """Test that can_invite_staff calls the model method."""
        # Arrange
        mock_user = Mock()
        mock_user.can_invite_staff.return_value = True

        serializer = StaffSerializer()

        # Act
        can_invite = serializer.get_can_invite_staff(mock_user)

        # Assert
        assert can_invite is True
        mock_user.can_invite_staff.assert_called_once()


class TestServiceSerializer:
    """Test ServiceSerializer."""

    def test_read_only_fields(self):
        """Test that correct fields are read-only."""
        serializer = ServiceSerializer()

        assert serializer.fields['duration_minutes'].read_only
        assert serializer.fields['deposit_display'].read_only
        assert serializer.fields['requires_deposit'].read_only
        assert serializer.fields['requires_saved_payment_method'].read_only

    def test_duration_minutes_source(self):
        """Test that duration_minutes comes from duration field."""
        serializer = ServiceSerializer()
        assert serializer.fields['duration_minutes'].source == 'duration'

    def test_get_deposit_display_fixed_amount(self):
        """Test deposit_display for fixed amount deposits."""
        mock_service = Mock()
        mock_service.deposit_amount = Decimal('25.00')
        mock_service.deposit_percent = None

        serializer = ServiceSerializer()
        display = serializer.get_deposit_display(mock_service)

        assert display == "$25.00 deposit"

    def test_get_deposit_display_percentage(self):
        """Test deposit_display for percentage deposits."""
        mock_service = Mock()
        mock_service.deposit_amount = None
        mock_service.deposit_percent = Decimal('20.00')

        serializer = ServiceSerializer()
        display = serializer.get_deposit_display(mock_service)

        assert display == "20.00% deposit"

    def test_get_deposit_display_none(self):
        """Test deposit_display when no deposit is required."""
        mock_service = Mock()
        mock_service.deposit_amount = None
        mock_service.deposit_percent = None

        serializer = ServiceSerializer()
        display = serializer.get_deposit_display(mock_service)

        assert display is None

    def test_validate_rejects_variable_pricing_with_percentage_deposit(self):
        """Test that variable pricing cannot use percentage deposits."""
        serializer = ServiceSerializer()
        attrs = {
            'variable_pricing': True,
            'deposit_percent': Decimal('20.00')
        }

        with pytest.raises(Exception) as exc_info:
            serializer.validate(attrs)

        assert 'deposit_percent' in str(exc_info.value)

    def test_validate_rejects_deposit_percent_over_100(self):
        """Test that deposit percentage cannot exceed 100%."""
        serializer = ServiceSerializer()
        attrs = {
            'deposit_percent': Decimal('150.00')
        }

        with pytest.raises(Exception) as exc_info:
            serializer.validate(attrs)

        assert 'deposit_percent' in str(exc_info.value)

    def test_validate_allows_valid_percentage_deposit(self):
        """Test that valid percentage deposits are allowed."""
        serializer = ServiceSerializer()
        attrs = {
            'variable_pricing': False,
            'deposit_percent': Decimal('50.00')
        }

        result = serializer.validate(attrs)
        assert result == attrs

    def test_validate_allows_variable_pricing_with_fixed_deposit(self):
        """Test that variable pricing can use fixed amount deposits."""
        serializer = ServiceSerializer()
        attrs = {
            'variable_pricing': True,
            'deposit_amount': Decimal('50.00')
        }

        result = serializer.validate(attrs)
        assert result == attrs


class TestResourceSerializer:
    """Test ResourceSerializer."""

    def test_read_only_fields(self):
        """Test that correct fields are read-only."""
        serializer = ResourceSerializer()

        assert serializer.fields['created_at'].read_only
        assert serializer.fields['updated_at'].read_only
        assert serializer.fields['is_archived_by_quota'].read_only
        assert serializer.fields['user_name'].read_only
        assert serializer.fields['capacity_description'].read_only

    def test_writable_fields(self):
        """Test that correct fields are writable."""
        serializer = ResourceSerializer()
        writable = [f for f in serializer.fields if not serializer.fields[f].read_only]

        assert 'name' in writable
        assert 'type' in writable
        assert 'description' in writable
        assert 'max_concurrent_events' in writable
        assert 'buffer_duration' in writable
        assert 'is_active' in writable
        assert 'user_id' in writable

    def test_get_capacity_description_unlimited(self):
        """Test capacity description for unlimited capacity."""
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 0

        serializer = ResourceSerializer()
        description = serializer.get_capacity_description(mock_resource)

        assert description == "Unlimited capacity"

    def test_get_capacity_description_exclusive(self):
        """Test capacity description for exclusive use."""
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 1

        serializer = ResourceSerializer()
        description = serializer.get_capacity_description(mock_resource)

        assert description == "Exclusive use (1 at a time)"

    def test_get_capacity_description_concurrent(self):
        """Test capacity description for concurrent events."""
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 5

        serializer = ResourceSerializer()
        description = serializer.get_capacity_description(mock_resource)

        assert description == "Up to 5 concurrent events"

    def test_to_representation_includes_user_id(self):
        """Test that to_representation includes user_id."""
        mock_resource = Mock()
        mock_resource.id = 1
        mock_resource.name = "Test Resource"
        mock_resource.type = 1
        mock_resource.description = "Test"
        mock_resource.user_id = 42
        mock_resource.max_concurrent_events = 1
        mock_resource.buffer_duration = timedelta(minutes=15)
        mock_resource.saved_lane_count = 1
        mock_resource.is_active = True
        mock_resource.is_archived_by_quota = False
        mock_resource.user_can_edit_schedule = False
        mock_resource.created_at = datetime.now()
        mock_resource.updated_at = datetime.now()
        mock_resource.user = None

        serializer = ResourceSerializer()
        with patch.object(serializer, 'get_capacity_description', return_value="Test"):
            result = serializer.to_representation(mock_resource)

        assert result['user_id'] == 42


class TestEventSerializer:
    """Test EventSerializer."""

    def test_read_only_fields(self):
        """Test that correct fields are read-only."""
        serializer = EventSerializer()

        assert serializer.fields['created_at'].read_only
        assert serializer.fields['updated_at'].read_only
        assert serializer.fields['created_by'].read_only
        assert serializer.fields['participants'].read_only
        assert serializer.fields['duration_minutes'].read_only
        assert serializer.fields['resource_id'].read_only
        assert serializer.fields['customer_id'].read_only
        assert serializer.fields['service_id'].read_only
        assert serializer.fields['customer_name'].read_only
        assert serializer.fields['service_name'].read_only
        assert serializer.fields['is_paid'].read_only

    def test_write_only_fields(self):
        """Test that correct fields are write-only."""
        serializer = EventSerializer()

        assert serializer.fields['resource_ids'].write_only
        assert serializer.fields['staff_ids'].write_only
        assert serializer.fields['customer'].write_only
        assert serializer.fields['service'].write_only

    def test_get_duration_minutes(self):
        """Test duration_minutes calculation."""
        mock_event = Mock()
        mock_event.duration = timedelta(minutes=90)

        serializer = EventSerializer()
        duration = serializer.get_duration_minutes(mock_event)

        assert duration == 90

    def test_get_resource_id_returns_first_resource(self):
        """Test that get_resource_id returns first resource participant."""
        mock_participant = Mock()
        mock_participant.object_id = 123

        mock_event = Mock()
        mock_event.participants.filter.return_value.first.return_value = mock_participant

        serializer = EventSerializer()
        resource_id = serializer.get_resource_id(mock_event)

        assert resource_id == 123
        mock_event.participants.filter.assert_called_with(role='RESOURCE')

    def test_get_resource_id_returns_none_when_no_resource(self):
        """Test that get_resource_id returns None when no resource participant."""
        mock_event = Mock()
        mock_event.participants.filter.return_value.first.return_value = None

        serializer = EventSerializer()
        resource_id = serializer.get_resource_id(mock_event)

        assert resource_id is None

    def test_get_customer_id_returns_customer(self):
        """Test that get_customer_id returns customer participant."""
        mock_participant = Mock()
        mock_participant.object_id = 456

        mock_event = Mock()
        mock_event.participants.filter.return_value.first.return_value = mock_participant

        serializer = EventSerializer()
        customer_id = serializer.get_customer_id(mock_event)

        assert customer_id == 456
        mock_event.participants.filter.assert_called_with(role='CUSTOMER')

    def test_get_customer_name_from_participant(self):
        """Test customer name extraction from participant."""
        mock_user = Mock()
        mock_user.full_name = "John Doe"

        mock_participant = Mock()
        mock_participant.content_object = mock_user

        mock_event = Mock()
        mock_event.participants.filter.return_value.first.return_value = mock_participant
        mock_event.title = "Test Event"

        serializer = EventSerializer()
        name = serializer.get_customer_name(mock_event)

        assert name == "John Doe"

    def test_get_customer_name_fallback_to_title(self):
        """Test customer name fallback when no participant."""
        mock_event = Mock()
        mock_event.participants.filter.return_value.first.return_value = None
        mock_event.title = "Jane Smith - Haircut"

        serializer = EventSerializer()
        name = serializer.get_customer_name(mock_event)

        assert name == "Jane Smith"

    def test_get_service_name_from_service_fk(self):
        """Test service name from service foreign key."""
        mock_service = Mock()
        mock_service.name = "Haircut"

        mock_event = Mock()
        mock_event.service = mock_service
        mock_event.title = "Test"

        serializer = EventSerializer()
        name = serializer.get_service_name(mock_event)

        assert name == "Haircut"

    def test_get_service_name_fallback_to_title(self):
        """Test service name fallback when no service FK."""
        mock_event = Mock()
        mock_event.service = None
        mock_event.title = "Customer - Massage"

        serializer = EventSerializer()
        name = serializer.get_service_name(mock_event)

        assert name == "Massage"

    def test_get_is_paid_returns_true_for_paid_status(self):
        """Test is_paid returns True for PAID status."""
        mock_event = Mock()
        mock_event.status = 'PAID'

        serializer = EventSerializer()
        is_paid = serializer.get_is_paid(mock_event)

        assert is_paid is True

    def test_get_is_paid_returns_false_for_other_status(self):
        """Test is_paid returns False for non-PAID status."""
        mock_event = Mock()
        mock_event.status = 'SCHEDULED'

        serializer = EventSerializer()
        is_paid = serializer.get_is_paid(mock_event)

        assert is_paid is False

    def test_validate_status_maps_pending_to_scheduled(self):
        """Test status mapping from PENDING to SCHEDULED."""
        serializer = EventSerializer()
        result = serializer.validate_status('PENDING')

        assert result == 'SCHEDULED'

    def test_validate_status_maps_confirmed_to_scheduled(self):
        """Test status mapping from CONFIRMED to SCHEDULED."""
        serializer = EventSerializer()
        result = serializer.validate_status('CONFIRMED')

        assert result == 'SCHEDULED'

    def test_validate_status_maps_cancelled_to_canceled(self):
        """Test status mapping from CANCELLED to CANCELED."""
        serializer = EventSerializer()
        result = serializer.validate_status('CANCELLED')

        assert result == 'CANCELED'

    def test_validate_status_maps_no_show_to_noshow(self):
        """Test status mapping from NO_SHOW to NOSHOW."""
        serializer = EventSerializer()
        result = serializer.validate_status('NO_SHOW')

        assert result == 'NOSHOW'

    def test_to_representation_maps_scheduled_to_confirmed(self):
        """Test reverse status mapping in serialization."""
        mock_event = Mock()
        mock_event.id = 1
        mock_event.status = 'SCHEDULED'
        mock_event.participants.all.return_value = []

        serializer = EventSerializer()
        with patch('smoothschedule.scheduling.schedule.serializers.EventSerializer.get_duration_minutes', return_value=60):
            with patch.object(serializer.__class__.__bases__[0], 'to_representation', return_value={'status': 'SCHEDULED'}):
                result = serializer.to_representation(mock_event)

        assert result['status'] == 'CONFIRMED'

    def test_validate_rejects_end_before_start(self):
        """Test validation rejects end_time before start_time."""
        serializer = EventSerializer()
        attrs = {
            'start_time': datetime(2024, 1, 1, 10, 0),
            'end_time': datetime(2024, 1, 1, 9, 0),
            'resource_ids': []
        }

        with pytest.raises(Exception) as exc_info:
            serializer.validate(attrs)

        assert 'end_time' in str(exc_info.value)

    def test_validate_rejects_past_events_for_new_events(self):
        """Test validation rejects past start times for new events."""
        from django.utils import timezone
        serializer = EventSerializer()
        past_time = timezone.now() - timedelta(days=1)
        attrs = {
            'start_time': past_time,
            'end_time': past_time + timedelta(hours=1),
            'resource_ids': []
        }

        with pytest.raises(Exception) as exc_info:
            serializer.validate(attrs)

        assert 'start_time' in str(exc_info.value) or 'past' in str(exc_info.value).lower()


class TestParticipantSerializer:
    """Test ParticipantSerializer."""

    def test_read_only_fields(self):
        """Test that correct fields are read-only."""
        serializer = ParticipantSerializer()

        assert serializer.fields['created_at'].read_only
        assert serializer.fields['content_type_str'].read_only
        assert serializer.fields['participant_display'].read_only

    def test_get_content_type_str(self):
        """Test content_type_str returns string representation."""
        mock_content_type = Mock()
        mock_content_type.__str__ = Mock(return_value="resource")

        mock_participant = Mock()
        mock_participant.content_type = mock_content_type

        serializer = ParticipantSerializer()
        result = serializer.get_content_type_str(mock_participant)

        assert result == "resource"

    def test_get_participant_display_with_object(self):
        """Test participant_display with valid content_object."""
        mock_content = Mock()
        mock_content.__str__ = Mock(return_value="John Doe")

        mock_participant = Mock()
        mock_participant.content_object = mock_content

        serializer = ParticipantSerializer()
        result = serializer.get_participant_display(mock_participant)

        assert result == "John Doe"

    def test_get_participant_display_without_object(self):
        """Test participant_display when content_object is None."""
        mock_participant = Mock()
        mock_participant.content_object = None

        serializer = ParticipantSerializer()
        result = serializer.get_participant_display(mock_participant)

        assert result is None


class TestTimeBlockSerializer:
    """Test TimeBlockSerializer."""

    def test_read_only_fields(self):
        """Test that correct fields are read-only."""
        serializer = TimeBlockSerializer()

        assert serializer.fields['created_by'].read_only
        assert serializer.fields['created_at'].read_only
        assert serializer.fields['updated_at'].read_only
        assert serializer.fields['reviewed_by'].read_only
        assert serializer.fields['reviewed_at'].read_only
        assert serializer.fields['resource_name'].read_only
        assert serializer.fields['created_by_name'].read_only
        assert serializer.fields['reviewed_by_name'].read_only
        assert serializer.fields['level'].read_only
        assert serializer.fields['pattern_display'].read_only
        assert serializer.fields['holiday_name'].read_only
        assert serializer.fields['conflict_count'].read_only

    def test_get_created_by_name_with_user(self):
        """Test created_by_name with valid user."""
        mock_user = Mock()
        mock_user.get_full_name.return_value = "John Doe"

        mock_block = Mock()
        mock_block.created_by = mock_user

        serializer = TimeBlockSerializer()
        name = serializer.get_created_by_name(mock_block)

        assert name == "John Doe"

    def test_get_created_by_name_without_user(self):
        """Test created_by_name when no user."""
        mock_block = Mock()
        mock_block.created_by = None

        serializer = TimeBlockSerializer()
        name = serializer.get_created_by_name(mock_block)

        assert name is None

    def test_get_reviewed_by_name_with_user(self):
        """Test reviewed_by_name with valid user."""
        mock_user = Mock()
        mock_user.get_full_name.return_value = "Jane Smith"

        mock_block = Mock()
        mock_block.reviewed_by = mock_user

        serializer = TimeBlockSerializer()
        name = serializer.get_reviewed_by_name(mock_block)

        assert name == "Jane Smith"

    def test_get_level_returns_business_when_no_resource(self):
        """Test level returns 'business' when no resource."""
        mock_block = Mock()
        mock_block.resource = None

        serializer = TimeBlockSerializer()
        level = serializer.get_level(mock_block)

        assert level == 'business'

    def test_get_level_returns_resource_when_has_resource(self):
        """Test level returns 'resource' when resource exists."""
        mock_block = Mock()
        mock_block.resource = Mock(id=1)

        serializer = TimeBlockSerializer()
        level = serializer.get_level(mock_block)

        assert level == 'resource'

    def test_get_pattern_display_one_time_single_day(self):
        """Test pattern display for single-day one-time block."""
        mock_block = Mock()
        mock_block.recurrence_type = 'NONE'
        mock_block.start_date = date(2024, 12, 25)
        mock_block.end_date = date(2024, 12, 25)

        serializer = TimeBlockSerializer()
        pattern = serializer.get_pattern_display(mock_block)

        assert "December 25, 2024" in pattern

    def test_get_pattern_display_weekly(self):
        """Test pattern display for weekly recurrence."""
        mock_block = Mock()
        mock_block.recurrence_type = 'WEEKLY'
        mock_block.recurrence_pattern = {'days_of_week': [0, 2, 4]}  # Mon, Wed, Fri

        serializer = TimeBlockSerializer()
        pattern = serializer.get_pattern_display(mock_block)

        assert "Weekly" in pattern
        assert "Mon" in pattern
        assert "Wed" in pattern
        assert "Fri" in pattern

    def test_get_pattern_display_monthly(self):
        """Test pattern display for monthly recurrence."""
        mock_block = Mock()
        mock_block.recurrence_type = 'MONTHLY'
        mock_block.recurrence_pattern = {'days_of_month': [1, 15]}

        serializer = TimeBlockSerializer()
        pattern = serializer.get_pattern_display(mock_block)

        assert "Monthly" in pattern
        assert "1st" in pattern
        assert "15th" in pattern

    def test_validate_none_type_requires_start_date(self):
        """Test NONE recurrence type requires start_date."""
        serializer = TimeBlockSerializer()
        attrs = {
            'recurrence_type': 'NONE',
            'recurrence_pattern': {}
        }

        with pytest.raises(Exception) as exc_info:
            serializer.validate(attrs)

        assert 'start_date' in str(exc_info.value)

    def test_validate_weekly_requires_days_of_week(self):
        """Test WEEKLY recurrence requires days_of_week."""
        serializer = TimeBlockSerializer()
        attrs = {
            'recurrence_type': 'WEEKLY',
            'recurrence_pattern': {}
        }

        with pytest.raises(Exception) as exc_info:
            serializer.validate(attrs)

        assert 'days_of_week' in str(exc_info.value)

    def test_validate_weekly_rejects_invalid_days(self):
        """Test WEEKLY validation rejects invalid day numbers."""
        serializer = TimeBlockSerializer()
        attrs = {
            'recurrence_type': 'WEEKLY',
            'recurrence_pattern': {'days_of_week': [0, 7]}  # 7 is invalid
        }

        with pytest.raises(Exception) as exc_info:
            serializer.validate(attrs)

        assert 'days_of_week' in str(exc_info.value)

    def test_validate_monthly_requires_days_of_month(self):
        """Test MONTHLY recurrence requires days_of_month."""
        serializer = TimeBlockSerializer()
        attrs = {
            'recurrence_type': 'MONTHLY',
            'recurrence_pattern': {}
        }

        with pytest.raises(Exception) as exc_info:
            serializer.validate(attrs)

        assert 'days_of_month' in str(exc_info.value)

    def test_validate_monthly_rejects_invalid_days(self):
        """Test MONTHLY validation rejects invalid day numbers."""
        serializer = TimeBlockSerializer()
        attrs = {
            'recurrence_type': 'MONTHLY',
            'recurrence_pattern': {'days_of_month': [1, 32]}  # 32 is invalid
        }

        with pytest.raises(Exception) as exc_info:
            serializer.validate(attrs)

        assert 'days_of_month' in str(exc_info.value)

    def test_validate_yearly_requires_month_and_day(self):
        """Test YEARLY recurrence requires month and day."""
        serializer = TimeBlockSerializer()
        attrs = {
            'recurrence_type': 'YEARLY',
            'recurrence_pattern': {'month': 12}  # Missing day
        }

        with pytest.raises(Exception) as exc_info:
            serializer.validate(attrs)

        assert 'month and day' in str(exc_info.value)

    def test_validate_yearly_rejects_invalid_month(self):
        """Test YEARLY validation rejects invalid month."""
        serializer = TimeBlockSerializer()
        attrs = {
            'recurrence_type': 'YEARLY',
            'recurrence_pattern': {'month': 13, 'day': 1}
        }

        with pytest.raises(Exception) as exc_info:
            serializer.validate(attrs)

        assert 'month' in str(exc_info.value)

    def test_validate_not_all_day_requires_times(self):
        """Test non-all-day blocks require start and end times."""
        serializer = TimeBlockSerializer()
        attrs = {
            'recurrence_type': 'NONE',
            'start_date': date(2024, 1, 1),
            'all_day': False,
            'recurrence_pattern': {}
        }

        with pytest.raises(Exception) as exc_info:
            serializer.validate(attrs)

        assert 'start_time' in str(exc_info.value)

    def test_validate_end_time_after_start_time(self):
        """Test end_time must be after start_time."""
        serializer = TimeBlockSerializer()
        attrs = {
            'recurrence_type': 'NONE',
            'start_date': date(2024, 1, 1),
            'all_day': False,
            'start_time': time(10, 0),
            'end_time': time(9, 0),  # Before start
            'recurrence_pattern': {}
        }

        with pytest.raises(Exception) as exc_info:
            serializer.validate(attrs)

        assert 'end_time' in str(exc_info.value)


class TestHolidaySerializer:
    """Test HolidaySerializer."""

    def test_all_fields_read_only(self):
        """Test that all fields are read-only (reference data)."""
        serializer = HolidaySerializer()

        # Holidays are reference data, all fields should be read-only
        for field_name, field in serializer.fields.items():
            assert field.read_only, f"Field {field_name} should be read-only"

    def test_get_next_occurrence_current_year(self):
        """Test next_occurrence for holiday later this year."""
        mock_holiday = Mock()
        mock_holiday.get_date_for_year = Mock()

        # Set up mock to return future date for current year
        today = date.today()
        future_date = date(today.year, 12, 25)
        mock_holiday.get_date_for_year.return_value = future_date

        serializer = HolidaySerializer()
        result = serializer.get_next_occurrence(mock_holiday)

        assert result == future_date.isoformat()

    def test_get_next_occurrence_next_year(self):
        """Test next_occurrence falls back to next year if past."""
        mock_holiday = Mock()

        today = date.today()
        past_date = date(today.year, 1, 1)
        next_year_date = date(today.year + 1, 1, 1)

        # Return past date for current year, future date for next year
        mock_holiday.get_date_for_year = Mock(side_effect=[past_date, next_year_date])

        serializer = HolidaySerializer()
        result = serializer.get_next_occurrence(mock_holiday)

        assert result == next_year_date.isoformat()


class TestPluginInstallationSerializer:
    """Test PluginInstallationSerializer."""

    def test_read_only_fields(self):
        """Test that correct fields are read-only."""
        serializer = PluginInstallationSerializer()

        assert serializer.fields['id'].read_only
        assert serializer.fields['installed_by'].read_only
        assert serializer.fields['installed_by_name'].read_only
        assert serializer.fields['installed_at'].read_only
        assert serializer.fields['template_version_hash'].read_only
        assert serializer.fields['reviewed_at'].read_only
        assert serializer.fields['template_name'].read_only
        assert serializer.fields['template_slug'].read_only
        assert serializer.fields['has_update'].read_only

    def test_get_installed_by_name_with_user(self):
        """Test installed_by_name with valid user."""
        mock_user = Mock()
        mock_user.get_full_name.return_value = "Alice Jones"
        mock_user.username = "alice"

        mock_installation = Mock()
        mock_installation.installed_by = mock_user

        serializer = PluginInstallationSerializer()
        name = serializer.get_installed_by_name(mock_installation)

        assert name == "Alice Jones"

    def test_get_installed_by_name_falls_back_to_username(self):
        """Test installed_by_name falls back to username."""
        mock_user = Mock()
        mock_user.get_full_name.return_value = ""
        mock_user.username = "bob"

        mock_installation = Mock()
        mock_installation.installed_by = mock_user

        serializer = PluginInstallationSerializer()
        name = serializer.get_installed_by_name(mock_installation)

        assert name == "bob"

    def test_get_installed_by_name_without_user(self):
        """Test installed_by_name when no user."""
        mock_installation = Mock()
        mock_installation.installed_by = None

        serializer = PluginInstallationSerializer()
        name = serializer.get_installed_by_name(mock_installation)

        assert name is None

    def test_get_has_update_calls_model_method(self):
        """Test has_update calls the model's method."""
        mock_installation = Mock()
        mock_installation.has_update_available.return_value = True

        serializer = PluginInstallationSerializer()
        has_update = serializer.get_has_update(mock_installation)

        assert has_update is True
        mock_installation.has_update_available.assert_called_once()


class TestEmailTemplateSerializer:
    """Test EmailTemplateSerializer."""

    def test_read_only_fields(self):
        """Test that correct fields are read-only."""
        serializer = EmailTemplateSerializer()

        assert serializer.fields['created_at'].read_only
        assert serializer.fields['updated_at'].read_only
        assert serializer.fields['created_by'].read_only
        assert serializer.fields['created_by_name'].read_only

    def test_writable_fields(self):
        """Test that correct fields are writable."""
        serializer = EmailTemplateSerializer()
        writable = [f for f in serializer.fields if not serializer.fields[f].read_only]

        assert 'name' in writable
        assert 'description' in writable
        assert 'subject' in writable
        assert 'html_content' in writable
        assert 'text_content' in writable
        assert 'scope' in writable
        assert 'is_default' in writable
        assert 'category' in writable
        assert 'preview_context' in writable

    def test_get_created_by_name_with_full_name(self):
        """Test created_by_name with full name."""
        mock_user = Mock()
        mock_user.full_name = "Sarah Wilson"
        mock_user.username = "swilson"

        mock_template = Mock()
        mock_template.created_by = mock_user

        serializer = EmailTemplateSerializer()
        name = serializer.get_created_by_name(mock_template)

        assert name == "Sarah Wilson"

    def test_get_created_by_name_falls_back_to_username(self):
        """Test created_by_name falls back to username."""
        mock_user = Mock()
        mock_user.full_name = None
        mock_user.username = "tsmith"

        mock_template = Mock()
        mock_template.created_by = mock_user

        serializer = EmailTemplateSerializer()
        name = serializer.get_created_by_name(mock_template)

        assert name == "tsmith"

    def test_get_created_by_name_without_user(self):
        """Test created_by_name when no user."""
        mock_template = Mock()
        mock_template.created_by = None

        serializer = EmailTemplateSerializer()
        name = serializer.get_created_by_name(mock_template)

        assert name is None

    def test_validate_rejects_empty_content(self):
        """Test validation rejects templates with no content."""
        serializer = EmailTemplateSerializer()
        attrs = {
            'html_content': '',
            'text_content': ''
        }

        with pytest.raises(Exception) as exc_info:
            serializer.validate(attrs)

        assert 'content' in str(exc_info.value).lower()

    def test_validate_allows_html_only(self):
        """Test validation allows HTML-only templates."""
        serializer = EmailTemplateSerializer()
        attrs = {
            'html_content': '<p>Hello</p>',
            'text_content': ''
        }

        result = serializer.validate(attrs)
        assert result == attrs

    def test_validate_allows_text_only(self):
        """Test validation allows text-only templates."""
        serializer = EmailTemplateSerializer()
        attrs = {
            'html_content': '',
            'text_content': 'Hello there'
        }

        result = serializer.validate(attrs)
        assert result == attrs

    def test_validate_allows_both_content_types(self):
        """Test validation allows both HTML and text."""
        serializer = EmailTemplateSerializer()
        attrs = {
            'html_content': '<p>Hello</p>',
            'text_content': 'Hello there'
        }

        result = serializer.validate(attrs)
        assert result == attrs
