"""
Unit tests for AvailabilityService.

Tests the resource availability checking logic with mocks to avoid database hits.
"""
from datetime import datetime, timedelta, timezone as dt_timezone
from unittest.mock import Mock, patch, MagicMock
from django.utils import timezone
import pytest

from smoothschedule.scheduling.schedule.services import AvailabilityService


class TestAvailabilityServiceBasicChecks:
    """Test basic availability checking logic."""

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_available_when_no_overlapping_events(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that resource is available when no overlapping events exist."""
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 2
        mock_resource.buffer_duration = timedelta(minutes=0)

        start = timezone.now()
        end = start + timedelta(hours=1)

        # Mock no time blocks
        mock_timeblock.objects.filter.return_value.order_by.return_value = []

        # Mock ContentType
        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)

        # Mock no overlapping events
        mock_participant.objects.filter.return_value.select_related.return_value = []

        # Act
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, start, end
        )

        # Assert
        assert is_available is True
        assert "1/2" in reason  # Shows slot count
        assert warnings == []

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_unavailable_when_capacity_exceeded(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that resource is unavailable when capacity is exceeded."""
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 1
        mock_resource.buffer_duration = timedelta(minutes=0)

        start = timezone.now()
        end = start + timedelta(hours=1)

        # Mock no time blocks
        mock_timeblock.objects.filter.return_value.order_by.return_value = []

        # Mock ContentType
        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)

        # Mock one overlapping event (at capacity)
        mock_event = Mock()
        mock_event.id = 1
        mock_event.status = 'SCHEDULED'
        mock_event.start_time = start
        mock_event.end_time = end

        mock_participant_obj = Mock()
        mock_participant_obj.event = mock_event

        mock_participant.objects.filter.return_value.select_related.return_value = [
            mock_participant_obj
        ]

        # Act
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, start, end
        )

        # Assert
        assert is_available is False
        assert "capacity exceeded" in reason.lower()
        assert "1/1" in reason

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_unlimited_capacity_always_available(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that unlimited capacity (0) resources are always available."""
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 0  # Unlimited
        mock_resource.buffer_duration = timedelta(minutes=0)

        start = timezone.now()
        end = start + timedelta(hours=1)

        # Mock no time blocks
        mock_timeblock.objects.filter.return_value.order_by.return_value = []

        # Mock ContentType
        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)

        # Mock many overlapping events
        mock_events = []
        for i in range(10):
            mock_event = Mock()
            mock_event.id = i
            mock_event.status = 'SCHEDULED'
            mock_event.start_time = start
            mock_event.end_time = end
            mock_participant_obj = Mock()
            mock_participant_obj.event = mock_event
            mock_events.append(mock_participant_obj)

        mock_participant.objects.filter.return_value.select_related.return_value = mock_events

        # Act
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, start, end
        )

        # Assert
        assert is_available is True
        assert "unlimited" in reason.lower()

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_cancelled_events_not_counted(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that cancelled events don't count toward capacity."""
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 1
        mock_resource.buffer_duration = timedelta(minutes=0)

        start = timezone.now()
        end = start + timedelta(hours=1)

        # Mock no time blocks
        mock_timeblock.objects.filter.return_value.order_by.return_value = []

        # Mock ContentType
        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)

        # Mock cancelled event (should be ignored)
        mock_event = Mock()
        mock_event.id = 1
        mock_event.status = 'CANCELED'  # Cancelled status
        mock_event.start_time = start
        mock_event.end_time = end

        mock_participant_obj = Mock()
        mock_participant_obj.event = mock_event

        mock_participant.objects.filter.return_value.select_related.return_value = [
            mock_participant_obj
        ]

        # Act
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, start, end
        )

        # Assert
        assert is_available is True  # Available because cancelled events ignored

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_exclude_event_id_not_counted(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that the event being updated is excluded from capacity check."""
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 1
        mock_resource.buffer_duration = timedelta(minutes=0)

        start = timezone.now()
        end = start + timedelta(hours=1)

        # Mock no time blocks
        mock_timeblock.objects.filter.return_value.order_by.return_value = []

        # Mock ContentType
        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)

        # Mock event with ID 5
        mock_event = Mock()
        mock_event.id = 5
        mock_event.status = 'SCHEDULED'
        mock_event.start_time = start
        mock_event.end_time = end

        mock_participant_obj = Mock()
        mock_participant_obj.event = mock_event

        mock_participant.objects.filter.return_value.select_related.return_value = [
            mock_participant_obj
        ]

        # Act - exclude event 5 (updating it)
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, start, end, exclude_event_id=5
        )

        # Assert
        assert is_available is True  # Available because we're updating event 5


class TestTimeBlockChecking:
    """Test time block checking logic."""

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_hard_block_makes_unavailable(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that hard blocks make resource unavailable."""
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 10
        mock_resource.buffer_duration = timedelta(minutes=0)
        mock_resource.name = "Room A"

        start = timezone.now()
        end = start + timedelta(hours=1)

        # Mock hard time block
        mock_block = Mock()
        mock_block.block_type = 'HARD'
        mock_block.title = "Maintenance"
        mock_block.resource = mock_resource
        mock_block.blocks_datetime_range.return_value = True
        mock_block.is_business_hours = False

        mock_timeblock.objects.filter.return_value.order_by.return_value = [mock_block]
        mock_timeblock.BlockType.HARD = 'HARD'

        # Mock no events
        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)
        mock_participant.objects.filter.return_value.select_related.return_value = []

        # Act
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, start, end
        )

        # Assert
        assert is_available is False
        assert "maintenance" in reason.lower()

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_soft_block_returns_warning(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that soft blocks return warnings but still allow booking."""
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 10
        mock_resource.buffer_duration = timedelta(minutes=0)
        mock_resource.name = "Room A"

        start = timezone.now()
        end = start + timedelta(hours=1)

        # Mock soft time block
        mock_block = Mock()
        mock_block.block_type = 'SOFT'
        mock_block.title = "Reduced Staff"
        mock_block.resource = mock_resource
        mock_block.blocks_datetime_range.return_value = True
        mock_block.is_business_hours = False

        mock_timeblock.objects.filter.return_value.order_by.return_value = [mock_block]
        mock_timeblock.BlockType.HARD = 'HARD'

        # Mock no events
        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)
        mock_participant.objects.filter.return_value.select_related.return_value = []

        # Act
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, start, end
        )

        # Assert
        assert is_available is True  # Still available
        assert len(warnings) == 1
        assert "reduced staff" in warnings[0].lower()


class TestOverlapLogic:
    """Test event overlap detection logic."""

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_non_overlapping_events_not_counted(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that events before or after don't count as overlapping."""
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 1
        mock_resource.buffer_duration = timedelta(minutes=0)

        now = timezone.now()
        query_start = now
        query_end = now + timedelta(hours=1)

        # Mock no time blocks
        mock_timeblock.objects.filter.return_value.order_by.return_value = []

        # Mock ContentType
        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)

        # Mock event that ends before our query
        mock_event = Mock()
        mock_event.id = 1
        mock_event.status = 'SCHEDULED'
        mock_event.start_time = now - timedelta(hours=2)
        mock_event.end_time = now - timedelta(hours=1)  # Ends before query start

        mock_participant_obj = Mock()
        mock_participant_obj.event = mock_event

        mock_participant.objects.filter.return_value.select_related.return_value = [
            mock_participant_obj
        ]

        # Act
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, query_start, query_end
        )

        # Assert
        assert is_available is True  # No overlap

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_buffer_duration_extends_overlap_check(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that buffer duration extends the overlap check window."""
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 1
        mock_resource.buffer_duration = timedelta(minutes=15)  # 15 min buffer

        now = timezone.now()
        query_start = now
        query_end = now + timedelta(hours=1)

        # Mock no time blocks
        mock_timeblock.objects.filter.return_value.order_by.return_value = []

        # Mock ContentType
        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)

        # Mock event that ends 5 minutes before query start
        # Without buffer: no overlap
        # With 15 min buffer: should overlap
        mock_event = Mock()
        mock_event.id = 1
        mock_event.status = 'SCHEDULED'
        mock_event.start_time = now - timedelta(hours=1)
        mock_event.end_time = now - timedelta(minutes=5)  # Ends 5 min before query

        mock_participant_obj = Mock()
        mock_participant_obj.event = mock_event

        mock_participant.objects.filter.return_value.select_related.return_value = [
            mock_participant_obj
        ]

        # Act
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, query_start, query_end
        )

        # Assert
        # With 15 min buffer, query window becomes (now-15min to now+1hr+15min)
        # Event ends at now-5min which is > now-15min, so there IS overlap
        assert is_available is False


class TestSimpleAvailabilityCheck:
    """Test the backwards-compatible simple check method."""

    @patch.object(AvailabilityService, 'check_availability')
    def test_simple_check_returns_tuple(self, mock_check):
        """Test that simple check returns (bool, str) tuple."""
        # Arrange
        mock_resource = Mock()
        start = timezone.now()
        end = start + timedelta(hours=1)

        mock_check.return_value = (True, "Available", ["Warning 1"])

        # Act
        is_available, reason = AvailabilityService.check_availability_simple(
            mock_resource, start, end
        )

        # Assert
        assert is_available is True
        assert reason == "Available"
        # Warnings are not returned in simple mode


class TestSendEmailAutomation:
    """Test SendEmailAutomation execution logic."""

    @patch('smoothschedule.scheduling.automations.builtin.send_mail')
    @patch('smoothschedule.scheduling.automations.builtin.settings')
    def test_execute_sends_email_successfully(self, mock_settings, mock_send_mail):
        """Test that plugin sends email with correct parameters."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import SendEmailAutomation

        mock_settings.DEFAULT_FROM_EMAIL = 'noreply@example.com'

        config = {
            'recipients': ['user@example.com', 'admin@example.com'],
            'subject': 'Test Subject',
            'message': 'Test message body',
        }
        plugin = SendEmailAutomation(config=config)
        context = {}

        # Act
        result = plugin.execute(context)

        # Assert
        mock_send_mail.assert_called_once_with(
            subject='Test Subject',
            message='Test message body',
            from_email='noreply@example.com',
            recipient_list=['user@example.com', 'admin@example.com'],
            fail_silently=False,
        )
        assert result['success'] is True
        assert result['message'] == 'Email sent to 2 recipient(s)'
        assert result['data']['recipient_count'] == 2

    @patch('smoothschedule.scheduling.automations.builtin.send_mail')
    @patch('smoothschedule.scheduling.automations.builtin.settings')
    def test_execute_uses_custom_from_email(self, mock_settings, mock_send_mail):
        """Test that custom from_email is used when provided."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import SendEmailAutomation

        mock_settings.DEFAULT_FROM_EMAIL = 'noreply@example.com'

        config = {
            'recipients': ['user@example.com'],
            'subject': 'Test',
            'message': 'Body',
            'from_email': 'custom@example.com',
        }
        plugin = SendEmailAutomation(config=config)

        # Act
        result = plugin.execute({})

        # Assert
        mock_send_mail.assert_called_once()
        assert mock_send_mail.call_args[1]['from_email'] == 'custom@example.com'

    def test_execute_raises_error_when_no_recipients(self):
        """Test that plugin raises error when no recipients specified."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import SendEmailAutomation
        from smoothschedule.scheduling.automations.registry import AutomationExecutionError as PluginExecutionError

        config = {
            'recipients': [],
            'subject': 'Test',
            'message': 'Body',
        }
        plugin = SendEmailAutomation(config=config)

        # Act & Assert
        with pytest.raises(PluginExecutionError) as exc_info:
            plugin.execute({})
        assert 'No recipients specified' in str(exc_info.value)

    @patch('smoothschedule.scheduling.automations.builtin.send_mail')
    def test_execute_raises_error_on_send_failure(self, mock_send_mail):
        """Test that plugin raises PluginExecutionError on send failure."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import SendEmailAutomation
        from smoothschedule.scheduling.automations.registry import AutomationExecutionError as PluginExecutionError

        mock_send_mail.side_effect = Exception('SMTP error')

        config = {
            'recipients': ['user@example.com'],
            'subject': 'Test',
            'message': 'Body',
        }
        plugin = SendEmailAutomation(config=config)

        # Act & Assert
        with pytest.raises(PluginExecutionError) as exc_info:
            plugin.execute({})
        assert 'Failed to send email' in str(exc_info.value)


class TestCleanupOldEventsAutomation:
    """Test CleanupOldEventsAutomation execution logic."""

    def test_execute_counts_events_in_dry_run_mode(self):
        """Test that dry run mode only counts events without deleting."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import CleanupOldEventsAutomation

        now = datetime(2024, 1, 15, 12, 0, tzinfo=dt_timezone.utc)

        config = {
            'days_old': 90,
            'statuses': ['COMPLETED', 'CANCELED'],
            'dry_run': True,
        }
        plugin = CleanupOldEventsAutomation(config=config)

        # Mock Event model
        mock_event_query = Mock()
        mock_event_query.count.return_value = 5

        with patch('smoothschedule.scheduling.automations.builtin.timezone') as mock_timezone:
            mock_timezone.now.return_value = now
            with patch('smoothschedule.scheduling.schedule.models.Event') as mock_event:
                mock_event.objects.filter.return_value = mock_event_query

                # Act
                result = plugin.execute({})

        # Assert
        mock_event_query.delete.assert_not_called()  # Dry run shouldn't delete
        assert result['success'] is True
        assert result['message'] == 'Found 5 old event(s) (dry run, not deleted)'
        assert result['data']['count'] == 5
        assert result['data']['dry_run'] is True

    def test_execute_deletes_events_when_not_dry_run(self):
        """Test that events are deleted when not in dry run mode."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import CleanupOldEventsAutomation

        now = datetime(2024, 1, 15, 12, 0, tzinfo=dt_timezone.utc)

        config = {
            'days_old': 30,
            'statuses': ['COMPLETED'],
            'dry_run': False,
        }
        plugin = CleanupOldEventsAutomation(config=config)

        # Mock Event model
        mock_event_query = Mock()
        mock_event_query.count.return_value = 3

        with patch('smoothschedule.scheduling.automations.builtin.timezone') as mock_timezone:
            mock_timezone.now.return_value = now
            with patch('smoothschedule.scheduling.schedule.models.Event') as mock_event:
                mock_event.objects.filter.return_value = mock_event_query

                # Act
                result = plugin.execute({})

        # Assert
        mock_event_query.delete.assert_called_once()
        assert result['success'] is True
        assert result['message'] == 'Deleted 3 old event(s)'
        assert result['data']['count'] == 3
        assert result['data']['dry_run'] is False

    def test_execute_uses_correct_cutoff_date(self):
        """Test that cutoff date is calculated correctly."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import CleanupOldEventsAutomation

        now = datetime(2024, 6, 15, 12, 0, tzinfo=dt_timezone.utc)

        config = {'days_old': 90}
        plugin = CleanupOldEventsAutomation(config=config)

        mock_event_query = Mock()
        mock_event_query.count.return_value = 0

        with patch('smoothschedule.scheduling.automations.builtin.timezone') as mock_timezone:
            mock_timezone.now.return_value = now
            with patch('smoothschedule.scheduling.schedule.models.Event') as mock_event:
                mock_event.objects.filter.return_value = mock_event_query

                # Act
                plugin.execute({})

                # Assert - verify filter called with correct cutoff (90 days ago)
                expected_cutoff = now - timedelta(days=90)
                filter_call = mock_event.objects.filter.call_args
                assert filter_call[1]['end_time__lt'] == expected_cutoff

    def test_execute_uses_default_values(self):
        """Test that default values are used when not specified."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import CleanupOldEventsAutomation

        now = datetime(2024, 1, 15, tzinfo=dt_timezone.utc)

        config = {}  # Empty config
        plugin = CleanupOldEventsAutomation(config=config)

        mock_event_query = Mock()
        mock_event_query.count.return_value = 0

        with patch('smoothschedule.scheduling.automations.builtin.timezone') as mock_timezone:
            mock_timezone.now.return_value = now
            with patch('smoothschedule.scheduling.schedule.models.Event') as mock_event:
                mock_event.objects.filter.return_value = mock_event_query

                # Act
                result = plugin.execute({})

                # Assert - check defaults: 90 days, ['COMPLETED', 'CANCELED'], dry_run=False
                filter_call = mock_event.objects.filter.call_args
                assert filter_call[1]['status__in'] == ['COMPLETED', 'CANCELED']
                assert result['data']['days_old'] == 90


class TestDailyReportAutomation:
    """Test DailyReportAutomation execution logic."""

    @patch('smoothschedule.scheduling.automations.builtin.send_mail')
    @patch('smoothschedule.scheduling.automations.builtin.timezone')
    @patch('smoothschedule.scheduling.automations.builtin.settings')
    def test_execute_sends_report_with_all_sections(self, mock_settings, mock_timezone, mock_send_mail):
        """Test that plugin generates and sends complete daily report."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import DailyReportAutomation

        mock_settings.DEFAULT_FROM_EMAIL = 'reports@example.com'

        now = timezone.make_aware(datetime(2024, 1, 15, 12, 0))
        mock_timezone.now.return_value = now
        mock_timezone.make_aware = timezone.make_aware
        mock_timezone.datetime = datetime

        config = {
            'recipients': ['manager@example.com'],
            'include_upcoming': True,
            'include_completed': True,
        }

        mock_business = Mock()
        mock_business.name = 'Test Business'
        context = {'business': mock_business}

        plugin = DailyReportAutomation(config=config)

        # Mock Event queries
        with patch('smoothschedule.scheduling.schedule.models.Event') as mock_event:
            mock_upcoming_query = Mock()
            mock_upcoming_query.count.return_value = 5

            mock_completed_query = Mock()
            mock_completed_query.count.return_value = 8

            mock_canceled_query = Mock()
            mock_canceled_query.count.return_value = 2

            # Setup side effects for multiple filter calls
            mock_event.objects.filter.side_effect = [
                mock_upcoming_query,
                mock_completed_query,
                mock_canceled_query,
            ]

            # Act
            result = plugin.execute(context)

        # Assert
        mock_send_mail.assert_called_once()
        call_args = mock_send_mail.call_args
        assert call_args[1]['subject'] == 'Daily Report - 2024-01-15'
        assert 'Test Business' in call_args[1]['message']
        assert "Today's Upcoming Appointments: 5" in call_args[1]['message']
        assert 'Completed: 8' in call_args[1]['message']
        assert 'Canceled: 2' in call_args[1]['message']
        assert call_args[1]['recipient_list'] == ['manager@example.com']

        assert result['success'] is True
        assert result['data']['recipient_count'] == 1

    def test_execute_raises_error_when_no_recipients(self):
        """Test that plugin raises error when no recipients specified."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import DailyReportAutomation
        from smoothschedule.scheduling.automations.registry import AutomationExecutionError as PluginExecutionError

        config = {'recipients': []}
        plugin = DailyReportAutomation(config=config)

        # Act & Assert
        with pytest.raises(PluginExecutionError) as exc_info:
            plugin.execute({})
        assert 'No recipients specified' in str(exc_info.value)

    @patch('smoothschedule.scheduling.automations.builtin.send_mail')
    @patch('smoothschedule.scheduling.automations.builtin.timezone')
    @patch('smoothschedule.scheduling.automations.builtin.settings')
    def test_execute_excludes_sections_based_on_config(self, mock_settings, mock_timezone, mock_send_mail):
        """Test that sections are excluded when config options are False."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import DailyReportAutomation

        mock_settings.DEFAULT_FROM_EMAIL = 'reports@example.com'

        now = timezone.make_aware(datetime(2024, 1, 15, 12, 0))
        mock_timezone.now.return_value = now
        mock_timezone.make_aware = timezone.make_aware
        mock_timezone.datetime = datetime

        config = {
            'recipients': ['manager@example.com'],
            'include_upcoming': False,
            'include_completed': False,
        }

        mock_business = Mock()
        mock_business.name = 'Test Business'
        context = {'business': mock_business}

        plugin = DailyReportAutomation(config=config)

        # Act
        result = plugin.execute(context)

        # Assert
        call_args = mock_send_mail.call_args
        message = call_args[1]['message']
        assert "Today's Upcoming Appointments" not in message
        assert "Yesterday's Summary" not in message
        assert "Test Business" in message  # Header still present

    @patch('smoothschedule.scheduling.automations.builtin.send_mail')
    def test_execute_raises_error_on_send_failure(self, mock_send_mail):
        """Test that plugin raises PluginExecutionError on send failure."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import DailyReportAutomation
        from smoothschedule.scheduling.automations.registry import AutomationExecutionError as PluginExecutionError

        mock_send_mail.side_effect = Exception('SMTP error')

        config = {'recipients': ['manager@example.com']}
        plugin = DailyReportAutomation(config=config)
        context = {'business': Mock(name='Test')}

        with patch('smoothschedule.scheduling.automations.builtin.timezone'):
            with patch('smoothschedule.scheduling.schedule.models.Event'):
                # Act & Assert
                with pytest.raises(PluginExecutionError) as exc_info:
                    plugin.execute(context)
                assert 'Failed to send report' in str(exc_info.value)


class TestAppointmentReminderAutomation:
    """Test AppointmentReminderAutomation execution logic."""

    @patch('smoothschedule.platform.admin.tasks.send_appointment_reminder_email')
    @patch('smoothschedule.scheduling.automations.builtin.timezone')
    @patch('smoothschedule.scheduling.automations.builtin.logger')
    def test_execute_queues_email_reminders(self, mock_logger, mock_timezone, mock_task):
        """Test that plugin queues email reminders for upcoming appointments."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import AppointmentReminderAutomation

        now = datetime(2024, 1, 15, 12, 0, tzinfo=dt_timezone.utc)
        mock_timezone.now.return_value = now

        config = {
            'hours_before': 24,
            'method': 'email',
        }
        plugin = AppointmentReminderAutomation(config=config)

        # Mock event with participants
        mock_customer = Mock()
        mock_customer.email = 'customer@example.com'

        mock_participant = Mock()
        mock_participant.customer = mock_customer

        mock_event = Mock()
        mock_event.id = 1
        mock_event.title = 'Haircut Appointment'
        mock_event.participants.all.return_value = [mock_participant]

        mock_event_query = Mock()
        mock_event_query.prefetch_related.return_value = [mock_event]

        with patch('smoothschedule.scheduling.schedule.models.Event') as mock_event_class:
            mock_event_class.objects.filter.return_value = mock_event_query
            mock_event_class.Status.SCHEDULED = 'SCHEDULED'

            # Act
            result = plugin.execute({})

        # Assert
        mock_task.delay.assert_called_once_with(
            event_id=1,
            customer_email='customer@example.com',
            hours_before=24
        )
        assert result['success'] is True
        assert result['data']['reminders_queued'] == 1
        assert result['data']['hours_before'] == 24
        assert result['data']['method'] == 'email'

    @patch('smoothschedule.platform.admin.tasks.send_appointment_reminder_email')
    @patch('smoothschedule.scheduling.automations.builtin.timezone')
    @patch('smoothschedule.scheduling.automations.builtin.logger')
    def test_execute_handles_multiple_participants(self, mock_logger, mock_timezone, mock_task):
        """Test that reminders are sent to all participants."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import AppointmentReminderAutomation

        now = datetime(2024, 1, 15, 12, 0, tzinfo=dt_timezone.utc)
        mock_timezone.now.return_value = now

        config = {'hours_before': 24, 'method': 'email'}
        plugin = AppointmentReminderAutomation(config=config)

        # Mock event with multiple participants
        mock_customer1 = Mock()
        mock_customer1.email = 'customer1@example.com'
        mock_participant1 = Mock()
        mock_participant1.customer = mock_customer1

        mock_customer2 = Mock()
        mock_customer2.email = 'customer2@example.com'
        mock_participant2 = Mock()
        mock_participant2.customer = mock_customer2

        mock_event = Mock()
        mock_event.id = 1
        mock_event.title = 'Group Session'
        mock_event.participants.all.return_value = [mock_participant1, mock_participant2]

        mock_event_query = Mock()
        mock_event_query.prefetch_related.return_value = [mock_event]

        with patch('smoothschedule.scheduling.schedule.models.Event') as mock_event_class:
            mock_event_class.objects.filter.return_value = mock_event_query
            mock_event_class.Status.SCHEDULED = 'SCHEDULED'

            # Act
            result = plugin.execute({})

        # Assert
        assert mock_task.delay.call_count == 2
        assert result['data']['reminders_queued'] == 2

    @patch('smoothschedule.platform.admin.tasks.send_appointment_reminder_email')
    @patch('smoothschedule.scheduling.automations.builtin.timezone')
    def test_execute_skips_participants_without_email(self, mock_timezone, mock_task):
        """Test that participants without email are skipped."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import AppointmentReminderAutomation

        now = datetime(2024, 1, 15, 12, 0, tzinfo=dt_timezone.utc)
        mock_timezone.now.return_value = now

        config = {'hours_before': 24, 'method': 'email'}
        plugin = AppointmentReminderAutomation(config=config)

        # Mock participant with no customer
        mock_participant1 = Mock()
        mock_participant1.customer = None

        # Mock participant with customer but no email
        mock_customer2 = Mock(spec=[])  # No email attribute
        mock_participant2 = Mock()
        mock_participant2.customer = mock_customer2

        mock_event = Mock()
        mock_event.id = 1
        mock_event.title = 'Test Event'
        mock_event.participants.all.return_value = [mock_participant1, mock_participant2]

        mock_event_query = Mock()
        mock_event_query.prefetch_related.return_value = [mock_event]

        with patch('smoothschedule.scheduling.schedule.models.Event') as mock_event_class:
            mock_event_class.objects.filter.return_value = mock_event_query
            mock_event_class.Status.SCHEDULED = 'SCHEDULED'

            # Act
            result = plugin.execute({})

        # Assert - no reminders sent
        mock_task.delay.assert_not_called()
        assert result['data']['reminders_queued'] == 0

    @patch('smoothschedule.platform.admin.tasks.send_appointment_reminder_email')
    @patch('smoothschedule.scheduling.automations.builtin.timezone')
    @patch('smoothschedule.scheduling.automations.builtin.logger')
    def test_execute_logs_sms_intent_for_sms_method(self, mock_logger, mock_timezone, mock_task):
        """Test that SMS method logs intent (not yet implemented)."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import AppointmentReminderAutomation

        now = datetime(2024, 1, 15, 12, 0, tzinfo=dt_timezone.utc)
        mock_timezone.now.return_value = now

        config = {'hours_before': 2, 'method': 'sms'}
        plugin = AppointmentReminderAutomation(config=config)

        mock_customer = Mock()
        mock_customer.email = 'customer@example.com'
        mock_participant = Mock()
        mock_participant.customer = mock_customer

        mock_event = Mock()
        mock_event.id = 1
        mock_event.title = 'Appointment'
        mock_event.participants.all.return_value = [mock_participant]

        mock_event_query = Mock()
        mock_event_query.prefetch_related.return_value = [mock_event]

        with patch('smoothschedule.scheduling.schedule.models.Event') as mock_event_class:
            mock_event_class.objects.filter.return_value = mock_event_query
            mock_event_class.Status.SCHEDULED = 'SCHEDULED'

            # Act
            result = plugin.execute({})

        # Assert - logger should have been called for SMS intent
        mock_logger.info.assert_called()
        assert result['data']['method'] == 'sms'


class TestBackupDatabaseAutomation:
    """Test BackupDatabaseAutomation execution logic."""

    @patch('smoothschedule.scheduling.automations.builtin.logger')
    def test_execute_returns_success_placeholder(self, mock_logger):
        """Test that plugin returns success (placeholder implementation)."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import BackupDatabaseAutomation

        config = {'compress': True}
        plugin = BackupDatabaseAutomation(config=config)

        mock_business = Mock()
        mock_business.name = 'Test Business'
        context = {'business': mock_business}

        # Act
        result = plugin.execute(context)

        # Assert
        assert result['success'] is True
        assert 'backup created successfully' in result['message'].lower()
        assert 'backup_file' in result['data']
        mock_logger.info.assert_called_once()

    def test_execute_with_custom_backup_location(self):
        """Test that custom backup location is accepted."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import BackupDatabaseAutomation

        config = {
            'backup_location': '/custom/path',
            'compress': False,
        }
        plugin = BackupDatabaseAutomation(config=config)

        context = {'business': Mock(name='Test')}

        # Act
        result = plugin.execute(context)

        # Assert
        assert result['success'] is True


class TestWebhookAutomation:
    """Test WebhookAutomation execution logic."""

    def test_execute_makes_post_request_successfully(self):
        """Test that plugin makes POST request with correct parameters."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import WebhookAutomation
        import requests

        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.text = 'Success response'

        config = {
            'url': 'https://api.example.com/webhook',
            'method': 'POST',
            'headers': {'Authorization': 'Bearer token123'},
            'payload': {'event': 'test', 'data': 'value'},
        }
        plugin = WebhookAutomation(config=config)

        with patch('requests.request', return_value=mock_response) as mock_request:
            # Act
            result = plugin.execute({})

            # Assert
            mock_request.assert_called_once_with(
                method='POST',
                url='https://api.example.com/webhook',
                json={'event': 'test', 'data': 'value'},
                headers={'Authorization': 'Bearer token123'},
                timeout=30,
            )
            assert result['success'] is True
            assert result['data']['status_code'] == 200
            assert 'Success response' in result['data']['response']

    def test_execute_supports_different_http_methods(self):
        """Test that plugin supports GET, PUT, PATCH methods."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import WebhookAutomation

        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.text = 'OK'

        for method in ['GET', 'PUT', 'PATCH']:
            config = {
                'url': 'https://api.example.com/resource',
                'method': method,
                'payload': {'key': 'value'},
            }
            plugin = WebhookAutomation(config=config)

            with patch('requests.request', return_value=mock_response) as mock_request:
                # Act
                plugin.execute({})

                # Assert
                call_args = mock_request.call_args
                assert call_args[1]['method'] == method

    def test_execute_uses_default_method_post(self):
        """Test that POST is used as default method."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import WebhookAutomation

        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.text = 'OK'

        config = {'url': 'https://api.example.com/webhook'}
        plugin = WebhookAutomation(config=config)

        with patch('requests.request', return_value=mock_response) as mock_request:
            # Act
            plugin.execute({})

            # Assert
            call_args = mock_request.call_args
            assert call_args[1]['method'] == 'POST'

    def test_execute_raises_error_when_no_url(self):
        """Test that plugin raises ValueError during init when URL is not provided."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import WebhookAutomation

        config = {}

        # Act & Assert - config validation happens during __init__
        with pytest.raises(ValueError) as exc_info:
            plugin = WebhookAutomation(config=config)
        assert 'url' in str(exc_info.value).lower()

    def test_execute_raises_error_on_request_failure(self):
        """Test that plugin raises PluginExecutionError on request failure."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import WebhookAutomation
        from smoothschedule.scheduling.automations.registry import AutomationExecutionError as PluginExecutionError
        import requests

        config = {'url': 'https://api.example.com/webhook'}
        plugin = WebhookAutomation(config=config)

        with patch('requests.request', side_effect=requests.RequestException('Connection timeout')):
            # Act & Assert
            with pytest.raises(PluginExecutionError) as exc_info:
                plugin.execute({})
            assert 'Webhook request failed' in str(exc_info.value)

    def test_execute_raises_error_on_http_error_status(self):
        """Test that plugin raises error on HTTP error status codes."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import WebhookAutomation
        from smoothschedule.scheduling.automations.registry import AutomationExecutionError as PluginExecutionError
        import requests

        mock_response = Mock()
        mock_response.status_code = 500
        mock_response.raise_for_status.side_effect = requests.HTTPError('500 Server Error')

        config = {'url': 'https://api.example.com/webhook'}
        plugin = WebhookAutomation(config=config)

        with patch('requests.request', return_value=mock_response):
            # Act & Assert
            with pytest.raises(PluginExecutionError):
                plugin.execute({})

    def test_execute_truncates_long_response(self):
        """Test that long responses are truncated to 500 chars."""
        # Arrange
        from smoothschedule.scheduling.automations.builtin import WebhookAutomation

        long_response = 'x' * 1000  # 1000 character response
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.text = long_response

        config = {'url': 'https://api.example.com/webhook'}
        plugin = WebhookAutomation(config=config)

        with patch('requests.request', return_value=mock_response):
            # Act
            result = plugin.execute({})

            # Assert
            assert len(result['data']['response']) == 500
            assert result['data']['response'] == 'x' * 500


class TestBusinessHoursBookingContext:
    """Test business hours checking with booking context."""

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_business_hours_block_customer_booking(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that OUTSIDE business hours blocks customer bookings.

        When blocks_datetime_range returns False for business hours blocks,
        it means the time is OUTSIDE business hours and should be blocked.
        """
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 10
        mock_resource.buffer_duration = timedelta(minutes=0)
        mock_resource.name = "Staff Member"

        start = timezone.now()
        end = start + timedelta(hours=1)

        # Mock business hours block (SOFT with BUSINESS_HOURS purpose)
        mock_block = Mock()
        mock_block.block_type = 'SOFT'
        mock_block.title = "Business Hours"
        mock_block.resource = None  # Business-level
        # blocks_datetime_range=False means time is OUTSIDE business hours
        mock_block.blocks_datetime_range.return_value = False
        mock_block.is_business_hours = True
        mock_block.Purpose = Mock()
        mock_block.Purpose.BUSINESS_HOURS = 'BUSINESS_HOURS'
        mock_block.purpose = 'BUSINESS_HOURS'

        mock_timeblock.objects.filter.return_value.order_by.return_value = [mock_block]
        mock_timeblock.BlockType.HARD = 'HARD'

        # Mock no events
        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)
        mock_participant.objects.filter.return_value.select_related.return_value = []

        # Act
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, start, end, booking_context='customer_booking'
        )

        # Assert - Should be blocked because outside business hours
        assert is_available is False
        assert "outside business hours" in reason.lower()

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_business_hours_allow_staff_scheduling(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that staff can schedule OUTSIDE business hours with warning.

        When blocks_datetime_range returns False for business hours blocks,
        the time is outside business hours. Staff can still schedule but get a warning.
        """
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 10
        mock_resource.buffer_duration = timedelta(minutes=0)
        mock_resource.name = "Staff Member"

        start = timezone.now()
        end = start + timedelta(hours=1)

        # Mock business hours block
        mock_block = Mock()
        mock_block.block_type = 'SOFT'
        mock_block.title = "Business Hours"
        mock_block.resource = None
        # blocks_datetime_range=False means time is OUTSIDE business hours
        mock_block.blocks_datetime_range.return_value = False
        mock_block.is_business_hours = True
        mock_block.Purpose = Mock()
        mock_block.Purpose.BUSINESS_HOURS = 'BUSINESS_HOURS'
        mock_block.purpose = 'BUSINESS_HOURS'

        mock_timeblock.objects.filter.return_value.order_by.return_value = [mock_block]
        mock_timeblock.BlockType.HARD = 'HARD'

        # Mock no events
        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)
        mock_participant.objects.filter.return_value.select_related.return_value = []

        # Act
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, start, end, booking_context='staff_scheduling'
        )

        # Assert - Staff can schedule outside business hours but get a warning
        assert is_available is True
        assert len(warnings) == 1
        assert "outside business hours" in warnings[0].lower()

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_non_business_hours_block_not_affected_by_context(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that non-business hours blocks work normally regardless of context."""
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 10
        mock_resource.buffer_duration = timedelta(minutes=0)
        mock_resource.name = "Room A"

        start = timezone.now()
        end = start + timedelta(hours=1)

        # Mock regular HARD block (not business hours)
        mock_block = Mock()
        mock_block.block_type = 'HARD'
        mock_block.title = "Maintenance"
        mock_block.resource = mock_resource
        mock_block.blocks_datetime_range.return_value = True
        mock_block.is_business_hours = False
        mock_block.Purpose = Mock()
        mock_block.Purpose.BUSINESS_HOURS = 'BUSINESS_HOURS'
        mock_block.purpose = 'CLOSURE'

        mock_timeblock.objects.filter.return_value.order_by.return_value = [mock_block]
        mock_timeblock.BlockType.HARD = 'HARD'

        # Mock no events
        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)
        mock_participant.objects.filter.return_value.select_related.return_value = []

        # Act - Test with customer context
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, start, end, booking_context='customer_booking'
        )

        # Assert
        assert is_available is False
        assert "maintenance" in reason.lower()

        # Act - Test with staff context
        is_available2, reason2, warnings2 = AvailabilityService.check_availability(
            mock_resource, start, end, booking_context='staff_scheduling'
        )

        # Assert - Same result for both contexts
        assert is_available2 is False
        assert "maintenance" in reason2.lower()


class TestServicePrepAndTakedownTime:
    """Test service prep_time and takedown_time buffer handling."""

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_service_prep_time_used_for_conflict_check(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that service prep_time is used when checking for overlapping events."""
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 1
        mock_resource.buffer_duration = timedelta(minutes=0)
        mock_resource.name = "Staff Member"

        # Service with 15 minute prep time
        mock_service = Mock()
        mock_service.prep_time = 15
        mock_service.takedown_time = 0

        start = timezone.now()
        end = start + timedelta(hours=1)

        # No time blocks
        mock_timeblock.objects.filter.return_value.order_by.return_value = []
        mock_timeblock.BlockType.HARD = 'HARD'

        # Mock existing event that ends 10 minutes before our start
        # Without prep_time, this would be available
        # With 15 min prep_time, our window starts 15 min earlier, causing overlap
        mock_event = Mock()
        mock_event.id = 1
        mock_event.status = 'SCHEDULED'
        # Event ends 10 minutes before our service would start
        mock_event.start_time = start - timedelta(minutes=70)
        mock_event.end_time = start - timedelta(minutes=10)

        mock_participant_obj = Mock()
        mock_participant_obj.event = mock_event

        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)
        mock_participant.objects.filter.return_value.select_related.return_value = [mock_participant_obj]

        # Act - With service that has prep_time
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, start, end, service=mock_service
        )

        # Assert - Should be unavailable because prep_time causes overlap
        assert is_available is False
        assert "capacity exceeded" in reason.lower()

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_service_takedown_time_used_for_conflict_check(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that service takedown_time is used when checking for overlapping events."""
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 1
        mock_resource.buffer_duration = timedelta(minutes=0)
        mock_resource.name = "Staff Member"

        # Service with 30 minute takedown time
        mock_service = Mock()
        mock_service.prep_time = 0
        mock_service.takedown_time = 30

        start = timezone.now()
        end = start + timedelta(hours=1)

        # No time blocks
        mock_timeblock.objects.filter.return_value.order_by.return_value = []
        mock_timeblock.BlockType.HARD = 'HARD'

        # Mock existing event that starts 20 minutes after our end
        # Without takedown_time, this would be available
        # With 30 min takedown_time, our window extends 30 min, causing overlap
        mock_event = Mock()
        mock_event.id = 2
        mock_event.status = 'SCHEDULED'
        # Event starts 20 minutes after our service ends
        mock_event.start_time = end + timedelta(minutes=20)
        mock_event.end_time = end + timedelta(minutes=80)

        mock_participant_obj = Mock()
        mock_participant_obj.event = mock_event

        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)
        mock_participant.objects.filter.return_value.select_related.return_value = [mock_participant_obj]

        # Act - With service that has takedown_time
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, start, end, service=mock_service
        )

        # Assert - Should be unavailable because takedown_time causes overlap
        assert is_available is False
        assert "capacity exceeded" in reason.lower()

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_buffer_check_ignores_business_hours(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that prep/takedown windows can extend outside business hours."""
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 10
        mock_resource.buffer_duration = timedelta(minutes=0)
        mock_resource.name = "Staff Member"

        # Service with prep time that extends before business hours
        mock_service = Mock()
        mock_service.prep_time = 30  # 30 min prep before
        mock_service.takedown_time = 0

        start = timezone.now()
        end = start + timedelta(hours=1)

        # Mock business hours block that would block times outside hours
        mock_block = Mock()
        mock_block.block_type = 'SOFT'
        mock_block.title = "Business Hours"
        mock_block.resource = None
        mock_block.is_business_hours = True
        # Return True for service window (within hours), False for prep window (outside)
        mock_block.blocks_datetime_range.side_effect = lambda s, e: s >= start

        mock_timeblock.objects.filter.return_value.order_by.return_value = [mock_block]
        mock_timeblock.BlockType.HARD = 'HARD'

        # No events
        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)
        mock_participant.objects.filter.return_value.select_related.return_value = []

        # Act - Customer booking with service
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, start, end, booking_context='customer_booking', service=mock_service
        )

        # Assert - Should be available because prep time can extend outside business hours
        assert is_available is True

    @patch('smoothschedule.scheduling.schedule.services.ContentType')
    @patch('smoothschedule.scheduling.schedule.services.Participant')
    @patch('smoothschedule.scheduling.schedule.services.TimeBlock')
    def test_buffer_check_respects_hard_blocks(
        self, mock_timeblock, mock_participant, mock_contenttype
    ):
        """Test that prep/takedown windows are blocked by hard blocks."""
        # Arrange
        mock_resource = Mock()
        mock_resource.max_concurrent_events = 10
        mock_resource.buffer_duration = timedelta(minutes=0)
        mock_resource.name = "Staff Member"

        # Service with prep time
        mock_service = Mock()
        mock_service.prep_time = 30
        mock_service.takedown_time = 0

        start = timezone.now()
        end = start + timedelta(hours=1)
        prep_start = start - timedelta(minutes=30)

        # Mock hard block that covers the prep time window
        mock_block = Mock()
        mock_block.block_type = 'HARD'
        mock_block.title = "Maintenance"
        mock_block.resource = None
        mock_block.is_business_hours = False
        # Block the prep window but not the service window
        mock_block.blocks_datetime_range.side_effect = lambda s, e: s < start

        mock_timeblock.objects.filter.return_value.order_by.return_value = [mock_block]
        mock_timeblock.BlockType.HARD = 'HARD'

        # No events
        mock_contenttype.objects.get_for_model.return_value = Mock(id=1)
        mock_participant.objects.filter.return_value.select_related.return_value = []

        # Act
        is_available, reason, warnings = AvailabilityService.check_availability(
            mock_resource, start, end, service=mock_service
        )

        # Assert - Should be unavailable because hard block covers prep time
        assert is_available is False
        assert "prep time blocked" in reason.lower()
