"""Signed webhook delivery regressions without external Stripe or database writes."""
import hashlib
import hmac
import json
import time
from collections import UserDict
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from sqlalchemy.dialects.postgresql import JSONB, dialect
from src.domain.billing.repository import StripeEventRepository
from src.domain.billing.router import get_billing_service, router
from src.domain.billing.service import BillingService
from src.integrations.stripe_client import StripeClientImpl


def make_service(event_repo):
    return BillingService(
        customer_repo=AsyncMock(), subscription_repo=AsyncMock(),
        invoice_repo=AsyncMock(), event_repo=event_repo,
        usage_repo=AsyncMock(), purchase_repo=AsyncMock(),
        ledger_repo=AsyncMock(), catalog=AsyncMock(),
    )


def test_verified_sdk_data_is_converted_to_plain_json():
    payload = {
        "id": "evt_test", "type": "charge.succeeded",
        "data": {"object": {"id": "ch_test", "metadata": {}, "items": [None, {"x": 1}]}},
    }
    # Model SDK releases whose Data object is a mapping, not a dict subclass.
    sdk_event = SimpleNamespace(
        id=payload["id"], type=payload["type"], data=UserDict(payload["data"]),
    )
    client = StripeClientImpl(api_key="sk_test_local", webhook_secret="whsec_test")
    body = json.dumps(payload).encode()
    with patch(
        "src.integrations.stripe_client.stripe.Webhook.construct_event", return_value=sdk_event,
    ) as verify:
        result = client.verify_webhook_signature(body, "test_signature")
    verify.assert_called_once_with(body, "test_signature", "whsec_test")
    assert json.loads(json.dumps(result)) == payload
    assert type(result["data"]) is dict


def test_invalid_signature_never_returns_payload():
    from stripe import SignatureVerificationError

    client = StripeClientImpl(api_key="sk_test_local", webhook_secret="whsec_test")
    with pytest.raises(SignatureVerificationError):
        client.verify_webhook_signature(
            b'{"id":"evt_test","type":"charge.succeeded","data":{}}',
            f"t={int(time.time())},v1=invalid",
        )


@pytest.mark.parametrize("event_type,status", [
    ("payment_intent.payment_failed", "requires_payment_method"),
    ("payment_intent.succeeded", "succeeded"),
    ("charge.succeeded", "succeeded"),
])
def test_signed_clover_delivery_and_duplicate(event_type, status):
    secret = "whsec_test_delivery"
    obj = {
        "id": "ch_test" if event_type.startswith("charge.") else "pi_test",
        "object": "charge" if event_type.startswith("charge.") else "payment_intent",
        "amount": 2000, "currency": "usd", "status": status,
        "metadata": {}, "payment_method": None,
        "last_payment_error": {"code": "payment_intent_authentication_failure"}
        if status == "requires_payment_method" else None,
        "payment_details": {"order_reference": "in_test"},
        "payment_method_types": ["card"],
    }
    event = {
        "id": "evt_test_delivery", "object": "event",
        "api_version": "2026-02-25.clover", "type": event_type,
        "data": {"object": obj},
    }
    body = json.dumps(event).encode()
    timestamp = str(int(time.time()))
    signature = hmac.new(
        secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256,
    ).hexdigest()

    db = AsyncMock()
    db.add = MagicMock()
    db.execute.return_value.scalar_one_or_none = MagicMock(side_effect=[None, "stored"])

    async def serialize_payload():
        # Exercise the real repository and PostgreSQL's JSONB serializer.
        stored = db.add.call_args.args[0]
        serializer = JSONB().bind_processor(dialect())
        assert json.loads(serializer(stored.payload)) == event["data"]

    db.flush.side_effect = serialize_payload
    service = make_service(StripeEventRepository(db))
    app = FastAPI()
    app.include_router(router)
    app.dependency_overrides[get_billing_service] = lambda: service
    stripe_client = StripeClientImpl(api_key="sk_test_local", webhook_secret=secret)
    settings = SimpleNamespace(stripe_webhook_secret=secret, stripe_webhook_allow_unsigned=False)
    with (
        patch("src.domain.billing.router.get_settings", return_value=settings),
        patch("src.domain.billing.router._get_stripe_client", return_value=stripe_client),
        TestClient(app) as client,
    ):
        for expected in [
            {"received": True, "processed": True, "event_type": event_type},
            {"received": True, "processed": False, "reason": "duplicate"},
        ]:
            response = client.post(
                "/api/billing/stripe/webhook", content=body,
                headers={"Stripe-Signature": f"t={timestamp},v1={signature}"},
            )
            assert response.status_code == 200, response.text
            assert response.json() == expected
    db.flush.assert_awaited_once()
    service._invoice_repo.upsert_from_stripe.assert_not_awaited()
    service._ledger_repo.add_entry.assert_not_awaited()


@pytest.mark.parametrize("stage", ["deduplicate", "record", "dispatch"])
async def test_webhook_failure_logs_event_and_stage_and_still_raises(stage):
    repo = AsyncMock()
    repo.already_processed.return_value = False
    handler = AsyncMock()
    failing_call = {
        "deduplicate": repo.already_processed,
        "record": repo.record,
        "dispatch": handler,
    }[stage]
    error = RuntimeError("simulated failure")
    failing_call.side_effect = error
    with (
        patch("src.domain.billing.service.logger") as logger,
        patch.dict("src.domain.billing.service.STRIPE_EVENT_HANDLERS", {"invoice.paid": handler}),
        pytest.raises(RuntimeError, match="simulated failure"),
    ):
        await make_service(repo).handle_webhook_event(
            "evt_test_failure", "invoice.paid", {"object": {"id": "in_test"}},
        )
    logger.error.assert_called_once_with(
        "stripe_webhook_error", stripe_event_id="evt_test_failure",
        event_type="invoice.paid", object_id="in_test", stage=stage,
        error=str(error), exc_info=True,
    )
