"""
Unit tests for platform API models.

These are fast unit tests using mocks - no database access required.
For integration tests with database, see test_integration.py
"""

import hashlib
import secrets
from datetime import datetime, timedelta
from unittest.mock import Mock, patch, MagicMock

import pytest
from django.core.exceptions import ValidationError
from django.utils import timezone

from smoothschedule.platform.api.models import (
    APIScope,
    APIToken,
    WebhookEvent,
    WebhookSubscription,
    WebhookDelivery,
)


# ==============================================================================
# APIScope Tests
# ==============================================================================


class TestAPIScope:
    """Test APIScope constants and utility methods."""

    def test_scope_constants_exist(self):
        """Verify all expected scope constants are defined."""
        assert APIScope.SERVICES_READ == 'services:read'
        assert APIScope.RESOURCES_READ == 'resources:read'
        assert APIScope.AVAILABILITY_READ == 'availability:read'
        assert APIScope.BOOKINGS_READ == 'bookings:read'
        assert APIScope.BOOKINGS_WRITE == 'bookings:write'
        assert APIScope.CUSTOMERS_READ == 'customers:read'
        assert APIScope.CUSTOMERS_WRITE == 'customers:write'
        assert APIScope.BUSINESS_READ == 'business:read'
        assert APIScope.WEBHOOKS_MANAGE == 'webhooks:manage'

    def test_choices_contains_all_scopes(self):
        """Verify CHOICES list contains tuples with descriptions."""
        assert len(APIScope.CHOICES) == 9
        assert all(isinstance(choice, tuple) and len(choice) == 2 for choice in APIScope.CHOICES)
        assert (APIScope.SERVICES_READ, 'View services and pricing') in APIScope.CHOICES

    def test_all_scopes_extracted_from_choices(self):
        """Verify ALL_SCOPES contains all scope strings."""
        assert len(APIScope.ALL_SCOPES) == 9
        assert APIScope.SERVICES_READ in APIScope.ALL_SCOPES
        assert APIScope.WEBHOOKS_MANAGE in APIScope.ALL_SCOPES

    def test_booking_widget_scopes_grouping(self):
        """Verify BOOKING_WIDGET_SCOPES contains appropriate scopes."""
        expected = [
            APIScope.SERVICES_READ,
            APIScope.RESOURCES_READ,
            APIScope.AVAILABILITY_READ,
            APIScope.BOOKINGS_WRITE,
            APIScope.CUSTOMERS_WRITE,
        ]
        assert APIScope.BOOKING_WIDGET_SCOPES == expected

    def test_business_directory_scopes_grouping(self):
        """Verify BUSINESS_DIRECTORY_SCOPES contains appropriate scopes."""
        expected = [
            APIScope.BUSINESS_READ,
            APIScope.SERVICES_READ,
            APIScope.RESOURCES_READ,
        ]
        assert APIScope.BUSINESS_DIRECTORY_SCOPES == expected

    def test_appointment_dashboard_scopes_grouping(self):
        """Verify APPOINTMENT_DASHBOARD_SCOPES contains appropriate scopes."""
        expected = [
            APIScope.BOOKINGS_READ,
            APIScope.BOOKINGS_WRITE,
            APIScope.CUSTOMERS_READ,
        ]
        assert APIScope.APPOINTMENT_DASHBOARD_SCOPES == expected

    def test_customer_self_service_scopes_grouping(self):
        """Verify CUSTOMER_SELF_SERVICE_SCOPES contains appropriate scopes."""
        expected = [
            APIScope.BOOKINGS_READ,
            APIScope.BOOKINGS_WRITE,
            APIScope.AVAILABILITY_READ,
        ]
        assert APIScope.CUSTOMER_SELF_SERVICE_SCOPES == expected

    def test_full_integration_scopes_equals_all_scopes(self):
        """Verify FULL_INTEGRATION_SCOPES includes all available scopes."""
        assert APIScope.FULL_INTEGRATION_SCOPES == APIScope.ALL_SCOPES


# ==============================================================================
# APIToken Tests
# ==============================================================================


class TestAPITokenGenerateKey:
    """Test APIToken.generate_key() class method."""

    def test_generate_live_key_format(self):
        """Verify live key starts with ss_live_ and has correct length."""
        full_key, key_hash, key_prefix = APIToken.generate_key(is_sandbox=False)

        assert full_key.startswith('ss_live_')
        assert len(full_key) == 72  # "ss_live_" (8) + 64 hex chars
        assert key_prefix == full_key[:16]

    def test_generate_sandbox_key_format(self):
        """Verify sandbox key starts with ss_test_ and has correct length."""
        full_key, key_hash, key_prefix = APIToken.generate_key(is_sandbox=True)

        assert full_key.startswith('ss_test_')
        assert len(full_key) == 72  # "ss_test_" (8) + 64 hex chars
        assert key_prefix == full_key[:16]

    def test_generate_key_returns_sha256_hash(self):
        """Verify the returned hash is SHA-256 of the full key."""
        full_key, key_hash, key_prefix = APIToken.generate_key()

        expected_hash = hashlib.sha256(full_key.encode()).hexdigest()
        assert key_hash == expected_hash

    def test_generate_key_prefix_is_first_16_chars(self):
        """Verify key_prefix is the first 16 characters of full key."""
        full_key, key_hash, key_prefix = APIToken.generate_key()

        assert key_prefix == full_key[:16]
        assert len(key_prefix) == 16

    @patch('smoothschedule.platform.api.models.secrets.token_hex')
    def test_generate_key_uses_secure_random(self, mock_token_hex):
        """Verify generate_key uses secrets.token_hex for randomness."""
        mock_token_hex.return_value = 'a' * 64

        full_key, _, _ = APIToken.generate_key()

        mock_token_hex.assert_called_once_with(32)
        assert 'a' * 64 in full_key

    def test_generate_key_produces_unique_keys(self):
        """Verify multiple calls produce different keys."""
        key1, _, _ = APIToken.generate_key()
        key2, _, _ = APIToken.generate_key()
        key3, _, _ = APIToken.generate_key()

        assert key1 != key2
        assert key2 != key3
        assert key1 != key3


class TestAPITokenIsSandboxKey:
    """Test APIToken.is_sandbox_key() class method."""

    def test_is_sandbox_key_returns_true_for_test_key(self):
        """Verify test keys are identified as sandbox."""
        assert APIToken.is_sandbox_key('ss_test_abc123') is True

    def test_is_sandbox_key_returns_false_for_live_key(self):
        """Verify live keys are not identified as sandbox."""
        assert APIToken.is_sandbox_key('ss_live_abc123') is False

    def test_is_sandbox_key_returns_false_for_invalid_key(self):
        """Verify invalid keys are not identified as sandbox."""
        assert APIToken.is_sandbox_key('invalid_key') is False
        assert APIToken.is_sandbox_key('') is False


class TestAPITokenHashKey:
    """Test APIToken.hash_key() class method."""

    def test_hash_key_returns_sha256_hex(self):
        """Verify hash_key returns SHA-256 hex digest."""
        key = 'ss_live_test123'
        expected = hashlib.sha256(key.encode()).hexdigest()

        assert APIToken.hash_key(key) == expected

    def test_hash_key_same_input_produces_same_hash(self):
        """Verify hashing is deterministic."""
        key = 'ss_test_abc123'
        hash1 = APIToken.hash_key(key)
        hash2 = APIToken.hash_key(key)

        assert hash1 == hash2

    def test_hash_key_different_inputs_produce_different_hashes(self):
        """Verify different keys produce different hashes."""
        hash1 = APIToken.hash_key('ss_live_key1')
        hash2 = APIToken.hash_key('ss_live_key2')

        assert hash1 != hash2


class TestAPITokenGetByKey:
    """Test APIToken.get_by_key() class method."""

    @patch('smoothschedule.platform.api.models.APIToken.objects')
    def test_get_by_key_hashes_key_and_queries(self, mock_objects):
        """Verify get_by_key hashes the key and queries database."""
        key = 'ss_live_test123'
        expected_hash = APIToken.hash_key(key)

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

        result = APIToken.get_by_key(key)

        mock_objects.select_related.assert_called_once_with('tenant')
        mock_queryset.get.assert_called_once_with(
            key_hash=expected_hash,
            is_active=True
        )
        assert result == mock_token

    @patch('smoothschedule.platform.api.models.APIToken.objects')
    def test_get_by_key_returns_none_when_not_found(self, mock_objects):
        """Verify get_by_key returns None when token doesn't exist."""
        mock_queryset = Mock()
        mock_queryset.get.side_effect = APIToken.DoesNotExist
        mock_objects.select_related.return_value = mock_queryset

        result = APIToken.get_by_key('ss_live_nonexistent')

        assert result is None

    @patch('smoothschedule.platform.api.models.APIToken.objects')
    def test_get_by_key_only_returns_active_tokens(self, mock_objects):
        """Verify get_by_key filters for is_active=True."""
        key = 'ss_live_test123'

        mock_queryset = Mock()
        mock_objects.select_related.return_value = mock_queryset

        APIToken.get_by_key(key)

        call_kwargs = mock_queryset.get.call_args[1]
        assert call_kwargs['is_active'] is True


class TestAPITokenInstanceMethods:
    """Test APIToken instance methods."""

    def test_str_returns_name_and_prefix(self):
        """Verify __str__ returns formatted string with name and prefix."""
        token = APIToken(name='Test Token', key_prefix='ss_live_abc123')

        assert str(token) == 'Test Token (ss_live_abc123...)'

    def test_has_scope_returns_true_when_scope_exists(self):
        """Verify has_scope returns True for granted scopes."""
        token = APIToken(scopes=[APIScope.BOOKINGS_READ, APIScope.SERVICES_READ])

        assert token.has_scope(APIScope.BOOKINGS_READ) is True
        assert token.has_scope(APIScope.SERVICES_READ) is True

    def test_has_scope_returns_false_when_scope_missing(self):
        """Verify has_scope returns False for non-granted scopes."""
        token = APIToken(scopes=[APIScope.BOOKINGS_READ])

        assert token.has_scope(APIScope.BOOKINGS_WRITE) is False
        assert token.has_scope(APIScope.CUSTOMERS_READ) is False

    def test_has_any_scope_returns_true_when_any_match(self):
        """Verify has_any_scope returns True if any scope matches."""
        token = APIToken(scopes=[APIScope.BOOKINGS_READ])

        assert token.has_any_scope([
            APIScope.BOOKINGS_READ,
            APIScope.BOOKINGS_WRITE
        ]) is True

    def test_has_any_scope_returns_false_when_no_match(self):
        """Verify has_any_scope returns False if no scopes match."""
        token = APIToken(scopes=[APIScope.SERVICES_READ])

        assert token.has_any_scope([
            APIScope.BOOKINGS_READ,
            APIScope.BOOKINGS_WRITE
        ]) is False

    def test_has_all_scopes_returns_true_when_all_match(self):
        """Verify has_all_scopes returns True when all scopes are granted."""
        token = APIToken(scopes=[
            APIScope.BOOKINGS_READ,
            APIScope.BOOKINGS_WRITE,
            APIScope.SERVICES_READ
        ])

        assert token.has_all_scopes([
            APIScope.BOOKINGS_READ,
            APIScope.BOOKINGS_WRITE
        ]) is True

    def test_has_all_scopes_returns_false_when_any_missing(self):
        """Verify has_all_scopes returns False if any scope is missing."""
        token = APIToken(scopes=[APIScope.BOOKINGS_READ])

        assert token.has_all_scopes([
            APIScope.BOOKINGS_READ,
            APIScope.BOOKINGS_WRITE
        ]) is False

    def test_is_expired_returns_false_when_no_expiration(self):
        """Verify is_expired returns False when expires_at is None."""
        token = APIToken(expires_at=None)

        assert token.is_expired() is False

    @patch('smoothschedule.platform.api.models.timezone.now')
    def test_is_expired_returns_false_when_not_yet_expired(self, mock_now):
        """Verify is_expired returns False when current time before expiration."""
        now = timezone.make_aware(datetime(2024, 1, 1, 12, 0))
        future = timezone.make_aware(datetime(2024, 1, 2, 12, 0))
        mock_now.return_value = now

        token = APIToken(expires_at=future)

        assert token.is_expired() is False

    @patch('smoothschedule.platform.api.models.timezone.now')
    def test_is_expired_returns_true_when_expired(self, mock_now):
        """Verify is_expired returns True when current time after expiration."""
        now = timezone.make_aware(datetime(2024, 1, 2, 12, 0))
        past = timezone.make_aware(datetime(2024, 1, 1, 12, 0))
        mock_now.return_value = now

        token = APIToken(expires_at=past)

        assert token.is_expired() is True

    def test_is_valid_returns_true_when_active_and_not_expired(self):
        """Verify is_valid returns True for active, non-expired tokens."""
        token = APIToken(is_active=True, expires_at=None)

        with patch.object(token, 'is_expired', return_value=False):
            assert token.is_valid() is True

    def test_is_valid_returns_false_when_inactive(self):
        """Verify is_valid returns False for inactive tokens."""
        token = APIToken(is_active=False, expires_at=None)

        with patch.object(token, 'is_expired', return_value=False):
            assert token.is_valid() is False

    def test_is_valid_returns_false_when_expired(self):
        """Verify is_valid returns False for expired tokens."""
        token = APIToken(is_active=True)

        with patch.object(token, 'is_expired', return_value=True):
            assert token.is_valid() is False

    @patch('smoothschedule.platform.api.models.timezone.now')
    def test_update_last_used_sets_timestamp_and_saves(self, mock_now):
        """Verify update_last_used updates timestamp and saves only that field."""
        now = timezone.make_aware(datetime(2024, 1, 1, 12, 0))
        mock_now.return_value = now

        token = APIToken(last_used_at=None)
        token.save = Mock()

        token.update_last_used()

        assert token.last_used_at == now
        token.save.assert_called_once_with(update_fields=['last_used_at'])


class TestAPITokenValidation:
    """Test APIToken.clean() validation method."""

    def test_clean_allows_plaintext_for_sandbox_tokens(self):
        """Verify clean allows plaintext_key for sandbox tokens with ss_test_ prefix."""
        token = APIToken(
            is_sandbox=True,
            plaintext_key='ss_test_abc123'
        )

        # Should not raise
        token.clean()

    def test_clean_raises_when_plaintext_on_live_token(self):
        """Verify clean raises ValidationError for plaintext_key on live tokens."""
        token = APIToken(
            is_sandbox=False,
            plaintext_key='ss_test_abc123'
        )

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

        assert 'plaintext_key' in exc_info.value.message_dict
        assert 'SECURITY VIOLATION' in exc_info.value.message_dict['plaintext_key'][0]
        assert 'live/production tokens' in exc_info.value.message_dict['plaintext_key'][0]

    def test_clean_raises_when_plaintext_starts_with_ss_live(self):
        """Verify clean raises ValidationError if plaintext_key starts with ss_live_."""
        token = APIToken(
            is_sandbox=True,
            plaintext_key='ss_live_abc123'  # This should never be stored in plaintext
        )

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

        assert 'plaintext_key' in exc_info.value.message_dict
        assert 'ss_live_*' in exc_info.value.message_dict['plaintext_key'][0]

    def test_clean_raises_when_plaintext_not_ss_test_format(self):
        """Verify clean raises ValidationError for invalid plaintext_key format."""
        token = APIToken(
            is_sandbox=True,
            plaintext_key='invalid_key_format'
        )

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

        assert 'plaintext_key' in exc_info.value.message_dict
        assert 'Must start with ss_test_' in exc_info.value.message_dict['plaintext_key'][0]

    def test_clean_allows_no_plaintext_key(self):
        """Verify clean passes when plaintext_key is None or empty."""
        token1 = APIToken(is_sandbox=False, plaintext_key=None)
        token2 = APIToken(is_sandbox=True, plaintext_key=None)
        token3 = APIToken(is_sandbox=False, plaintext_key='')

        # None should not raise
        token1.clean()
        token2.clean()
        token3.clean()

    @patch.object(APIToken, 'full_clean')
    def test_save_calls_full_clean(self, mock_full_clean):
        """Verify save() always calls full_clean() for validation."""
        token = APIToken()
        token.save = Mock()  # Mock the parent save

        # Create a real save method that calls full_clean
        def custom_save(*args, **kwargs):
            token.full_clean()

        with patch.object(APIToken, 'save', custom_save):
            with patch('smoothschedule.platform.api.models.models.Model.save'):
                custom_save()

        mock_full_clean.assert_called_once()


# ==============================================================================
# WebhookEvent Tests
# ==============================================================================


class TestWebhookEvent:
    """Test WebhookEvent constants."""

    def test_event_constants_exist(self):
        """Verify all expected event constants are defined."""
        assert WebhookEvent.APPOINTMENT_CREATED == 'appointment.created'
        assert WebhookEvent.APPOINTMENT_UPDATED == 'appointment.updated'
        assert WebhookEvent.APPOINTMENT_CANCELLED == 'appointment.cancelled'
        assert WebhookEvent.APPOINTMENT_COMPLETED == 'appointment.completed'
        assert WebhookEvent.APPOINTMENT_REMINDER == 'appointment.reminder'
        assert WebhookEvent.CUSTOMER_CREATED == 'customer.created'
        assert WebhookEvent.CUSTOMER_UPDATED == 'customer.updated'
        assert WebhookEvent.PAYMENT_SUCCEEDED == 'payment.succeeded'
        assert WebhookEvent.PAYMENT_FAILED == 'payment.failed'

    def test_choices_contains_all_events(self):
        """Verify CHOICES list contains tuples with descriptions."""
        assert len(WebhookEvent.CHOICES) == 9
        assert all(isinstance(choice, tuple) and len(choice) == 2 for choice in WebhookEvent.CHOICES)

    def test_all_events_extracted_from_choices(self):
        """Verify ALL_EVENTS contains all event strings."""
        assert len(WebhookEvent.ALL_EVENTS) == 9
        assert WebhookEvent.APPOINTMENT_CREATED in WebhookEvent.ALL_EVENTS
        assert WebhookEvent.PAYMENT_FAILED in WebhookEvent.ALL_EVENTS


# ==============================================================================
# WebhookSubscription Tests
# ==============================================================================


class TestWebhookSubscriptionGenerateSecret:
    """Test WebhookSubscription.generate_secret() class method."""

    @patch('smoothschedule.platform.api.models.secrets.token_hex')
    def test_generate_secret_uses_secure_random(self, mock_token_hex):
        """Verify generate_secret uses secrets.token_hex."""
        mock_token_hex.return_value = 'abc123'

        result = WebhookSubscription.generate_secret()

        mock_token_hex.assert_called_once_with(32)
        assert result == 'abc123'

    def test_generate_secret_produces_unique_secrets(self):
        """Verify multiple calls produce different secrets."""
        secret1 = WebhookSubscription.generate_secret()
        secret2 = WebhookSubscription.generate_secret()
        secret3 = WebhookSubscription.generate_secret()

        assert secret1 != secret2
        assert secret2 != secret3


class TestWebhookSubscriptionInstanceMethods:
    """Test WebhookSubscription instance methods."""

    def test_str_returns_url_and_event_count(self):
        """Verify __str__ returns formatted string with URL and event count."""
        subscription = WebhookSubscription(
            url='https://example.com/webhook',
            events=['appointment.created', 'customer.created']
        )

        assert str(subscription) == 'Webhook to https://example.com/webhook (2 events)'

    def test_is_subscribed_to_returns_true_for_subscribed_event(self):
        """Verify is_subscribed_to returns True for subscribed events."""
        subscription = WebhookSubscription(
            events=[WebhookEvent.APPOINTMENT_CREATED, WebhookEvent.CUSTOMER_CREATED]
        )

        assert subscription.is_subscribed_to(WebhookEvent.APPOINTMENT_CREATED) is True
        assert subscription.is_subscribed_to(WebhookEvent.CUSTOMER_CREATED) is True

    def test_is_subscribed_to_returns_false_for_unsubscribed_event(self):
        """Verify is_subscribed_to returns False for non-subscribed events."""
        subscription = WebhookSubscription(
            events=[WebhookEvent.APPOINTMENT_CREATED]
        )

        assert subscription.is_subscribed_to(WebhookEvent.PAYMENT_SUCCEEDED) is False

    @patch('smoothschedule.platform.api.models.timezone.now')
    def test_record_success_resets_failure_count_and_updates_timestamps(self, mock_now):
        """Verify record_success resets failures and updates success timestamp."""
        now = timezone.make_aware(datetime(2024, 1, 1, 12, 0))
        mock_now.return_value = now

        subscription = WebhookSubscription(
            failure_count=5,
            last_success_at=None,
            last_triggered_at=None
        )
        subscription.save = Mock()

        subscription.record_success()

        assert subscription.failure_count == 0
        assert subscription.last_success_at == now
        assert subscription.last_triggered_at == now
        subscription.save.assert_called_once_with(
            update_fields=['failure_count', 'last_success_at', 'last_triggered_at']
        )

    @patch('smoothschedule.platform.api.models.timezone.now')
    def test_record_failure_increments_count_and_updates_timestamp(self, mock_now):
        """Verify record_failure increments count and updates failure timestamp."""
        now = timezone.make_aware(datetime(2024, 1, 1, 12, 0))
        mock_now.return_value = now

        subscription = WebhookSubscription(
            failure_count=3,
            is_active=True
        )
        subscription.save = Mock()

        subscription.record_failure()

        assert subscription.failure_count == 4
        assert subscription.last_failure_at == now
        assert subscription.last_triggered_at == now
        assert subscription.is_active is True  # Not yet at max failures

    @patch('smoothschedule.platform.api.models.timezone.now')
    def test_record_failure_disables_after_max_failures(self, mock_now):
        """Verify record_failure disables subscription after max consecutive failures."""
        now = timezone.make_aware(datetime(2024, 1, 1, 12, 0))
        mock_now.return_value = now

        subscription = WebhookSubscription(
            failure_count=9,  # One below max
            is_active=True
        )
        subscription.save = Mock()

        subscription.record_failure()

        assert subscription.failure_count == 10
        assert subscription.is_active is False  # Now disabled
        subscription.save.assert_called_once_with(
            update_fields=['failure_count', 'last_failure_at', 'last_triggered_at', 'is_active']
        )

    def test_max_consecutive_failures_constant(self):
        """Verify MAX_CONSECUTIVE_FAILURES is set to 10."""
        assert WebhookSubscription.MAX_CONSECUTIVE_FAILURES == 10


# ==============================================================================
# WebhookDelivery Tests
# ==============================================================================


class TestWebhookDeliveryInstanceMethods:
    """Test WebhookDelivery instance methods."""

    @patch.object(WebhookDelivery, 'subscription', new_callable=lambda: Mock(url='https://example.com/webhook'))
    def test_str_returns_formatted_string_for_success(self, mock_subscription):
        """Verify __str__ returns formatted string for successful delivery."""
        delivery = WebhookDelivery(
            event_type=WebhookEvent.APPOINTMENT_CREATED,
            success=True,
            retry_count=0
        )

        assert str(delivery) == 'appointment.created to https://example.com/webhook - Success'

    @patch.object(WebhookDelivery, 'subscription', new_callable=lambda: Mock(url='https://example.com/webhook'))
    def test_str_returns_formatted_string_for_failure(self, mock_subscription):
        """Verify __str__ returns formatted string for failed delivery."""
        delivery = WebhookDelivery(
            event_type=WebhookEvent.PAYMENT_FAILED,
            success=False,
            retry_count=2
        )

        assert str(delivery) == 'payment.failed to https://example.com/webhook - Failed (retry 2)'

    def test_can_retry_returns_true_when_not_success_and_under_max(self):
        """Verify can_retry returns True when delivery failed and retries available."""
        delivery = WebhookDelivery(success=False, retry_count=2)

        assert delivery.can_retry() is True

    def test_can_retry_returns_false_when_success(self):
        """Verify can_retry returns False when delivery succeeded."""
        delivery = WebhookDelivery(success=True, retry_count=0)

        assert delivery.can_retry() is False

    def test_can_retry_returns_false_when_max_retries_reached(self):
        """Verify can_retry returns False when max retries reached."""
        delivery = WebhookDelivery(success=False, retry_count=5)

        assert delivery.can_retry() is False

    def test_max_retries_constant(self):
        """Verify MAX_RETRIES is set to 5."""
        assert WebhookDelivery.MAX_RETRIES == 5

    def test_retry_delays_constant(self):
        """Verify RETRY_DELAYS contains expected exponential backoff values."""
        expected = [60, 300, 1800, 7200, 28800]
        assert WebhookDelivery.RETRY_DELAYS == expected

    def test_get_next_retry_delay_returns_correct_delay_for_retry_count(self):
        """Verify get_next_retry_delay returns correct delay based on retry count."""
        delivery = WebhookDelivery(retry_count=0)
        assert delivery.get_next_retry_delay() == 60  # 1 min

        delivery.retry_count = 1
        assert delivery.get_next_retry_delay() == 300  # 5 min

        delivery.retry_count = 2
        assert delivery.get_next_retry_delay() == 1800  # 30 min

        delivery.retry_count = 3
        assert delivery.get_next_retry_delay() == 7200  # 2 hr

        delivery.retry_count = 4
        assert delivery.get_next_retry_delay() == 28800  # 8 hr

    def test_get_next_retry_delay_returns_last_delay_when_beyond_list(self):
        """Verify get_next_retry_delay returns last delay when retry count exceeds list."""
        delivery = WebhookDelivery(retry_count=10)

        assert delivery.get_next_retry_delay() == 28800  # Last value

    @patch('smoothschedule.platform.api.models.timezone.now')
    def test_schedule_retry_sets_next_retry_time(self, mock_now):
        """Verify schedule_retry calculates and sets next_retry_at."""
        now = timezone.make_aware(datetime(2024, 1, 1, 12, 0))
        mock_now.return_value = now

        delivery = WebhookDelivery(success=False, retry_count=1)
        delivery.save = Mock()

        result = delivery.schedule_retry()

        expected_time = now + timedelta(seconds=300)  # 5 min delay for retry_count=1
        assert delivery.next_retry_at == expected_time
        assert result is True
        delivery.save.assert_called_once_with(update_fields=['next_retry_at'])

    def test_schedule_retry_returns_false_when_cannot_retry(self):
        """Verify schedule_retry returns False when retries exhausted."""
        delivery = WebhookDelivery(success=False, retry_count=5)
        delivery.save = Mock()

        result = delivery.schedule_retry()

        assert result is False
        delivery.save.assert_not_called()

    def test_schedule_retry_returns_false_when_already_successful(self):
        """Verify schedule_retry returns False when delivery already succeeded."""
        delivery = WebhookDelivery(success=True, retry_count=0)
        delivery.save = Mock()

        result = delivery.schedule_retry()

        assert result is False
        delivery.save.assert_not_called()

    @patch('smoothschedule.platform.api.models.timezone.now')
    def test_mark_success_updates_fields_and_calls_subscription(self, mock_now):
        """Verify mark_success updates delivery and calls subscription.record_success()."""
        now = timezone.make_aware(datetime(2024, 1, 1, 12, 0))
        mock_now.return_value = now

        delivery = WebhookDelivery(
            success=False,
            response_status=None,
            delivered_at=None
        )

        # Patch the subscription property
        mock_subscription = Mock()
        with patch.object(type(delivery), 'subscription', new_callable=lambda: mock_subscription):
            delivery.save = Mock()

            delivery.mark_success(status_code=200, response_body='OK')

            assert delivery.success is True
            assert delivery.response_status == 200
            assert delivery.response_body == 'OK'
            assert delivery.delivered_at == now
            assert delivery.next_retry_at is None
            delivery.save.assert_called_once()
            mock_subscription.record_success.assert_called_once()

    @patch('smoothschedule.platform.api.models.timezone.now')
    def test_mark_success_truncates_large_response_body(self, mock_now):
        """Verify mark_success truncates response body to 10KB."""
        now = timezone.make_aware(datetime(2024, 1, 1, 12, 0))
        mock_now.return_value = now

        delivery = WebhookDelivery()

        # Patch the subscription property
        mock_subscription = Mock()
        with patch.object(type(delivery), 'subscription', new_callable=lambda: mock_subscription):
            delivery.save = Mock()

            large_body = 'x' * 20000  # 20KB
            delivery.mark_success(status_code=200, response_body=large_body)

            assert len(delivery.response_body) == 10240  # Truncated to 10KB

    def test_mark_failure_increments_retry_and_schedules_retry(self):
        """Verify mark_failure increments retry count and schedules next retry."""
        delivery = WebhookDelivery(
            retry_count=0,
            success=True  # Will be set to False
        )

        # Patch the subscription property
        mock_subscription = Mock()
        with patch.object(type(delivery), 'subscription', new_callable=lambda: mock_subscription):
            delivery.save = Mock()
            delivery.schedule_retry = Mock(return_value=True)

            delivery.mark_failure(error_message='Connection timeout', status_code=500, response_body='Error')

            assert delivery.success is False
            assert delivery.response_status == 500
            assert delivery.response_body == 'Error'
            assert delivery.error_message == 'Connection timeout'
            assert delivery.retry_count == 1
            delivery.save.assert_called_once()
            delivery.schedule_retry.assert_called_once()
            mock_subscription.record_success.assert_not_called()

    def test_mark_failure_truncates_large_response_body(self):
        """Verify mark_failure truncates response body to 10KB."""
        delivery = WebhookDelivery()

        # Patch the subscription property
        mock_subscription = Mock()
        with patch.object(type(delivery), 'subscription', new_callable=lambda: mock_subscription):
            delivery.save = Mock()
            delivery.schedule_retry = Mock(return_value=True)

            large_body = 'y' * 20000  # 20KB
            delivery.mark_failure(error_message='Error', response_body=large_body)

            assert len(delivery.response_body) == 10240  # Truncated to 10KB

    def test_mark_failure_calls_subscription_record_failure_when_max_retries(self):
        """Verify mark_failure calls subscription.record_failure() when retries exhausted."""
        delivery = WebhookDelivery(
            retry_count=4  # Will become 5 after increment (MAX_RETRIES)
        )

        # Patch the subscription property
        mock_subscription = Mock()
        with patch.object(type(delivery), 'subscription', new_callable=lambda: mock_subscription):
            delivery.save = Mock()

            delivery.mark_failure(error_message='Final failure')

            assert delivery.retry_count == 5
            # When can_retry() returns False, schedule_retry() is not called
            # Instead, subscription.record_failure() is called directly
            mock_subscription.record_failure.assert_called_once()

    def test_mark_failure_does_not_call_subscription_when_retries_available(self):
        """Verify mark_failure doesn't call subscription.record_failure() when retries remain."""
        delivery = WebhookDelivery(
            retry_count=2
        )

        # Patch the subscription property
        mock_subscription = Mock()
        with patch.object(type(delivery), 'subscription', new_callable=lambda: mock_subscription):
            delivery.save = Mock()
            delivery.schedule_retry = Mock(return_value=True)  # Can still retry

            delivery.mark_failure(error_message='Temporary failure')

            mock_subscription.record_failure.assert_not_called()
