"""HTTP webhook authentication tests with HMAC-SHA256 signatures."""

import hashlib
import hmac
import json

import httpx
import pytest

from helpers import HTTP_WEBHOOK_SECRET


def compute_signature(secret: str, body: bytes) -> str:
    """Compute X-Hub-Signature-256 HMAC-SHA256 signature."""
    mac = hmac.new(secret.encode(), body, hashlib.sha256)
    return f"sha256={mac.hexdigest()}"


async def _post_webhook(
    base_url: str,
    body_data: dict,
    *,
    signature: str | None = None,
    content_type: str = "application/json",
) -> httpx.Response:
    """Send a raw webhook request with optional signature."""
    body_bytes = json.dumps(body_data).encode()
    headers = {"Content-Type": content_type}
    if signature is not None:
        headers["X-Hub-Signature-256"] = signature

    async with httpx.AsyncClient() as client:
        return await client.post(
            f"{base_url}/webhook",
            content=body_bytes,
            headers=headers,
        )


@pytest.mark.asyncio
async def test_webhook_requires_http_webhook_secret_configured(
    http_channel_server_without_secret,
):
    """Webhook fails closed when no secret is configured."""
    response = await _post_webhook(
        http_channel_server_without_secret,
        {"content": "test message"},
    )

    assert response.status_code == 503
    data = response.json()
    assert data["status"] == "error"
    assert "Webhook authentication not configured" in data.get("response", "")


@pytest.mark.asyncio
async def test_webhook_hmac_signature_valid(http_channel_server):
    """Valid X-Hub-Signature-256 HMAC signature is accepted."""
    body = {"content": "hello from webhook"}
    signature = compute_signature(HTTP_WEBHOOK_SECRET, json.dumps(body).encode())

    response = await _post_webhook(http_channel_server, body, signature=signature)

    assert response.status_code == 200, (
        f"Expected 200, got {response.status_code}: {response.text}"
    )
    data = response.json()
    assert data["status"] == "accepted"


@pytest.mark.asyncio
async def test_webhook_invalid_hmac_signature_rejected(http_channel_server):
    """Invalid X-Hub-Signature-256 signature is rejected with 401."""
    response = await _post_webhook(
        http_channel_server,
        {"content": "hello"},
        signature="sha256=0000000000000000000000000000000000000000000000000000000000000000",
    )

    assert response.status_code == 401
    data = response.json()
    assert data["status"] == "error"
    assert "Invalid webhook signature" in data.get("response", "")


@pytest.mark.asyncio
async def test_webhook_wrong_secret_rejected(http_channel_server):
    """Signature computed with wrong secret is rejected."""
    body = {"content": "hello"}
    signature = compute_signature("wrong-secret", json.dumps(body).encode())

    response = await _post_webhook(http_channel_server, body, signature=signature)

    assert response.status_code == 401
    assert response.json()["status"] == "error"


@pytest.mark.asyncio
async def test_webhook_missing_signature_header_rejected(http_channel_server):
    """Missing X-Hub-Signature-256 header is rejected when no body secret is provided."""
    response = await _post_webhook(http_channel_server, {"content": "hello"})

    assert response.status_code == 401
    data = response.json()
    assert "Webhook authentication required" in data.get("response", "")
    assert "X-Hub-Signature-256" in data.get("response", "")


@pytest.mark.asyncio
async def test_webhook_deprecated_body_secret_still_works(http_channel_server):
    """Deprecated body secret support still accepts old clients."""
    response = await _post_webhook(
        http_channel_server,
        {"content": "hello", "secret": HTTP_WEBHOOK_SECRET},
    )

    assert response.status_code == 200, (
        f"Expected 200, got {response.status_code}: {response.text}"
    )
    assert response.json()["status"] == "accepted"


@pytest.mark.asyncio
async def test_webhook_header_takes_precedence_over_body_secret(http_channel_server):
    """Header signature wins when both header and body secret are provided."""
    body = {"content": "hello", "secret": "wrong-secret-in-body"}
    signature = compute_signature(HTTP_WEBHOOK_SECRET, json.dumps(body).encode())

    response = await _post_webhook(http_channel_server, body, signature=signature)

    assert response.status_code == 200
    assert response.json()["status"] == "accepted"


@pytest.mark.asyncio
async def test_webhook_case_insensitive_header_lookup(http_channel_server):
    """HTTP headers are treated case-insensitively."""
    body = {"content": "hello"}
    body_bytes = json.dumps(body).encode()
    signature = compute_signature(HTTP_WEBHOOK_SECRET, body_bytes)

    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{http_channel_server}/webhook",
            content=body_bytes,
            headers={
                "Content-Type": "application/json",
                "x-hub-signature-256": signature,
            },
        )

    assert response.status_code == 200


@pytest.mark.asyncio
async def test_webhook_wrong_content_type_rejected(http_channel_server):
    """Webhook only accepts application/json Content-Type."""
    body = {"content": "hello"}
    signature = compute_signature(HTTP_WEBHOOK_SECRET, json.dumps(body).encode())

    response = await _post_webhook(
        http_channel_server,
        body,
        signature=signature,
        content_type="text/plain",
    )

    assert response.status_code == 415
    assert "application/json" in response.json().get("response", "")


@pytest.mark.asyncio
async def test_webhook_invalid_json_rejected(http_channel_server):
    """Invalid JSON in body is rejected."""
    body_bytes = b"not valid json"
    signature = compute_signature(HTTP_WEBHOOK_SECRET, body_bytes)

    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{http_channel_server}/webhook",
            content=body_bytes,
            headers={
                "Content-Type": "application/json",
                "X-Hub-Signature-256": signature,
            },
        )

    assert response.status_code in (400, 401)


@pytest.mark.asyncio
async def test_webhook_message_queued_for_processing(http_channel_server):
    """Accepted webhook requests return a real message id."""
    body = {"content": "webhook test message 12345"}
    signature = compute_signature(HTTP_WEBHOOK_SECRET, json.dumps(body).encode())

    response = await _post_webhook(http_channel_server, body, signature=signature)

    assert response.status_code == 200
    data = response.json()
    assert data["status"] == "accepted"
    assert "message_id" in data
    assert data["message_id"] != "00000000-0000-0000-0000-000000000000"
