"""Tests for webhook tracking events system."""

import json
from datetime import UTC
from datetime import datetime
from typing import Any
from unittest.mock import AsyncMock
from unittest.mock import patch

import pytest

from amsdal_mail.events import EmailTrackingContext
from amsdal_mail.events import EmailTrackingEvent
from amsdal_mail.events import TrackingEventType
from amsdal_mail.webhooks.registry import WebhookRegistry
from amsdal_mail.webhooks.ses import SESWebhookParser

# ────────────────────────────── SES Event Fixtures ──────────────────────────────


def _sns_wrap(ses_event: dict[str, Any]) -> tuple[bytes, dict[str, str]]:
    """Wrap an SES event in an SNS Notification envelope."""
    sns_message = {
        'Type': 'Notification',
        'MessageId': 'sns-msg-id-001',
        'TopicArn': 'arn:aws:sns:us-east-1:123456789:ses-events',
        'Message': json.dumps(ses_event),
        'Timestamp': '2024-01-15T10:30:00.000Z',
    }
    headers = {
        'x-amz-sns-message-type': 'Notification',
        'x-amz-sns-message-id': 'sns-msg-id-001',
    }
    return json.dumps(sns_message).encode(), headers


def _make_ses_event(event_type: str, detail_key: str, detail: dict[str, Any], **mail_overrides: Any) -> dict[str, Any]:
    mail = {
        'messageId': 'ses-msg-001',
        'timestamp': '2024-01-15T10:00:00.000Z',
        'destination': ['user@example.com'],
        'tags': {},
        'headers': [],
        **mail_overrides,
    }
    return {
        'eventType': event_type,
        'mail': mail,
        detail_key: detail,
    }


# ────────────────────────────── WebhookRegistry ──────────────────────────────


class TestWebhookRegistry:
    def setup_method(self):
        WebhookRegistry.reset()

    def test_register_and_get(self):
        parser = SESWebhookParser()
        WebhookRegistry.register('ses', parser)

        assert WebhookRegistry.get_parser('ses') is parser

    def test_get_unregistered(self):
        assert WebhookRegistry.get_parser('nonexistent') is None

    def test_register_overwrites(self):
        parser1 = SESWebhookParser()
        parser2 = SESWebhookParser(secret='new')
        WebhookRegistry.register('ses', parser1)
        WebhookRegistry.register('ses', parser2)

        assert WebhookRegistry.get_parser('ses') is parser2

    def test_reset(self):
        WebhookRegistry.register('ses', SESWebhookParser())
        WebhookRegistry.reset()

        assert WebhookRegistry.get_parser('ses') is None


# ────────────────────────────── SES Verify ──────────────────────────────


class TestSESWebhookParserVerify:
    def setup_method(self):
        self.parser = SESWebhookParser()

    def _make_body(self, msg_type: str = 'Notification', msg_id: str = 'some-id') -> bytes:
        return json.dumps({'Type': msg_type, 'MessageId': msg_id}).encode()

    def test_valid_notification(self):
        body = self._make_body('Notification', 'id-1')
        headers = {'x-amz-sns-message-type': 'Notification', 'x-amz-sns-message-id': 'id-1'}
        assert self.parser.verify(body, headers) is True

    def test_valid_subscription_confirmation(self):
        body = self._make_body('SubscriptionConfirmation', 'id-2')
        headers = {'x-amz-sns-message-type': 'SubscriptionConfirmation', 'x-amz-sns-message-id': 'id-2'}
        assert self.parser.verify(body, headers) is True

    def test_missing_message_type_header(self):
        body = self._make_body('Notification', 'id-1')
        headers = {'x-amz-sns-message-id': 'id-1'}
        assert self.parser.verify(body, headers) is False

    def test_missing_message_id_header(self):
        body = self._make_body('Notification', 'id-1')
        headers = {'x-amz-sns-message-type': 'Notification'}
        assert self.parser.verify(body, headers) is False

    def test_invalid_message_type(self):
        body = self._make_body('InvalidType', 'id-1')
        headers = {'x-amz-sns-message-type': 'InvalidType', 'x-amz-sns-message-id': 'id-1'}
        assert self.parser.verify(body, headers) is False

    def test_empty_headers(self):
        assert self.parser.verify(b'{}', {}) is False

    def test_header_type_mismatch_body(self):
        body = self._make_body('SubscriptionConfirmation', 'id-1')
        headers = {'x-amz-sns-message-type': 'Notification', 'x-amz-sns-message-id': 'id-1'}
        assert self.parser.verify(body, headers) is False

    def test_header_id_mismatch_body(self):
        body = self._make_body('Notification', 'id-real')
        headers = {'x-amz-sns-message-type': 'Notification', 'x-amz-sns-message-id': 'id-spoofed'}
        assert self.parser.verify(body, headers) is False

    def test_invalid_json_body(self):
        headers = {'x-amz-sns-message-type': 'Notification', 'x-amz-sns-message-id': 'id-1'}
        assert self.parser.verify(b'not json', headers) is False

    def test_body_missing_type_field(self):
        body = json.dumps({'MessageId': 'id-1'}).encode()
        headers = {'x-amz-sns-message-type': 'Notification', 'x-amz-sns-message-id': 'id-1'}
        assert self.parser.verify(body, headers) is False


# ────────────────────────────── SES Parse: Subscription ──────────────────────────────


class TestSESWebhookParserSubscription:
    def setup_method(self):
        self.parser = SESWebhookParser()

    def test_subscription_confirmation_returns_empty(self):
        sns_message = {
            'Type': 'SubscriptionConfirmation',
            'SubscribeURL': 'https://sns.amazonaws.com/confirm?token=abc',
        }
        headers = {
            'x-amz-sns-message-type': 'SubscriptionConfirmation',
            'x-amz-sns-message-id': 'some-id',
        }
        result = self.parser.parse(json.dumps(sns_message).encode(), headers)
        assert result == []

    def test_unsubscribe_confirmation_returns_empty(self):
        sns_message = {'Type': 'UnsubscribeConfirmation'}
        headers = {
            'x-amz-sns-message-type': 'UnsubscribeConfirmation',
            'x-amz-sns-message-id': 'some-id',
        }
        result = self.parser.parse(json.dumps(sns_message).encode(), headers)
        assert result == []


# ────────────────────────────── SES Parse: Delivery ──────────────────────────────


class TestSESParseDelivery:
    def setup_method(self):
        self.parser = SESWebhookParser()

    def test_delivery_event(self):
        ses_event = _make_ses_event(
            'Delivery',
            'delivery',
            {
                'timestamp': '2024-01-15T10:05:00.000Z',
                'recipients': ['user@example.com'],
            },
        )
        body, headers = _sns_wrap(ses_event)
        result = self.parser.parse(body, headers)

        assert len(result) == 1
        ctx = result[0]
        assert ctx.event_type == TrackingEventType.DELIVERED
        assert ctx.message_id == 'ses-msg-001'
        assert ctx.recipient == 'user@example.com'
        assert ctx.esp_name == 'ses'
        assert ctx.timestamp == datetime(2024, 1, 15, 10, 5, 0, tzinfo=UTC)

    def test_delivery_multiple_recipients(self):
        ses_event = _make_ses_event(
            'Delivery',
            'delivery',
            {
                'timestamp': '2024-01-15T10:05:00.000Z',
                'recipients': ['a@example.com', 'b@example.com', 'c@example.com'],
            },
        )
        body, headers = _sns_wrap(ses_event)
        result = self.parser.parse(body, headers)

        assert len(result) == 3
        assert {ctx.recipient for ctx in result} == {'a@example.com', 'b@example.com', 'c@example.com'}


# ────────────────────────────── SES Parse: Bounce ──────────────────────────────


class TestSESParseBounce:
    def setup_method(self):
        self.parser = SESWebhookParser()

    def test_bounce_event(self):
        ses_event = _make_ses_event(
            'Bounce',
            'bounce',
            {
                'bounceType': 'Permanent',
                'bounceSubType': 'General',
                'timestamp': '2024-01-15T10:05:00.000Z',
                'bouncedRecipients': [
                    {
                        'emailAddress': 'bounce@example.com',
                        'diagnosticCode': 'smtp; 550 User not found',
                    }
                ],
            },
        )
        body, headers = _sns_wrap(ses_event)
        result = self.parser.parse(body, headers)

        assert len(result) == 1
        ctx = result[0]
        assert ctx.event_type == TrackingEventType.BOUNCED
        assert ctx.recipient == 'bounce@example.com'
        assert ctx.reject_reason == 'General'
        assert ctx.description == 'smtp; 550 User not found'

    def test_bounce_multiple_recipients(self):
        ses_event = _make_ses_event(
            'Bounce',
            'bounce',
            {
                'bounceType': 'Permanent',
                'bounceSubType': 'General',
                'timestamp': '2024-01-15T10:05:00.000Z',
                'bouncedRecipients': [
                    {'emailAddress': 'a@example.com', 'diagnosticCode': 'err1'},
                    {'emailAddress': 'b@example.com', 'diagnosticCode': 'err2'},
                ],
            },
        )
        body, headers = _sns_wrap(ses_event)
        result = self.parser.parse(body, headers)

        assert len(result) == 2
        assert {ctx.recipient for ctx in result} == {'a@example.com', 'b@example.com'}

    def test_bounce_fallback_to_bounce_type(self):
        ses_event = _make_ses_event(
            'Bounce',
            'bounce',
            {
                'bounceType': 'Transient',
                'timestamp': '2024-01-15T10:05:00.000Z',
                'bouncedRecipients': [{'emailAddress': 'x@example.com'}],
            },
        )
        body, headers = _sns_wrap(ses_event)
        result = self.parser.parse(body, headers)

        assert result[0].reject_reason == 'Transient'


# ────────────────────────────── SES Parse: Complaint ──────────────────────────────


class TestSESParseComplaint:
    def setup_method(self):
        self.parser = SESWebhookParser()

    def test_complaint_event(self):
        ses_event = _make_ses_event(
            'Complaint',
            'complaint',
            {
                'timestamp': '2024-01-15T10:05:00.000Z',
                'complainedRecipients': [{'emailAddress': 'spam@example.com'}],
            },
        )
        body, headers = _sns_wrap(ses_event)
        result = self.parser.parse(body, headers)

        assert len(result) == 1
        assert result[0].event_type == TrackingEventType.COMPLAINED
        assert result[0].recipient == 'spam@example.com'


# ────────────────────────────── SES Parse: Send ──────────────────────────────


class TestSESParseSend:
    def setup_method(self):
        self.parser = SESWebhookParser()

    def test_send_event(self):
        ses_event = _make_ses_event('Send', 'send', {'timestamp': '2024-01-15T10:00:00.000Z'})
        body, headers = _sns_wrap(ses_event)
        result = self.parser.parse(body, headers)

        assert len(result) == 1
        assert result[0].event_type == TrackingEventType.SENT
        assert result[0].recipient == 'user@example.com'


# ────────────────────────────── SES Parse: Reject ──────────────────────────────


class TestSESParseReject:
    def setup_method(self):
        self.parser = SESWebhookParser()

    def test_reject_event(self):
        ses_event = _make_ses_event(
            'Reject',
            'reject',
            {'reason': 'Bad content', 'timestamp': '2024-01-15T10:00:00.000Z'},
        )
        body, headers = _sns_wrap(ses_event)
        result = self.parser.parse(body, headers)

        assert len(result) == 1
        assert result[0].event_type == TrackingEventType.REJECTED
        assert result[0].reject_reason == 'Bad content'


# ────────────────────────────── SES Parse: Open ──────────────────────────────


class TestSESParseOpen:
    def setup_method(self):
        self.parser = SESWebhookParser()

    def test_open_event(self):
        ses_event = _make_ses_event(
            'Open',
            'open',
            {
                'timestamp': '2024-01-15T12:00:00.000Z',
                'userAgent': 'Mozilla/5.0',
            },
        )
        body, headers = _sns_wrap(ses_event)
        result = self.parser.parse(body, headers)

        assert len(result) == 1
        assert result[0].event_type == TrackingEventType.OPENED
        assert result[0].user_agent == 'Mozilla/5.0'


# ────────────────────────────── SES Parse: Click ──────────────────────────────


class TestSESParseClick:
    def setup_method(self):
        self.parser = SESWebhookParser()

    def test_click_event(self):
        ses_event = _make_ses_event(
            'Click',
            'click',
            {
                'timestamp': '2024-01-15T12:00:00.000Z',
                'link': 'https://example.com/promo',
                'userAgent': 'Chrome/120',
            },
        )
        body, headers = _sns_wrap(ses_event)
        result = self.parser.parse(body, headers)

        assert len(result) == 1
        assert result[0].event_type == TrackingEventType.CLICKED
        assert result[0].click_url == 'https://example.com/promo'
        assert result[0].user_agent == 'Chrome/120'


# ────────────────────────────── SES Parse: DeliveryDelay ──────────────────────────────


class TestSESParseDeliveryDelay:
    def setup_method(self):
        self.parser = SESWebhookParser()

    def test_delivery_delay_event(self):
        ses_event = _make_ses_event(
            'DeliveryDelay',
            'deliveryDelay',
            {
                'timestamp': '2024-01-15T10:10:00.000Z',
                'delayType': 'InternalFailure',
            },
        )
        body, headers = _sns_wrap(ses_event)
        result = self.parser.parse(body, headers)

        assert len(result) == 1
        assert result[0].event_type == TrackingEventType.DEFERRED
        assert result[0].description == 'InternalFailure'


# ────────────────────────────── SES Parse: Unknown ──────────────────────────────


class TestSESParseUnknown:
    def setup_method(self):
        self.parser = SESWebhookParser()

    def test_unknown_event_type(self):
        ses_event = {
            'eventType': 'SomeFutureEventType',
            'mail': {
                'messageId': 'ses-msg-001',
                'timestamp': '2024-01-15T10:00:00.000Z',
                'destination': ['user@example.com'],
                'tags': {},
                'headers': [],
            },
        }
        body, headers = _sns_wrap(ses_event)
        result = self.parser.parse(body, headers)

        assert len(result) == 1
        assert result[0].event_type == TrackingEventType.UNKNOWN


# ────────────────────────────── SES Parse: Tags & Metadata ──────────────────────────────


class TestSESParseTagsMetadata:
    def setup_method(self):
        self.parser = SESWebhookParser()

    def test_tags_extraction(self):
        ses_event = _make_ses_event(
            'Delivery',
            'delivery',
            {'timestamp': '2024-01-15T10:05:00.000Z', 'recipients': ['user@example.com']},
            tags={'campaign': ['welcome'], 'env': ['production']},
        )
        body, headers = _sns_wrap(ses_event)
        result = self.parser.parse(body, headers)

        assert 'campaign:welcome' in result[0].tags
        assert 'env:production' in result[0].tags

    def test_metadata_from_custom_headers(self):
        ses_event = _make_ses_event(
            'Delivery',
            'delivery',
            {'timestamp': '2024-01-15T10:05:00.000Z', 'recipients': ['user@example.com']},
            headers=[
                {'name': 'X-Metadata-order-id', 'value': '12345'},
                {'name': 'X-Metadata-user-id', 'value': '67890'},
                {'name': 'Subject', 'value': 'Hello'},
            ],
        )
        body, headers = _sns_wrap(ses_event)
        result = self.parser.parse(body, headers)

        assert result[0].metadata == {'order-id': '12345', 'user-id': '67890'}


# ────────────────────────────── SES Parse: raw_event ──────────────────────────────


class TestSESParseRawEvent:
    def setup_method(self):
        self.parser = SESWebhookParser()

    def test_raw_event_preserved(self):
        ses_event = _make_ses_event(
            'Send',
            'send',
            {'timestamp': '2024-01-15T10:00:00.000Z'},
        )
        body, headers = _sns_wrap(ses_event)
        result = self.parser.parse(body, headers)

        assert result[0].raw_event == ses_event


# ────────────────────────────── Webhook Handler ──────────────────────────────


class _FakeHTTPError(Exception):
    """Stand-in for fastapi.HTTPException when fastapi is not installed."""
    def __init__(self, *, status_code: int, detail: str = ''):
        self.status_code = status_code
        self.detail = detail
        super().__init__(detail)


def _patch_fastapi():
    """Create a mock fastapi module with HTTPException for testing."""
    import types
    fake_fastapi = types.ModuleType('fastapi')
    fake_fastapi.HTTPException = _FakeHTTPError  # type: ignore[attr-defined]
    return patch.dict('sys.modules', {'fastapi': fake_fastapi})


class TestWebhookHandler:
    def setup_method(self):
        WebhookRegistry.reset()

    @pytest.mark.asyncio
    async def test_handler_processes_events(self):
        with _patch_fastapi():
            from amsdal_mail.webhooks.handler import SupportedESP
            from amsdal_mail.webhooks.handler import webhook_handler

            parser = SESWebhookParser()
            WebhookRegistry.register('ses', parser)

            ses_event = _make_ses_event(
                'Delivery',
                'delivery',
                {'timestamp': '2024-01-15T10:05:00.000Z', 'recipients': ['user@example.com']},
            )
            body, headers = _sns_wrap(ses_event)

            mock_request = AsyncMock()
            mock_request.body.return_value = body
            mock_request.headers = headers

            with patch('amsdal_mail.webhooks.handler.EventBus') as mock_bus:
                mock_bus.aemit = AsyncMock()
                result = await webhook_handler(SupportedESP.ses, mock_request)

            assert result == {'status': 'ok', 'events_processed': 1}
            mock_bus.aemit.assert_called_once()
            call_args = mock_bus.aemit.call_args
            assert call_args[0][0] is EmailTrackingEvent
            assert isinstance(call_args[0][1], EmailTrackingContext)

    @pytest.mark.asyncio
    async def test_handler_no_parser_returns_404(self):
        with _patch_fastapi():
            from amsdal_mail.webhooks.handler import SupportedESP
            from amsdal_mail.webhooks.handler import webhook_handler

            mock_request = AsyncMock()

            with pytest.raises(_FakeHTTPError) as exc_info:
                await webhook_handler(SupportedESP.ses, mock_request)

            assert exc_info.value.status_code == 404

    @pytest.mark.asyncio
    async def test_handler_verify_fails_returns_403(self):
        with _patch_fastapi():
            from amsdal_mail.webhooks.handler import SupportedESP
            from amsdal_mail.webhooks.handler import webhook_handler

            parser = SESWebhookParser()
            WebhookRegistry.register('ses', parser)

            mock_request = AsyncMock()
            mock_request.body.return_value = b'{}'
            mock_request.headers = {}  # Missing required SNS headers

            with pytest.raises(_FakeHTTPError) as exc_info:
                await webhook_handler(SupportedESP.ses, mock_request)

            assert exc_info.value.status_code == 403

    @pytest.mark.asyncio
    async def test_handler_emits_multiple_events(self):
        with _patch_fastapi():
            from amsdal_mail.webhooks.handler import SupportedESP
            from amsdal_mail.webhooks.handler import webhook_handler

            parser = SESWebhookParser()
            WebhookRegistry.register('ses', parser)

            ses_event = _make_ses_event(
                'Bounce',
                'bounce',
                {
                    'bounceType': 'Permanent',
                    'bounceSubType': 'General',
                    'timestamp': '2024-01-15T10:05:00.000Z',
                    'bouncedRecipients': [
                        {'emailAddress': 'a@example.com'},
                        {'emailAddress': 'b@example.com'},
                    ],
                },
            )
            body, headers = _sns_wrap(ses_event)

            mock_request = AsyncMock()
            mock_request.body.return_value = body
            mock_request.headers = headers

            with patch('amsdal_mail.webhooks.handler.EventBus') as mock_bus:
                mock_bus.aemit = AsyncMock()
                result = await webhook_handler(SupportedESP.ses, mock_request)

            assert result == {'status': 'ok', 'events_processed': 2}
            assert mock_bus.aemit.call_count == 2


# ────────────────────────────── TrackingEventType Enum ──────────────────────────────


class TestTrackingEventType:
    def test_all_values(self):
        expected = {
            'queued', 'sent', 'delivered', 'deferred', 'bounced',
            'rejected', 'failed', 'opened', 'clicked', 'complained',
            'unsubscribed', 'unknown',
        }
        assert {e.value for e in TrackingEventType} == expected

    def test_is_string_enum(self):
        assert TrackingEventType.BOUNCED == 'bounced'
        assert isinstance(TrackingEventType.BOUNCED, str)


# ────────────────────────────── EmailTrackingEvent ──────────────────────────────


class TestEmailTrackingEvent:
    def test_context_type(self):
        assert EmailTrackingEvent.context_type is EmailTrackingContext  # type: ignore[misc]

    def test_default_error_strategy(self):
        from amsdal_utils.events import ErrorStrategy

        assert EmailTrackingEvent.default_error_strategy == ErrorStrategy.LOG_AND_CONTINUE
