from datetime import timedelta

import pytest
from sqlalchemy import select

from orders_api.db import utcnow
from orders_api.models import OutboxEvent
from orders_api.outbox import relay_once

pytestmark = pytest.mark.anyio


async def relay(state, now=utcnow):
    return await relay_once(state["sessions"], state["http"], state["settings"], now=now)


async def events(state) -> list[OutboxEvent]:
    async with state["sessions"]() as session:
        return list(await session.scalars(select(OutboxEvent).order_by(OutboxEvent.id)))


async def test_changes_are_delivered_as_signed_webhooks(create_order, client, asha, admin, state, receiver):
    order = (await create_order(asha)).json()
    await client.post(f"/v1/orders/{order['id']}/status", json={"status": "paid"},
                      headers=admin | {"If-Match": f'"{order["id"]}.1"'})
    assert await relay(state) == {"delivered": 2, "retry": 0, "dead": 0}
    assert [(wid, body["type"], body["status"]) for wid, body in receiver.received] == [
        ("evt_1", "order.created", "pending"), ("evt_2", "order.status_changed", "paid")]
    assert receiver.bad_signatures == 0
    assert await relay(state) == {"delivered": 0, "retry": 0, "dead": 0}      # nothing is sent twice


async def test_failed_deliveries_back_off_and_retry(create_order, asha, state, receiver):
    await create_order(asha)
    receiver.failures_left = 1
    assert await relay(state) == {"delivered": 0, "retry": 1, "dead": 0}
    [event] = await events(state)
    assert (event.attempts, event.last_error) == (1, "HTTP 503")
    assert await relay(state) == {"delivered": 0, "retry": 0, "dead": 0}      # not due yet (backoff)
    later = lambda: utcnow() + timedelta(seconds=3)
    assert await relay(state, now=later) == {"delivered": 1, "retry": 0, "dead": 0}


async def test_events_die_after_max_attempts(create_order, asha, state, receiver):
    await create_order(asha)
    receiver.failures_left = 100
    clock = utcnow()
    for attempt in range(state["settings"].outbox_max_attempts):
        clock += timedelta(minutes=10)
        await relay(state, now=lambda: clock)
    [event] = await events(state)
    assert event.status == "dead" and event.attempts == state["settings"].outbox_max_attempts


async def test_rejected_idempotent_retry_writes_no_event(create_order, asha, state):
    await create_order(asha, key="k")
    await create_order(asha, key="k")
    assert len(await events(state)) == 1
