"""
Comprehensive unit tests for Public API v1 Views.

These tests use mocks extensively to avoid database overhead and test business
logic in isolation. Following the testing pyramid: many unit tests (fast, mocked),
few integration tests (slow, database).

Coverage areas:
- APITokenViewSet: Token management (list, create, destroy, scopes, test-tokens)
- PublicBusinessView: Business information retrieval
- PublicServiceViewSet: Service listing and retrieval
- PublicResourceViewSet: Resource listing and retrieval with type filtering
- AvailabilityView: Availability checking with date range validation
- PublicAppointmentViewSet: Appointment CRUD operations
- PublicCustomerViewSet: Customer management with sandbox filtering
- WebhookViewSet: Webhook subscription CRUD and deliveries
- Permission checking: Scope-based permissions, feature flags
- Error handling: Not found, validation errors, permission denied
"""

import pytest
from unittest.mock import Mock, patch, MagicMock, PropertyMock
from datetime import datetime, timedelta, date
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APIRequestFactory
from rest_framework.request import Request
from rest_framework.exceptions import PermissionDenied

from smoothschedule.platform.api.views import (
    APITokenViewSet,
    PublicBusinessView,
    PublicServiceViewSet,
    PublicResourceViewSet,
    AvailabilityView,
    PublicAppointmentViewSet,
    PublicCustomerViewSet,
    WebhookViewSet,
)
from smoothschedule.platform.api.models import APIToken, APIScope, WebhookEvent


# =============================================================================
# APITokenViewSet Tests
# =============================================================================

class TestAPITokenViewSet:
    """Test suite for APITokenViewSet (token management for business owners)."""

    def setup_method(self):
        """Set up common test fixtures."""
        self.factory = APIRequestFactory()
        self.viewset = APITokenViewSet()

        # Mock tenant with all required attributes
        self.tenant = Mock()
        self.tenant.id = 1
        self.tenant.name = 'Test Business'
        self.tenant.has_feature = Mock(return_value=True)

        # Mock user (owner role)
        self.user = Mock()
        self.user.id = 1
        self.user.role = 'OWNER'
        self.user.tenant = self.tenant
        self.user.is_authenticated = True

    def test_list_returns_tokens_for_owner(self):
        """Owners can list all API tokens for their business."""
        request = self.factory.get('/api/tokens/')
        request.user = self.user

        # Mock APIToken queryset
        mock_token1 = Mock()
        mock_token1.id = '123'
        mock_token1.name = 'Token 1'

        mock_qs = Mock()
        mock_qs.filter = Mock(return_value=[mock_token1])

        with patch('smoothschedule.platform.api.views.APIToken.objects', mock_qs):
            with patch('smoothschedule.platform.api.views.APITokenListSerializer') as MockSerializer:
                mock_serializer = Mock()
                mock_serializer.data = [{'id': '123', 'name': 'Token 1'}]
                MockSerializer.return_value = mock_serializer

                response = self.viewset.list(request)

        assert response.status_code == 200
        mock_qs.filter.assert_called_once_with(tenant=self.tenant)

    def test_list_denies_staff_users(self):
        """Staff users cannot list API tokens (only owners)."""
        self.user.role = 'STAFF'
        request = self.factory.get('/api/tokens/')
        request.user = self.user

        response = self.viewset.list(request)

        assert response.status_code == 403
        assert 'Only business owners' in response.data['message']

    def test_list_denies_without_api_access_permission(self):
        """Cannot list tokens if tenant lacks can_api_access feature."""
        self.tenant.has_feature = Mock(return_value=False)
        request = self.factory.get('/api/tokens/')
        request.user = self.user

        with pytest.raises(PermissionDenied) as exc_info:
            self.viewset.list(request)

        assert 'API Access' in str(exc_info.value)

    def test_create_generates_live_token_by_default(self):
        """Token creation generates live token when sandbox_mode not set."""
        wsgi_request = self.factory.post('/api/tokens/')
        request = Request(wsgi_request)
        request._data = {
            'name': 'Test Token',
            'scopes': ['services:read']
        }
        request.user = self.user
        # No sandbox_mode attribute

        with patch('smoothschedule.platform.api.views.APITokenCreateSerializer') as MockSerializer:
            mock_serializer = Mock()
            mock_serializer.is_valid.return_value = True
            mock_serializer.validated_data = {
                'name': 'Test Token',
                'scopes': ['services:read'],
                'is_sandbox': None
            }
            MockSerializer.return_value = mock_serializer

            with patch('smoothschedule.platform.api.views.APIToken') as MockAPIToken:
                # Mock generate_key to return a live token
                MockAPIToken.generate_key.return_value = (
                    'ss_live_abc123',
                    'hash123',
                    'ss_live_abc1'
                )
                mock_token = Mock()
                mock_token.id = '123'

                # Mock the create call
                mock_objects = Mock()
                mock_objects.create = Mock(return_value=mock_token)
                MockAPIToken.objects = mock_objects

                with patch('smoothschedule.platform.api.views.APITokenResponseSerializer') as MockRespSerializer:
                    MockRespSerializer.return_value.data = {'id': '123'}

                    response = self.viewset.create(request)

        assert response.status_code == 201
        assert 'key' in response.data
        assert response.data['key'] == 'ss_live_abc123'

        # Verify plaintext_key was None for live token
        call_kwargs = mock_objects.create.call_args[1]
        assert call_kwargs['plaintext_key'] is None
        assert call_kwargs['is_sandbox'] is False

    def test_create_sandbox_token_stores_plaintext(self):
        """Sandbox tokens store plaintext key for documentation."""
        wsgi_request = self.factory.post('/api/tokens/')
        request = Request(wsgi_request)
        request._data = {
            'name': 'Sandbox Token',
            'scopes': ['services:read'],
            'is_sandbox': True
        }
        request.user = self.user

        with patch('smoothschedule.platform.api.views.APITokenCreateSerializer') as MockSerializer:
            mock_serializer = Mock()
            mock_serializer.is_valid.return_value = True
            mock_serializer.validated_data = {
                'name': 'Sandbox Token',
                'scopes': ['services:read'],
                'is_sandbox': True
            }
            MockSerializer.return_value = mock_serializer

            with patch('smoothschedule.platform.api.views.APIToken') as MockAPIToken:
                MockAPIToken.generate_key.return_value = (
                    'ss_test_xyz789',
                    'hash789',
                    'ss_test_xyz7'
                )
                mock_token = Mock()

                mock_objects = Mock()
                mock_objects.create = Mock(return_value=mock_token)
                MockAPIToken.objects = mock_objects

                with patch('smoothschedule.platform.api.views.APITokenResponseSerializer') as MockRespSerializer:
                    MockRespSerializer.return_value.data = {}

                    response = self.viewset.create(request)

        # Verify plaintext_key was passed to create
        call_kwargs = mock_objects.create.call_args[1]
        assert call_kwargs['plaintext_key'] == 'ss_test_xyz789'
        assert call_kwargs['is_sandbox'] is True

    def test_create_returns_validation_errors(self):
        """Invalid token data returns 400 with error details."""
        wsgi_request = self.factory.post('/api/tokens/')
        request = Request(wsgi_request)
        request._data = {}
        request.user = self.user

        with patch('smoothschedule.platform.api.views.APITokenCreateSerializer') as MockSerializer:
            mock_serializer = Mock()
            mock_serializer.is_valid.return_value = False
            mock_serializer.errors = {'name': ['This field is required']}
            MockSerializer.return_value = mock_serializer

            response = self.viewset.create(request)

        assert response.status_code == 400
        assert 'validation_error' in response.data['error']
        assert 'details' in response.data

    def test_destroy_deletes_token(self):
        """Owners can delete their own tokens."""
        request = self.factory.delete('/api/tokens/123/')
        request.user = self.user

        mock_token = Mock()
        mock_objects = Mock()
        mock_objects.get = Mock(return_value=mock_token)

        with patch('smoothschedule.platform.api.views.APIToken.objects', mock_objects):
            response = self.viewset.destroy(request, pk='123')

        assert response.status_code == 204
        mock_token.delete.assert_called_once()
        mock_objects.get.assert_called_once_with(pk='123', tenant=self.tenant)

    def test_destroy_returns_404_for_nonexistent_token(self):
        """Deleting non-existent token returns 404."""
        request = self.factory.delete('/api/tokens/999/')
        request.user = self.user

        from smoothschedule.platform.api.models import APIToken as RealAPIToken

        mock_objects = Mock()
        mock_objects.get = Mock(side_effect=RealAPIToken.DoesNotExist)
        mock_objects.DoesNotExist = RealAPIToken.DoesNotExist

        with patch('smoothschedule.platform.api.views.APIToken.objects', mock_objects):
            with patch('smoothschedule.platform.api.views.APIToken.DoesNotExist', RealAPIToken.DoesNotExist):
                response = self.viewset.destroy(request, pk='999')

        assert response.status_code == 404
        assert 'not_found' in response.data['error']

    def test_scopes_action_returns_available_scopes(self):
        """The /scopes/ action returns all available API scopes."""
        request = self.factory.get('/api/tokens/scopes/')
        request.user = self.user

        response = self.viewset.scopes(request)

        assert response.status_code == 200
        assert isinstance(response.data, list)
        # Verify it returns scope/description pairs
        assert all('scope' in item and 'description' in item for item in response.data)

    def test_test_tokens_returns_only_sandbox_tokens(self):
        """test-tokens endpoint returns only sandbox tokens, never live tokens."""
        request = self.factory.get('/api/tokens/test-tokens/')
        request.user = self.user

        # Create mock sandbox and live tokens
        sandbox_token = Mock()
        sandbox_token.id = '123'
        sandbox_token.name = 'Sandbox Token'
        sandbox_token.is_sandbox = True
        sandbox_token.plaintext_key = 'ss_test_abc123'
        sandbox_token.created_at = timezone.now()

        live_token = Mock()
        live_token.is_sandbox = False  # Should be filtered out

        mock_qs = Mock()
        # Filter returns both, view should double-check
        mock_qs.filter.return_value.order_by.return_value = [sandbox_token, live_token]

        with patch('smoothschedule.platform.api.views.APIToken.objects', mock_qs):
            response = self.viewset.test_tokens(request)

        assert response.status_code == 200
        # Should only include the sandbox token after double-check
        assert len(response.data) == 1
        assert response.data[0]['id'] == '123'


# =============================================================================
# PublicBusinessView Tests
# =============================================================================

class TestPublicBusinessView:
    """Test suite for PublicBusinessView (retrieve business info)."""

    def setup_method(self):
        """Set up common test fixtures."""
        self.factory = APIRequestFactory()
        self.view = PublicBusinessView()

        # Mock tenant
        self.tenant = Mock()
        self.tenant.id = 1
        self.tenant.name = 'Test Business'
        self.tenant.subdomain = 'testbiz'
        self.tenant.logo = None
        self.tenant.primary_color = '#FF5733'
        self.tenant.secondary_color = '#33FF57'
        self.tenant.timezone = 'America/New_York'
        self.tenant.cancellation_window_hours = 24

    def test_get_returns_business_info(self):
        """GET /business/ returns business information."""
        request = self.factory.get('/api/v1/business/')
        self.view.request = request

        with patch.object(self.view, 'get_tenant', return_value=self.tenant):
            with patch('smoothschedule.platform.api.views.PublicBusinessSerializer') as MockSerializer:
                mock_serializer = Mock()
                mock_serializer.data = {
                    'id': 1,
                    'name': 'Test Business',
                    'subdomain': 'testbiz'
                }
                MockSerializer.return_value = mock_serializer

                response = self.view.get(request)

        assert response.status_code == 200

    def test_get_returns_404_without_tenant(self):
        """Returns 404 if tenant not found."""
        request = self.factory.get('/api/v1/business/')
        self.view.request = request

        with patch.object(self.view, 'get_tenant', return_value=None):
            response = self.view.get(request)

        assert response.status_code == 404
        assert 'not_found' in response.data['error']

    def test_get_handles_missing_timezone_attribute(self):
        """Defaults to UTC if tenant lacks timezone attribute."""
        tenant_no_tz = Mock(spec=['id', 'name', 'subdomain', 'logo', 'primary_color',
                                   'secondary_color', 'cancellation_window_hours'])
        tenant_no_tz.id = 1
        tenant_no_tz.name = 'Test'
        tenant_no_tz.subdomain = 'test'
        tenant_no_tz.logo = None
        tenant_no_tz.primary_color = '#000'
        tenant_no_tz.secondary_color = '#FFF'
        tenant_no_tz.cancellation_window_hours = 24

        request = self.factory.get('/api/v1/business/')
        self.view.request = request

        with patch.object(self.view, 'get_tenant', return_value=tenant_no_tz):
            with patch('smoothschedule.platform.api.views.PublicBusinessSerializer') as MockSerializer:
                MockSerializer.return_value.data = {}
                self.view.get(request)

                call_args = MockSerializer.call_args[0][0]
                assert call_args['timezone'] == 'UTC'


# =============================================================================
# PublicServiceViewSet Tests
# =============================================================================

class TestPublicServiceViewSet:
    """Test suite for PublicServiceViewSet (list and retrieve services)."""

    def setup_method(self):
        """Set up common test fixtures."""
        self.factory = APIRequestFactory()
        self.viewset = PublicServiceViewSet()

    def test_list_returns_active_services(self):
        """List returns only active services ordered correctly."""
        mock_service = Mock()
        mock_service.id = 1
        mock_service.name = 'Haircut'
        mock_service.description = 'Standard haircut'
        mock_service.duration = 30
        mock_service.price = 25.00
        mock_service.is_active = True
        mock_service.photos = None

        request = self.factory.get('/api/v1/services/')

        mock_qs = Mock()
        mock_qs.filter.return_value.order_by.return_value = [mock_service]

        with patch('smoothschedule.scheduling.schedule.models.Service.objects', mock_qs):
            response = self.viewset.list(request)

        assert response.status_code == 200
        assert len(response.data) == 1
        assert response.data[0]['name'] == 'Haircut'

    def test_retrieve_returns_single_service(self):
        """Retrieve returns a single service by ID."""
        mock_service = Mock()
        mock_service.id = 1
        mock_service.name = 'Haircut'
        mock_service.description = 'Standard haircut'
        mock_service.duration = 30
        mock_service.price = 25.00
        mock_service.is_active = True
        mock_service.photos = None

        request = self.factory.get('/api/v1/services/1/')

        mock_objects = Mock()
        mock_objects.get = Mock(return_value=mock_service)

        with patch('smoothschedule.scheduling.schedule.models.Service.objects', mock_objects):
            response = self.viewset.retrieve(request, pk=1)

        assert response.status_code == 200
        assert response.data['name'] == 'Haircut'

    def test_retrieve_returns_404_for_inactive_service(self):
        """Retrieve returns 404 for inactive/non-existent services."""
        request = self.factory.get('/api/v1/services/999/')

        # Import the real exception class
        from django.core.exceptions import ObjectDoesNotExist

        mock_objects = Mock()
        mock_objects.get = Mock(side_effect=ObjectDoesNotExist)
        mock_objects.DoesNotExist = ObjectDoesNotExist

        with patch('smoothschedule.scheduling.schedule.models.Service.objects', mock_objects):
            with patch('smoothschedule.scheduling.schedule.models.Service.DoesNotExist', ObjectDoesNotExist):
                response = self.viewset.retrieve(request, pk=999)

        assert response.status_code == 404
        assert 'not_found' in response.data['error']


# =============================================================================
# PublicResourceViewSet Tests
# =============================================================================

class TestPublicResourceViewSet:
    """Test suite for PublicResourceViewSet (list and retrieve resources)."""

    def setup_method(self):
        """Set up common test fixtures."""
        self.factory = APIRequestFactory()
        self.viewset = PublicResourceViewSet()

    def test_list_returns_active_resources(self):
        """List returns only active resources."""
        mock_resource_type = Mock()
        mock_resource_type.id = 1
        mock_resource_type.name = 'Stylist'
        mock_resource_type.category = 'STAFF'

        mock_resource = Mock()
        mock_resource.id = 1
        mock_resource.name = 'Jane Doe'
        mock_resource.description = 'Senior Stylist'
        mock_resource.resource_type = mock_resource_type
        mock_resource.photo = None
        mock_resource.is_active = True

        wsgi_request = self.factory.get('/api/v1/resources/')
        request = Request(wsgi_request)

        mock_qs = Mock()
        mock_qs.filter.return_value.select_related.return_value.order_by.return_value = [mock_resource]

        with patch('smoothschedule.scheduling.schedule.models.Resource.objects', mock_qs):
            response = self.viewset.list(request)

        assert response.status_code == 200
        assert len(response.data) == 1
        assert response.data[0]['name'] == 'Jane Doe'

    def test_list_filters_by_resource_type(self):
        """List can filter resources by type query parameter."""
        wsgi_request = self.factory.get('/api/v1/resources/?type=stylist')
        request = Request(wsgi_request)

        # Create a complete mock chain
        # objects.filter() -> filter() -> select_related() -> filter() -> order_by() -> []
        mock_qs = Mock()

        # First level: Resource.objects.filter(is_active=True)
        mock_filtered_active = Mock()

        # Second level: .select_related('resource_type')
        mock_selected = Mock()

        # Third level: .filter(resource_type__name__iexact=resource_type)
        mock_filtered_type = Mock()

        # Final level: .order_by('name') returns empty list
        mock_filtered_type.order_by.return_value = []
        mock_selected.filter.return_value = mock_filtered_type
        mock_filtered_active.select_related.return_value = mock_selected
        mock_qs.filter.return_value = mock_filtered_active

        with patch('smoothschedule.scheduling.schedule.models.Resource.objects', mock_qs):
            response = self.viewset.list(request)

        # Should have called filter with resource_type__name__iexact
        assert mock_selected.filter.called
        assert response.status_code == 200


# =============================================================================
# AvailabilityView Tests
# =============================================================================

class TestAvailabilityView:
    """Test suite for AvailabilityView (check availability for bookings)."""

    def setup_method(self):
        """Set up common test fixtures."""
        self.factory = APIRequestFactory()
        self.view = AvailabilityView()

    def test_get_validates_required_parameters(self):
        """GET requires service_id and date parameters."""
        wsgi_request = self.factory.get('/api/v1/availability/')
        request = Request(wsgi_request)

        with patch('smoothschedule.platform.api.views.AvailabilityRequestSerializer') as MockSerializer:
            mock_serializer = Mock()
            mock_serializer.is_valid.return_value = False
            mock_serializer.errors = {'service_id': ['This field is required']}
            MockSerializer.return_value = mock_serializer

            response = self.view.get(request)

        assert response.status_code == 400
        assert 'validation_error' in response.data['error']

    def test_get_returns_404_for_nonexistent_service(self):
        """Returns 404 if service not found."""
        wsgi_request = self.factory.get('/api/v1/availability/?service_id=999&date=2024-01-01')
        request = Request(wsgi_request)

        from django.core.exceptions import ObjectDoesNotExist

        with patch('smoothschedule.platform.api.views.AvailabilityRequestSerializer') as MockSerializer:
            mock_serializer = Mock()
            mock_serializer.is_valid.return_value = True
            mock_serializer.validated_data = {
                'service_id': '999',
                'date': date(2024, 1, 1),
                'days': 7
            }
            MockSerializer.return_value = mock_serializer

            mock_objects = Mock()
            mock_objects.get = Mock(side_effect=ObjectDoesNotExist)
            mock_objects.DoesNotExist = ObjectDoesNotExist

            with patch('smoothschedule.scheduling.schedule.models.Service.objects', mock_objects):
                with patch('smoothschedule.scheduling.schedule.models.Service.DoesNotExist', ObjectDoesNotExist):
                    response = self.view.get(request)

        assert response.status_code == 404
        assert 'Service not found' in response.data['message']

    def test_get_calculates_date_range_correctly(self):
        """Calculates end date based on start date and days parameter."""
        wsgi_request = self.factory.get('/api/v1/availability/?service_id=1&date=2024-01-01&days=14')
        request = Request(wsgi_request)

        mock_service = Mock()
        mock_service.id = 1
        mock_service.name = 'Service'
        mock_service.description = 'Test'
        mock_service.duration = 60
        mock_service.price = 100.00
        mock_service.is_active = True

        with patch('smoothschedule.platform.api.views.AvailabilityRequestSerializer') as MockSerializer:
            mock_serializer = Mock()
            mock_serializer.is_valid.return_value = True
            mock_serializer.validated_data = {
                'service_id': '1',
                'date': date(2024, 1, 1),
                'days': 14
            }
            MockSerializer.return_value = mock_serializer

            mock_objects = Mock()
            mock_objects.get = Mock(return_value=mock_service)

            with patch('smoothschedule.scheduling.schedule.models.Service.objects', mock_objects):
                response = self.view.get(request)

        # Check date range in response
        assert response.data['date_range']['start'] == '2024-01-01'
        assert response.data['date_range']['end'] == '2024-01-15'  # 1 + 14 days


# =============================================================================
# PublicAppointmentViewSet Tests
# =============================================================================

class TestPublicAppointmentViewSet:
    """Test suite for PublicAppointmentViewSet (appointment/booking CRUD)."""

    def setup_method(self):
        """Set up common test fixtures."""
        self.factory = APIRequestFactory()
        self.viewset = PublicAppointmentViewSet()

    def test_list_returns_appointments(self):
        """List returns appointments (events)."""
        mock_event = Mock()
        mock_event.id = 1
        mock_event.start = timezone.now()
        mock_event.end = timezone.now() + timedelta(hours=1)
        mock_event.status = 'CONFIRMED'
        mock_event.notes = 'Test appointment'
        mock_event.created = timezone.now()

        wsgi_request = self.factory.get('/api/v1/appointments/')
        request = Request(wsgi_request)

        mock_qs = Mock()
        mock_qs.all.return_value.order_by.return_value = [mock_event]

        with patch('smoothschedule.scheduling.schedule.models.Event.objects', mock_qs):
            response = self.viewset.list(request)

        assert response.status_code == 200
        assert len(response.data) == 1

    def test_create_validates_input_data(self):
        """Create validates input before processing."""
        wsgi_request = self.factory.post('/api/v1/appointments/')
        request = Request(wsgi_request)
        request._data = {}

        with patch('smoothschedule.platform.api.views.AppointmentCreateSerializer') as MockSerializer:
            mock_serializer = Mock()
            mock_serializer.is_valid.return_value = False
            mock_serializer.errors = {'service_id': ['Required']}
            MockSerializer.return_value = mock_serializer

            response = self.viewset.create(request)

        assert response.status_code == 400
        assert 'validation_error' in response.data['error']

    def test_retrieve_returns_appointment_by_id(self):
        """Retrieve returns a single appointment."""
        mock_event = Mock()
        mock_event.id = 1
        mock_event.start = timezone.now()
        mock_event.end = timezone.now() + timedelta(hours=1)
        mock_event.status = 'CONFIRMED'
        mock_event.notes = 'Test'
        mock_event.created = timezone.now()

        request = self.factory.get('/api/v1/appointments/1/')

        mock_objects = Mock()
        mock_objects.get = Mock(return_value=mock_event)

        with patch('smoothschedule.scheduling.schedule.models.Event.objects', mock_objects):
            response = self.viewset.retrieve(request, pk=1)

        assert response.status_code == 200
        assert response.data['id'] == 1


# =============================================================================
# PublicCustomerViewSet Tests
# =============================================================================

class TestPublicCustomerViewSet:
    """Test suite for PublicCustomerViewSet (customer management)."""

    def setup_method(self):
        """Set up common test fixtures."""
        self.factory = APIRequestFactory()
        self.viewset = PublicCustomerViewSet()

    def test_list_filters_by_sandbox_mode(self):
        """List filters customers by sandbox mode from request."""
        wsgi_request = self.factory.get('/api/v1/customers/')
        request = Request(wsgi_request)
        request.sandbox_mode = True

        mock_qs = Mock()
        # Create a chain that supports multiple filters
        filtered = Mock()
        filtered.filter.return_value = filtered
        filtered.order_by.return_value = []
        mock_qs.filter.return_value = filtered

        with patch('smoothschedule.identity.users.models.User.objects', mock_qs):
            response = self.viewset.list(request)

        assert response.status_code == 200
        # Should have called filter at least twice (role and is_sandbox)
        assert filtered.filter.call_count >= 1

    def test_retrieve_filters_by_sandbox_mode(self):
        """Retrieve filters by sandbox mode."""
        mock_customer = Mock()
        mock_customer.id = 1
        mock_customer.email = 'test@example.com'
        mock_customer.get_full_name.return_value = 'Test User'
        mock_customer.username = 'testuser'
        mock_customer.date_joined = timezone.now()

        request = self.factory.get('/api/v1/customers/1/')
        request.sandbox_mode = True

        mock_objects = Mock()
        mock_objects.get = Mock(return_value=mock_customer)

        with patch('smoothschedule.identity.users.models.User.objects', mock_objects):
            response = self.viewset.retrieve(request, pk=1)

        # Should filter by is_sandbox=True
        mock_objects.get.assert_called_once_with(pk=1, role='CUSTOMER', is_sandbox=True)


# =============================================================================
# WebhookViewSet Tests
# =============================================================================

class TestWebhookViewSet:
    """Test suite for WebhookViewSet (webhook subscription management)."""

    def setup_method(self):
        """Set up common test fixtures."""
        self.factory = APIRequestFactory()
        self.viewset = WebhookViewSet()

        # Mock API token
        self.token = Mock()
        self.token.id = 1
        self.token.tenant = Mock()
        self.token.tenant.has_feature = Mock(return_value=True)

    def test_list_checks_webhooks_permission(self):
        """List checks that tenant has webhooks feature enabled."""
        self.token.tenant.has_feature = Mock(return_value=False)

        request = self.factory.get('/api/v1/webhooks/')
        request.api_token = self.token

        with pytest.raises(PermissionDenied) as exc_info:
            self.viewset.list(request)

        assert 'Webhooks' in str(exc_info.value)

    def test_list_returns_subscriptions_for_token(self):
        """List returns webhook subscriptions for the current API token."""
        mock_subscription = Mock()
        mock_subscription.id = 1

        request = self.factory.get('/api/v1/webhooks/')
        request.api_token = self.token

        mock_qs = Mock()
        mock_qs.filter.return_value = [mock_subscription]

        with patch('smoothschedule.platform.api.views.WebhookSubscription.objects', mock_qs):
            with patch('smoothschedule.platform.api.views.WebhookSubscriptionSerializer') as MockSerializer:
                mock_serializer = Mock()
                mock_serializer.data = [{'id': 1}]
                MockSerializer.return_value = mock_serializer

                response = self.viewset.list(request)

        assert response.status_code == 200
        mock_qs.filter.assert_called_once_with(api_token=self.token)

    def test_retrieve_returns_subscription(self):
        """Retrieve returns a specific webhook subscription."""
        mock_subscription = Mock()
        mock_subscription.id = 1

        request = self.factory.get('/api/v1/webhooks/1/')
        request.api_token = self.token

        mock_objects = Mock()
        mock_objects.get = Mock(return_value=mock_subscription)

        with patch('smoothschedule.platform.api.views.WebhookSubscription.objects', mock_objects):
            with patch('smoothschedule.platform.api.views.WebhookSubscriptionSerializer') as MockSerializer:
                MockSerializer.return_value.data = {'id': 1}

                response = self.viewset.retrieve(request, pk=1)

        assert response.status_code == 200
        mock_objects.get.assert_called_once_with(pk=1, api_token=self.token)

    def test_partial_update_updates_fields(self):
        """Update can modify subscription fields."""
        mock_subscription = Mock()
        mock_subscription.id = 1
        mock_subscription.url = 'https://old.example.com/webhook'
        mock_subscription.events = ['appointment.created']
        mock_subscription.is_active = True

        wsgi_request = self.factory.patch('/api/v1/webhooks/1/')
        request = Request(wsgi_request)
        request._data = {
            'url': 'https://new.example.com/webhook',
            'is_active': False
        }
        request.api_token = self.token

        with patch('smoothschedule.platform.api.views.WebhookSubscriptionUpdateSerializer') as MockSerializer:
            mock_serializer = Mock()
            mock_serializer.is_valid.return_value = True
            mock_serializer.validated_data = {
                'url': 'https://new.example.com/webhook',
                'is_active': False
            }
            MockSerializer.return_value = mock_serializer

            mock_objects = Mock()
            mock_objects.get = Mock(return_value=mock_subscription)

            with patch('smoothschedule.platform.api.views.WebhookSubscription.objects', mock_objects):
                with patch('smoothschedule.platform.api.views.WebhookSubscriptionSerializer') as MockRespSerializer:
                    MockRespSerializer.return_value.data = {}

                    response = self.viewset.partial_update(request, pk=1)

        assert response.status_code == 200
        assert mock_subscription.url == 'https://new.example.com/webhook'
        assert mock_subscription.is_active is False
        mock_subscription.save.assert_called_once()

    def test_destroy_deletes_subscription(self):
        """Destroy deletes a webhook subscription."""
        mock_subscription = Mock()

        request = self.factory.delete('/api/v1/webhooks/1/')
        request.api_token = self.token

        mock_objects = Mock()
        mock_objects.get = Mock(return_value=mock_subscription)

        with patch('smoothschedule.platform.api.views.WebhookSubscription.objects', mock_objects):
            response = self.viewset.destroy(request, pk=1)

        assert response.status_code == 204
        mock_subscription.delete.assert_called_once()

    def test_events_action_returns_available_events(self):
        """The /events/ action returns all available webhook event types."""
        request = self.factory.get('/api/v1/webhooks/events/')
        request.api_token = self.token

        response = self.viewset.events(request)

        assert response.status_code == 200
        assert isinstance(response.data, list)
        assert all('event' in item and 'description' in item for item in response.data)

    def test_deliveries_action_returns_delivery_history(self):
        """The /deliveries/ action returns webhook delivery history."""
        mock_subscription = Mock()
        mock_delivery = Mock()

        request = self.factory.get('/api/v1/webhooks/1/deliveries/')
        request.api_token = self.token

        mock_webhook_objects = Mock()
        mock_webhook_objects.get = Mock(return_value=mock_subscription)

        mock_delivery_qs = Mock()
        mock_delivery_qs.filter.return_value.order_by.return_value.__getitem__ = Mock(return_value=[mock_delivery])

        with patch('smoothschedule.platform.api.views.WebhookSubscription.objects', mock_webhook_objects):
            with patch('smoothschedule.platform.api.views.WebhookDelivery.objects', mock_delivery_qs):
                with patch('smoothschedule.platform.api.views.WebhookDeliverySerializer') as MockSerializer:
                    MockSerializer.return_value.data = [{'id': 1}]

                    response = self.viewset.deliveries(request, pk=1)

        assert response.status_code == 200


# =============================================================================
# PublicAPIViewMixin Tests
# =============================================================================

class TestPublicAPIViewMixin:
    """Test suite for PublicAPIViewMixin base functionality."""

    def test_get_tenant_returns_request_tenant(self):
        """get_tenant() returns tenant from request."""
        from smoothschedule.platform.api.views import PublicAPIViewMixin

        mixin = PublicAPIViewMixin()
        mock_request = Mock()
        mock_tenant = Mock()
        mock_request.tenant = mock_tenant
        mixin.request = mock_request

        tenant = mixin.get_tenant()

        assert tenant == mock_tenant

    def test_get_tenant_returns_none_if_not_set(self):
        """get_tenant() returns None if tenant not on request."""
        from smoothschedule.platform.api.views import PublicAPIViewMixin

        mixin = PublicAPIViewMixin()
        mock_request = Mock(spec=[])  # No tenant attribute
        mixin.request = mock_request

        tenant = mixin.get_tenant()

        assert tenant is None
