"""Tests for Phase 11.D — event outbox + async dispatch."""

from __future__ import annotations

from datetime import timedelta

import pytest
from django.test import override_settings
from django.utils import timezone

from simorgh.apps.events.bus import (
    EventError,
    clear_subscribers,
    dispatch_async,
    register_event,
    subscribe,
)
from simorgh.apps.events.models import EventOutboxEntry, OutboxStatus
from simorgh.apps.events.tasks import flush_outbox

EVENT_NAME = "test.outbox_demo"


@pytest.fixture(autouse=True)
def _register_test_event():
    register_event(EVENT_NAME, description="demo", payload_keys=("v",))
    yield
    clear_subscribers(EVENT_NAME)


@pytest.mark.django_db
def test_dispatch_async_sync_mode_runs_inline():
    """With EVENTS_ASYNC_DISPATCH=False, dispatch_async is just dispatch."""
    received = []

    @subscribe(EVENT_NAME)
    def _h(payload):
        received.append(payload)

    dispatch_async(EVENT_NAME, {"v": 1})
    assert received == [{"v": 1}]
    # No outbox row written in sync mode.
    assert EventOutboxEntry.objects.count() == 0


@pytest.mark.django_db
@override_settings(EVENTS_ASYNC_DISPATCH=True)
def test_dispatch_async_writes_outbox_row():
    dispatch_async(EVENT_NAME, {"v": 7}, tenant_id=42)
    entry = EventOutboxEntry.objects.get()
    # transaction.on_commit fired flush_outbox in CELERY_TASK_ALWAYS_EAGER mode,
    # which marks it dispatched. We assert the **persisted** trail rather than
    # status-pending (eager workers flip it in-process).
    assert entry.name == EVENT_NAME
    assert entry.payload == {"v": 7}
    assert entry.tenant_id_hint == 42


@pytest.mark.django_db
def test_dispatch_async_validates_payload():
    with pytest.raises(EventError):
        dispatch_async(EVENT_NAME, {})  # missing 'v'


@pytest.mark.django_db
def test_flush_outbox_dispatches_pending_entries():
    received = []

    @subscribe(EVENT_NAME)
    def _h(payload):
        received.append(payload["v"])

    entry = EventOutboxEntry.objects.create(
        name=EVENT_NAME,
        payload={"v": "hello"},
        status=OutboxStatus.PENDING,
        next_retry_at=timezone.now() - timedelta(seconds=1),
    )
    stats = flush_outbox()
    assert stats == {"dispatched": 1, "failed": 0, "dead": 0}
    entry.refresh_from_db()
    assert entry.status == OutboxStatus.DISPATCHED
    assert entry.dispatched_at is not None
    assert received == ["hello"]


@pytest.mark.django_db
def test_flush_outbox_retries_on_handler_failure():
    @subscribe(EVENT_NAME)
    def _explode(payload):
        raise RuntimeError("nope")

    entry = EventOutboxEntry.objects.create(
        name=EVENT_NAME,
        payload={"v": 1},
        status=OutboxStatus.PENDING,
        next_retry_at=timezone.now() - timedelta(seconds=1),
        max_attempts=2,
    )
    # First attempt — handler raises, but bus._dispatch_sync swallows
    # exceptions per-handler. So outbox sees success. To exercise failure
    # path we need an event the bus rejects (unknown name).
    entry.name = "test.does_not_exist"
    entry.save(update_fields=["name", "updated_at"])
    stats = flush_outbox()
    assert stats == {"dispatched": 0, "failed": 1, "dead": 0}
    entry.refresh_from_db()
    assert entry.status == OutboxStatus.FAILED
    assert entry.attempt == 1
    assert "unknown event" in entry.last_error.lower()

    # Set next_retry_at into the past so it'll be picked again.
    entry.next_retry_at = timezone.now() - timedelta(seconds=1)
    entry.save(update_fields=["next_retry_at", "updated_at"])
    stats = flush_outbox()
    assert stats == {"dispatched": 0, "failed": 0, "dead": 1}
    entry.refresh_from_db()
    assert entry.status == OutboxStatus.DEAD
    assert entry.attempt == 2


@pytest.mark.django_db
def test_flush_outbox_respects_next_retry_at():
    EventOutboxEntry.objects.create(
        name=EVENT_NAME,
        payload={"v": 1},
        status=OutboxStatus.PENDING,
        next_retry_at=timezone.now() + timedelta(hours=1),
    )
    stats = flush_outbox()
    assert stats == {"dispatched": 0, "failed": 0, "dead": 0}
