"""Unit tests for decnet.webhook.client — HMAC + retry policy."""
from __future__ import annotations

import hashlib
import hmac

import httpx
import pytest

from decnet.webhook.client import (
    DeliveryResult,
    SyntheticEvent,
    build_payload,
    deliver,
    sign,
)


_EVENT = SyntheticEvent(
    topic="attacker.observed",
    type="first_sighting",
    ts="2026-04-24T00:00:00+00:00",
    id="11111111-1111-1111-1111-111111111111",
    payload={"ip": "1.2.3.4"},
)


def _sub(url: str = "https://webhook.example/inbound", secret: str = "s" * 32) -> dict:
    return {"uuid": "w1", "url": url, "secret": secret}


def test_sign_matches_known_vector():
    body = b'{"hello":"world"}'
    secret = "0123456789abcdef"
    expected = (
        "sha256="
        + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    )
    assert sign(secret, body) == expected


def test_build_payload_stable_key_order():
    # Same input → same bytes → same HMAC, regardless of kwarg order.
    b1 = build_payload(_EVENT)
    b2 = build_payload(_EVENT)
    assert b1 == b2
    assert b'"topic":"attacker.observed"' in b1
    assert b'"v":1' in b1


@pytest.mark.asyncio
async def test_deliver_success_on_2xx():
    async def handler(request: httpx.Request) -> httpx.Response:
        assert request.headers.get("X-DECNET-Signature", "").startswith("sha256=")
        assert request.headers.get("X-DECNET-Event-Id") == _EVENT.id
        return httpx.Response(200, json={"ok": True})

    transport = httpx.MockTransport(handler)
    async with httpx.AsyncClient(transport=transport) as client:
        result = await deliver(_sub(), _EVENT, retry_schedule=[], client=client)
    assert result == DeliveryResult(ok=True, status_code=200, attempts=1)


@pytest.mark.asyncio
async def test_deliver_no_retry_on_4xx():
    calls = {"n": 0}

    async def handler(request: httpx.Request) -> httpx.Response:
        calls["n"] += 1
        return httpx.Response(400, text="bad body")

    transport = httpx.MockTransport(handler)
    async with httpx.AsyncClient(transport=transport) as client:
        result = await deliver(_sub(), _EVENT, retry_schedule=[1, 1, 1], client=client)
    assert result.ok is False
    assert result.status_code == 400
    assert calls["n"] == 1  # no retry


@pytest.mark.asyncio
async def test_deliver_retries_on_429():
    calls = {"n": 0}

    async def handler(request: httpx.Request) -> httpx.Response:
        calls["n"] += 1
        if calls["n"] < 3:
            return httpx.Response(429)
        return httpx.Response(200)

    transport = httpx.MockTransport(handler)
    async with httpx.AsyncClient(transport=transport) as client:
        result = await deliver(_sub(), _EVENT, retry_schedule=[0, 0], client=client)
    assert result.ok is True
    assert result.attempts == 3


@pytest.mark.asyncio
async def test_deliver_retries_on_5xx_then_gives_up():
    async def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(503)

    transport = httpx.MockTransport(handler)
    async with httpx.AsyncClient(transport=transport) as client:
        result = await deliver(_sub(), _EVENT, retry_schedule=[0, 0], client=client)
    assert result.ok is False
    assert result.status_code == 503
    assert result.attempts == 3


@pytest.mark.asyncio
async def test_deliver_retries_on_connection_error():
    async def handler(request: httpx.Request) -> httpx.Response:
        raise httpx.ConnectError("boom")

    transport = httpx.MockTransport(handler)
    async with httpx.AsyncClient(transport=transport) as client:
        result = await deliver(_sub(), _EVENT, retry_schedule=[0], client=client)
    assert result.ok is False
    assert result.status_code is None
    assert "ConnectError" in (result.error or "")
    assert result.attempts == 2


@pytest.mark.asyncio
async def test_deliver_receiver_can_verify_signature():
    """End-to-end: receiver recomputes HMAC over the posted body and matches ours."""
    sub = _sub(secret="deadbeefdeadbeef")
    captured: dict = {}

    async def handler(request: httpx.Request) -> httpx.Response:
        captured["body"] = request.content
        captured["sig"] = request.headers["X-DECNET-Signature"]
        return httpx.Response(200)

    transport = httpx.MockTransport(handler)
    async with httpx.AsyncClient(transport=transport) as client:
        result = await deliver(sub, _EVENT, retry_schedule=[], client=client)
    assert result.ok
    expected = (
        "sha256="
        + hmac.new(
            sub["secret"].encode(), captured["body"], hashlib.sha256
        ).hexdigest()
    )
    assert captured["sig"] == expected
