"""
Unit tests for Stripe webhook signal handlers.

Tests webhook signal handling logic with mocks to avoid database calls.
Follows CLAUDE.md guidelines: prefer mocks, avoid @pytest.mark.django_db.

Note: The webhooks.py module uses djstripe signals. These tests mock the
handler functions' dependencies to test their logic in isolation.
"""
from unittest.mock import Mock, patch, MagicMock
import pytest
from decimal import Decimal

from smoothschedule.commerce.payments import webhooks
from smoothschedule.commerce.payments.models import TransactionLink


class TestHandleWebhookError:
    """Test webhook error logging."""

    @patch.object(webhooks, 'logger')
    def test_logs_webhook_error_with_details(self, mock_logger):
        """Test that webhook errors are logged with full context."""
        # Arrange
        mock_exception = ValueError("Invalid signature")
        event_type = "payment_intent.succeeded"
        sender = "StripeWebhookHandler"

        # Act
        webhooks.handle_webhook_error(
            sender=sender,
            exception=mock_exception,
            event_type=event_type,
        )

        # Assert
        mock_logger.error.assert_called_once()
        call_args = mock_logger.error.call_args

        # Verify log message
        assert "Stripe webhook error: payment_intent.succeeded" in call_args[0][0]

        # Verify extra context
        extra = call_args[1]['extra']
        assert extra['exception'] == "Invalid signature"
        assert extra['event_type'] == "payment_intent.succeeded"
        assert extra['sender'] == "StripeWebhookHandler"

        # Verify exc_info passed
        assert call_args[1]['exc_info'] == mock_exception

    @patch.object(webhooks, 'logger')
    def test_handles_different_exception_types(self, mock_logger):
        """Test logging handles various exception types."""
        # Arrange
        exceptions = [
            Exception("Generic error"),
            RuntimeError("Runtime error"),
            KeyError("missing_key"),
        ]

        # Act & Assert
        for exc in exceptions:
            mock_logger.reset_mock()
            webhooks.handle_webhook_error(sender="test", exception=exc, event_type="test.event")
            mock_logger.error.assert_called_once()


class TestHandlePaymentSucceeded:
    """Test successful payment handling."""

    @patch.object(webhooks, 'timezone')
    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'Event')
    @patch.object(webhooks, 'logger')
    def test_updates_transaction_and_event_on_success(
        self, mock_logger, mock_event_class, mock_transaction_link, mock_timezone
    ):
        """Test that successful payment updates transaction and event status."""
        # Arrange
        mock_now = Mock()
        mock_timezone.now.return_value = mock_now

        # Mock the event
        mock_event = Mock()
        mock_event.data.object.id = 'pi_test123'

        # Mock the transaction
        mock_transaction = Mock()
        mock_transaction.event.id = 123
        mock_transaction.event.status = 'SCHEDULED'
        mock_transaction.amount = Decimal('100.00')
        mock_transaction.tenant_revenue = Decimal('95.00')
        mock_transaction.platform_revenue = Decimal('5.00')

        mock_transaction_link.objects.get.return_value = mock_transaction
        mock_transaction_link.Status.SUCCEEDED = 'SUCCEEDED'
        mock_event_class.Status.PAID = 'PAID'

        # Act
        webhooks.handle_payment_succeeded(sender=None, event=mock_event)

        # Assert - Verify transaction lookup
        mock_transaction_link.objects.get.assert_called_once_with(
            payment_intent_id='pi_test123'
        )

        # Assert - Verify transaction update
        assert mock_transaction.status == 'SUCCEEDED'
        assert mock_transaction.completed_at == mock_now
        mock_transaction.save.assert_called_once()

        # Assert - Verify event status update
        assert mock_transaction.event.status == 'PAID'
        mock_transaction.event.save.assert_called_once()

        # Assert - Verify logging
        mock_logger.info.assert_called_once()
        log_call = mock_logger.info.call_args
        assert "Payment succeeded for Event 123" in log_call[0][0]
        assert log_call[1]['extra']['payment_intent_id'] == 'pi_test123'
        assert log_call[1]['extra']['event_id'] == 123

    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'logger')
    def test_logs_warning_when_transaction_not_found(
        self, mock_logger, mock_transaction_link
    ):
        """Test that missing transaction logs warning instead of crashing."""
        # Arrange
        mock_event = Mock()
        mock_event.data.object.id = 'pi_nonexistent'

        # Create a proper exception class for DoesNotExist
        mock_transaction_link.DoesNotExist = type('DoesNotExist', (Exception,), {})
        mock_transaction_link.objects.get.side_effect = (
            mock_transaction_link.DoesNotExist("Not found")
        )

        # Act
        webhooks.handle_payment_succeeded(sender=None, event=mock_event)

        # Assert
        mock_logger.warning.assert_called_once()
        warning_msg = mock_logger.warning.call_args[0][0]
        assert "TransactionLink not found" in warning_msg
        assert "pi_nonexistent" in warning_msg

    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'logger')
    def test_logs_error_on_unexpected_exception(
        self, mock_logger, mock_transaction_link
    ):
        """Test that unexpected exceptions are logged with exc_info."""
        # Arrange
        mock_event = Mock()
        mock_event.data.object.id = 'pi_test123'

        # Simulate unexpected error during save
        mock_transaction = Mock()
        mock_transaction.save.side_effect = RuntimeError("Database error")
        mock_transaction_link.objects.get.return_value = mock_transaction
        mock_transaction_link.Status.SUCCEEDED = 'SUCCEEDED'
        # Create a proper exception class for DoesNotExist
        mock_transaction_link.DoesNotExist = type('DoesNotExist', (Exception,), {})

        # Act
        webhooks.handle_payment_succeeded(sender=None, event=mock_event)

        # Assert
        mock_logger.error.assert_called_once()
        error_call = mock_logger.error.call_args
        assert "Error processing payment_intent.succeeded" in error_call[0][0]
        assert "Database error" in error_call[0][0]
        assert error_call[1]['exc_info'] is not None

    @patch.object(webhooks, 'timezone')
    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'Event')
    @patch.object(webhooks, 'logger')
    def test_handles_payment_intent_without_metadata(
        self, mock_logger, mock_event_class, mock_transaction_link, mock_timezone
    ):
        """Test handling payment intent with minimal data."""
        # Arrange
        mock_event = Mock()
        mock_event.data.object.id = 'pi_minimal'

        mock_transaction = Mock()
        mock_transaction.event.id = 999
        mock_transaction.amount = Decimal('50.00')
        mock_transaction.tenant_revenue = Decimal('47.50')
        mock_transaction.platform_revenue = Decimal('2.50')

        mock_transaction_link.objects.get.return_value = mock_transaction
        mock_transaction_link.Status.SUCCEEDED = 'SUCCEEDED'
        mock_event_class.Status.PAID = 'PAID'

        # Act
        webhooks.handle_payment_succeeded(sender=None, event=mock_event)

        # Assert - Should still succeed
        mock_transaction.save.assert_called_once()
        mock_transaction.event.save.assert_called_once()
        mock_logger.info.assert_called_once()


class TestHandlePaymentFailed:
    """Test failed payment handling."""

    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'logger')
    def test_updates_transaction_with_error_message(
        self, mock_logger, mock_transaction_link
    ):
        """Test that failed payment updates transaction with error details."""
        # Arrange
        mock_error = Mock()
        mock_error.message = "Insufficient funds"

        mock_event = Mock()
        mock_event.data.object.id = 'pi_failed123'
        mock_event.data.object.last_payment_error = mock_error

        mock_transaction = Mock()
        mock_transaction.event.id = 456
        mock_transaction_link.objects.get.return_value = mock_transaction
        mock_transaction_link.Status.FAILED = 'FAILED'

        # Act
        webhooks.handle_payment_failed(sender=None, event=mock_event)

        # Assert
        mock_transaction_link.objects.get.assert_called_once_with(
            payment_intent_id='pi_failed123'
        )
        assert mock_transaction.status == 'FAILED'
        assert mock_transaction.error_message == "Insufficient funds"
        mock_transaction.save.assert_called_once()

        # Assert - Verify warning log
        mock_logger.warning.assert_called_once()
        log_call = mock_logger.warning.call_args
        assert "Payment failed for Event 456" in log_call[0][0]
        assert log_call[1]['extra']['payment_intent_id'] == 'pi_failed123'
        assert log_call[1]['extra']['error'] == "Insufficient funds"

    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'logger')
    def test_handles_payment_failure_without_error_details(
        self, mock_logger, mock_transaction_link
    ):
        """Test handling failed payment when error details are missing."""
        # Arrange
        mock_event = Mock()
        mock_event.data.object.id = 'pi_failed_no_details'
        mock_event.data.object.last_payment_error = None

        mock_transaction = Mock()
        mock_transaction.event.id = 789
        mock_transaction_link.objects.get.return_value = mock_transaction
        mock_transaction_link.Status.FAILED = 'FAILED'

        # Act
        webhooks.handle_payment_failed(sender=None, event=mock_event)

        # Assert
        assert mock_transaction.status == 'FAILED'
        assert mock_transaction.error_message == 'Unknown error'
        mock_transaction.save.assert_called_once()

    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'logger')
    def test_logs_warning_when_failed_transaction_not_found(
        self, mock_logger, mock_transaction_link
    ):
        """Test that missing transaction logs warning for failed payment."""
        # Arrange
        mock_event = Mock()
        mock_event.data.object.id = 'pi_not_found'
        mock_event.data.object.last_payment_error = None

        # Create a proper exception class for DoesNotExist
        mock_transaction_link.DoesNotExist = type('DoesNotExist', (Exception,), {})
        mock_transaction_link.objects.get.side_effect = (
            mock_transaction_link.DoesNotExist("Not found")
        )

        # Act
        webhooks.handle_payment_failed(sender=None, event=mock_event)

        # Assert
        mock_logger.warning.assert_called_once()
        warning_msg = mock_logger.warning.call_args[0][0]
        assert "TransactionLink not found" in warning_msg
        assert "pi_not_found" in warning_msg

    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'logger')
    def test_logs_error_on_unexpected_failure_exception(
        self, mock_logger, mock_transaction_link
    ):
        """Test that unexpected exceptions during failure handling are logged."""
        # Arrange
        mock_event = Mock()
        mock_event.data.object.id = 'pi_error'
        mock_event.data.object.last_payment_error = Mock(message="Card declined")

        mock_transaction = Mock()
        mock_transaction.save.side_effect = Exception("Save failed")
        mock_transaction_link.objects.get.return_value = mock_transaction
        mock_transaction_link.Status.FAILED = 'FAILED'
        # Create a proper exception class for DoesNotExist
        mock_transaction_link.DoesNotExist = type('DoesNotExist', (Exception,), {})

        # Act
        webhooks.handle_payment_failed(sender=None, event=mock_event)

        # Assert
        mock_logger.error.assert_called_once()
        error_call = mock_logger.error.call_args
        assert "Error processing payment_failed" in error_call[0][0]
        assert "Save failed" in error_call[0][0]

    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'logger')
    def test_handles_error_message_attribute_variations(
        self, mock_logger, mock_transaction_link
    ):
        """Test handling various error message formats from Stripe."""
        # Arrange - Error with complex nested structure
        mock_error = Mock()
        mock_error.message = "Your card was declined"

        mock_event = Mock()
        mock_event.data.object.id = 'pi_complex_error'
        mock_event.data.object.last_payment_error = mock_error

        mock_transaction = Mock()
        mock_transaction.event.id = 111
        mock_transaction_link.objects.get.return_value = mock_transaction
        mock_transaction_link.Status.FAILED = 'FAILED'

        # Act
        webhooks.handle_payment_failed(sender=None, event=mock_event)

        # Assert
        assert mock_transaction.error_message == "Your card was declined"
        mock_transaction.save.assert_called_once()


class TestHandlePaymentCanceled:
    """Test canceled payment handling."""

    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'logger')
    def test_updates_transaction_status_to_canceled(
        self, mock_logger, mock_transaction_link
    ):
        """Test that canceled payment updates transaction status."""
        # Arrange
        mock_event = Mock()
        mock_event.data.object.id = 'pi_canceled123'

        mock_transaction = Mock()
        mock_transaction.event.id = 321
        mock_transaction_link.objects.get.return_value = mock_transaction
        mock_transaction_link.Status.CANCELED = 'CANCELED'

        # Act
        webhooks.handle_payment_canceled(sender=None, event=mock_event)

        # Assert
        mock_transaction_link.objects.get.assert_called_once_with(
            payment_intent_id='pi_canceled123'
        )
        assert mock_transaction.status == 'CANCELED'
        mock_transaction.save.assert_called_once()

        # Assert - Verify info log
        mock_logger.info.assert_called_once()
        log_msg = mock_logger.info.call_args[0][0]
        assert "Payment canceled for Event 321" in log_msg

    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'logger')
    def test_logs_warning_when_canceled_transaction_not_found(
        self, mock_logger, mock_transaction_link
    ):
        """Test that missing transaction logs warning for canceled payment."""
        # Arrange
        mock_event = Mock()
        mock_event.data.object.id = 'pi_no_tx'

        # Create a proper exception class for DoesNotExist
        mock_transaction_link.DoesNotExist = type('DoesNotExist', (Exception,), {})
        mock_transaction_link.objects.get.side_effect = (
            mock_transaction_link.DoesNotExist("Not found")
        )

        # Act
        webhooks.handle_payment_canceled(sender=None, event=mock_event)

        # Assert
        mock_logger.warning.assert_called_once()
        warning_msg = mock_logger.warning.call_args[0][0]
        assert "TransactionLink not found" in warning_msg
        assert "pi_no_tx" in warning_msg

    @patch.object(webhooks, 'logger')
    @patch.object(webhooks, 'TransactionLink')
    def test_logs_error_on_unexpected_cancellation_exception(
        self, mock_transaction_link, mock_logger
    ):
        """Test that unexpected exceptions during cancellation are logged."""
        # Arrange
        mock_event = Mock()
        mock_event.data.object.id = 'pi_exception'

        mock_transaction = Mock()
        mock_transaction.save.side_effect = ValueError("Invalid state")
        mock_transaction_link.objects.get.return_value = mock_transaction
        mock_transaction_link.Status.CANCELED = 'CANCELED'
        # Create a proper exception class for DoesNotExist
        mock_transaction_link.DoesNotExist = type('DoesNotExist', (Exception,), {})

        # Act
        webhooks.handle_payment_canceled(sender=None, event=mock_event)

        # Assert
        mock_logger.error.assert_called_once()
        error_call = mock_logger.error.call_args
        assert "Error processing payment_canceled" in error_call[0][0]
        assert "Invalid state" in error_call[0][0]

    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'logger')
    def test_does_not_update_event_status_on_cancellation(
        self, mock_logger, mock_transaction_link
    ):
        """Test that event status is NOT updated when payment is canceled."""
        # Arrange
        mock_event = Mock()
        mock_event.data.object.id = 'pi_cancel_no_event_update'

        mock_transaction = Mock()
        mock_transaction.event.id = 555
        mock_transaction.event.status = 'SCHEDULED'  # Should remain unchanged
        mock_transaction_link.objects.get.return_value = mock_transaction
        mock_transaction_link.Status.CANCELED = 'CANCELED'

        # Act
        webhooks.handle_payment_canceled(sender=None, event=mock_event)

        # Assert
        mock_transaction.save.assert_called_once()
        # Event save should NOT be called (unlike payment_succeeded)
        mock_transaction.event.save.assert_not_called()
        # Event status should remain unchanged
        assert mock_transaction.event.status == 'SCHEDULED'


class TestEdgeCases:
    """Test edge cases and error scenarios."""

    def test_handle_payment_succeeded_with_missing_event_object(self):
        """Test handling succeeded payment when event has no data.object."""
        # Arrange
        mock_event = Mock()
        mock_event.data.object = None

        # Act & Assert - Should raise AttributeError
        with pytest.raises(AttributeError):
            webhooks.handle_payment_succeeded(sender=None, event=mock_event)

    @patch.object(webhooks, 'timezone')
    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'Event')
    @patch.object(webhooks, 'logger')
    def test_multiple_webhooks_for_same_payment_intent(
        self, mock_logger, mock_event_class, mock_transaction_link, mock_timezone
    ):
        """Test that multiple webhook calls for same payment are handled idempotently."""
        # Arrange
        mock_event = Mock()
        mock_event.data.object.id = 'pi_duplicate'

        mock_transaction = Mock()
        mock_transaction.event.id = 999
        mock_transaction.amount = Decimal('100.00')
        mock_transaction.tenant_revenue = Decimal('95.00')
        mock_transaction.platform_revenue = Decimal('5.00')
        mock_transaction.status = 'SUCCEEDED'  # Already processed

        mock_transaction_link.objects.get.return_value = mock_transaction
        mock_transaction_link.Status.SUCCEEDED = 'SUCCEEDED'
        mock_event_class.Status.PAID = 'PAID'

        # Act - Call twice with same payment intent
        webhooks.handle_payment_succeeded(sender=None, event=mock_event)
        webhooks.handle_payment_succeeded(sender=None, event=mock_event)

        # Assert - Should be called twice (idempotent)
        assert mock_transaction_link.objects.get.call_count == 2
        assert mock_transaction.save.call_count == 2

    @patch.object(webhooks, 'logger')
    def test_webhook_error_with_none_exception(self, mock_logger):
        """Test handling webhook error when exception is None."""
        # Act
        webhooks.handle_webhook_error(sender="test", exception=None, event_type="test.event")

        # Assert - Should still log
        mock_logger.error.assert_called_once()
        call_args = mock_logger.error.call_args
        assert call_args[1]['extra']['exception'] == "None"

    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'logger')
    def test_payment_failed_with_empty_error_message(
        self, mock_logger, mock_transaction_link
    ):
        """Test handling payment failure when error message is empty string."""
        # Arrange
        mock_error = Mock()
        mock_error.message = ""  # Empty string

        mock_event = Mock()
        mock_event.data.object.id = 'pi_empty_error'
        mock_event.data.object.last_payment_error = mock_error

        mock_transaction = Mock()
        mock_transaction.event.id = 888
        mock_transaction_link.objects.get.return_value = mock_transaction
        mock_transaction_link.Status.FAILED = 'FAILED'

        # Act
        webhooks.handle_payment_failed(sender=None, event=mock_event)

        # Assert - Empty error message should be stored (not replaced)
        assert mock_transaction.error_message == ""
        mock_transaction.save.assert_called_once()

    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'logger')
    def test_transaction_link_does_not_exist_exception_is_caught(
        self, mock_logger, mock_transaction_link
    ):
        """Test that TransactionLink.DoesNotExist is properly caught and handled."""
        # Arrange
        mock_event_success = Mock()
        mock_event_success.data.object.id = 'pi_missing_success'

        mock_event_failed = Mock()
        mock_event_failed.data.object.id = 'pi_missing_failed'
        mock_event_failed.data.object.last_payment_error = None

        mock_event_canceled = Mock()
        mock_event_canceled.data.object.id = 'pi_missing_canceled'

        # Create a proper exception class for DoesNotExist
        mock_transaction_link.DoesNotExist = type('DoesNotExist', (Exception,), {})
        mock_transaction_link.objects.get.side_effect = (
            mock_transaction_link.DoesNotExist("Transaction not found")
        )

        # Act - Should not raise exception
        webhooks.handle_payment_succeeded(sender=None, event=mock_event_success)
        webhooks.handle_payment_failed(sender=None, event=mock_event_failed)
        webhooks.handle_payment_canceled(sender=None, event=mock_event_canceled)

        # Assert - Warning should be logged 3 times (once per handler)
        assert mock_logger.warning.call_count == 3


class TestLoggingDetails:
    """Test detailed logging behavior."""

    @patch.object(webhooks, 'timezone')
    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'Event')
    @patch.object(webhooks, 'logger')
    def test_payment_succeeded_logs_revenue_breakdown(
        self, mock_logger, mock_event_class, mock_transaction_link, mock_timezone
    ):
        """Test that successful payment logs tenant and platform revenue."""
        # Arrange
        mock_event = Mock()
        mock_event.data.object.id = 'pi_revenue_test'

        mock_transaction = Mock()
        mock_transaction.event.id = 100
        mock_transaction.amount = Decimal('200.00')
        mock_transaction.tenant_revenue = Decimal('190.00')
        mock_transaction.platform_revenue = Decimal('10.00')

        mock_transaction_link.objects.get.return_value = mock_transaction
        mock_transaction_link.Status.SUCCEEDED = 'SUCCEEDED'
        mock_event_class.Status.PAID = 'PAID'

        # Act
        webhooks.handle_payment_succeeded(sender=None, event=mock_event)

        # Assert
        log_call = mock_logger.info.call_args
        extra = log_call[1]['extra']

        # Verify revenue is logged as float
        assert extra['tenant_revenue'] == 190.00
        assert extra['platform_revenue'] == 10.00
        assert extra['amount'] == Decimal('200.00')

    @patch.object(webhooks, 'TransactionLink')
    @patch.object(webhooks, 'logger')
    def test_payment_failed_logs_payment_intent_and_error(
        self, mock_logger, mock_transaction_link
    ):
        """Test that failed payment logs both payment intent ID and error."""
        # Arrange
        mock_error = Mock()
        mock_error.message = "Card expired"

        mock_event = Mock()
        mock_event.data.object.id = 'pi_expired_card'
        mock_event.data.object.last_payment_error = mock_error

        mock_transaction = Mock()
        mock_transaction.event.id = 200
        mock_transaction_link.objects.get.return_value = mock_transaction
        mock_transaction_link.Status.FAILED = 'FAILED'

        # Act
        webhooks.handle_payment_failed(sender=None, event=mock_event)

        # Assert
        log_call = mock_logger.warning.call_args
        extra = log_call[1]['extra']

        assert extra['payment_intent_id'] == 'pi_expired_card'
        assert extra['error'] == "Card expired"

    @patch.object(webhooks, 'logger')
    def test_webhook_error_includes_sender_in_log(self, mock_logger):
        """Test that webhook error logs include sender information."""
        # Act
        webhooks.handle_webhook_error(
            sender="CustomWebhookHandler",
            exception=Exception("Test error"),
            event_type="custom.event"
        )

        # Assert
        log_call = mock_logger.error.call_args
        extra = log_call[1]['extra']

        assert extra['sender'] == "CustomWebhookHandler"
        assert extra['event_type'] == "custom.event"
