"""Tests for GrowFlowBillingClient."""

import hashlib
import hmac
import json

import pytest
import respx
from httpx import Response

from growflow_billing import GrowFlowBillingClient
from growflow_billing.exceptions import (
    AuthenticationError,
    ConflictError,
    GrowFlowBillingError,
    NotFoundError,
    RateLimitError,
    ServerError,
    ValidationError,
)
from growflow_billing.models.invoice import Invoice, InvoiceStatus
from growflow_billing.models.plan import Plan, PlanLimits
from growflow_billing.models.subscriber import BillingAddress, Subscriber
from growflow_billing.models.subscription import BillingCycle, Subscription, SubscriptionStatus
from growflow_billing.models.checkout import CheckoutSession, PortalSession
from growflow_billing.models.product import Price, Product, ProductCheckoutSession
from growflow_billing.webhooks import (
    WebhookEvent,
    WebhookParseError,
    WebhookSignatureError,
    construct_event,
    parse_webhook_event,
    verify_webhook_signature,
    VALID_EVENT_TYPES,
)


# =============================================================================
# Client basics
# =============================================================================


@pytest.mark.asyncio
async def test_client_context_manager(api_key: str):
    """Test client as context manager."""
    async with GrowFlowBillingClient(api_key=api_key) as client:
        assert client._client is not None
    assert client._client is None


@pytest.mark.asyncio
async def test_client_without_context_manager(api_key: str):
    """Test client without context manager."""
    client = GrowFlowBillingClient(api_key=api_key)
    assert client._client is None
    # _get_client creates it lazily
    http_client = client._get_client()
    assert http_client is not None
    await client.close()
    assert client._client is None


@pytest.mark.asyncio
async def test_client_config(api_key: str):
    """Test client configuration."""
    client = GrowFlowBillingClient(
        api_key=api_key,
        base_url="https://custom.api.com/v1",
        timeout=60.0,
        max_retries=5,
        retry_delay=2.0,
    )
    assert client.config.api_key == api_key
    assert client.config.base_url == "https://custom.api.com/v1"
    assert client.config.timeout == 60.0
    assert client.config.max_retries == 5
    assert client.config.retry_delay == 2.0
    await client.close()


# =============================================================================
# Plans
# =============================================================================


@pytest.mark.asyncio
async def test_get_plans(client: GrowFlowBillingClient, mock_api):
    """Test getting plans."""
    mock_api.get("/plans").mock(
        return_value=Response(
            200,
            json=[
                {
                    "slug": "base",
                    "name": "Base",
                    "price_monthly": "149.00",
                    "currency": "EUR",
                    "limits": {"conversations": 1000},
                    "features": ["basic_support"],
                    "trial_days": 14,
                },
                {
                    "slug": "pro",
                    "name": "Pro",
                    "price_monthly": "349.00",
                    "currency": "EUR",
                    "limits": {"conversations": 5000},
                    "features": ["premium_support", "api_access"],
                    "trial_days": 14,
                },
            ],
        )
    )

    plans = await client.get_plans()
    assert len(plans) == 2
    assert plans[0].slug == "base"
    assert plans[1].slug == "pro"


@pytest.mark.asyncio
async def test_get_plan_by_slug(client: GrowFlowBillingClient, mock_api):
    """Test getting a specific plan by slug."""
    mock_api.get("/plans").mock(
        return_value=Response(
            200,
            json=[
                {"slug": "base", "name": "Base", "price_monthly": "149.00"},
                {"slug": "pro", "name": "Pro", "price_monthly": "349.00"},
            ],
        )
    )

    plan = await client.get_plan("pro")
    assert plan is not None
    assert plan.slug == "pro"
    assert plan.name == "Pro"


@pytest.mark.asyncio
async def test_get_plan_not_found(client: GrowFlowBillingClient, mock_api):
    """Test getting a non-existent plan returns None."""
    mock_api.get("/plans").mock(
        return_value=Response(200, json=[{"slug": "base", "name": "Base"}])
    )

    plan = await client.get_plan("nonexistent")
    assert plan is None


def test_plan_properties():
    """Test Plan model properties."""
    free_plan = Plan(slug="free", name="Free", price_monthly=None, price_yearly=None)
    assert free_plan.is_free is True
    assert free_plan.is_enterprise is False

    pro_plan = Plan(slug="pro", name="Pro", price_monthly="29.00", price_yearly="290.00")
    assert pro_plan.is_free is False
    assert pro_plan.get_price("monthly") is not None
    assert pro_plan.get_price("yearly") is not None

    enterprise_plan = Plan(slug="enterprise", name="Enterprise", contact_sales=True)
    assert enterprise_plan.is_enterprise is True

    # Dict-like access
    assert pro_plan["slug"] == "pro"
    assert pro_plan.get("name") == "Pro"
    assert pro_plan.get("missing", "default") == "default"

    with pytest.raises(KeyError):
        _ = pro_plan["missing"]


def test_plan_limits():
    """Test PlanLimits model."""
    plan = Plan(slug="pro", name="Pro", limits={"conversations": 5000, "stores": 10})
    limits = plan.plan_limits
    assert limits.conversations == 5000
    assert limits.stores == 10


# =============================================================================
# Subscribers
# =============================================================================


@pytest.mark.asyncio
async def test_create_subscriber(client: GrowFlowBillingClient, mock_api):
    """Test creating a subscriber."""
    mock_api.post("/subscribers").mock(
        return_value=Response(
            201,
            json={
                "id": "123e4567-e89b-12d3-a456-426614174000",
                "external_id": "store_123",
                "email": "test@example.com",
                "name": "Test Store",
                "is_active": True,
            },
        )
    )

    subscriber = await client.create_subscriber(
        external_id="store_123",
        email="test@example.com",
        name="Test Store",
    )
    assert subscriber.external_id == "store_123"
    assert subscriber.email == "test@example.com"


@pytest.mark.asyncio
async def test_create_subscriber_conflict(client: GrowFlowBillingClient, mock_api):
    """Test creating subscriber that already exists returns existing."""
    mock_api.post("/subscribers").mock(
        return_value=Response(409, json={"detail": "Already exists"})
    )
    mock_api.get("/subscribers/store_existing").mock(
        return_value=Response(
            200,
            json={
                "id": "123e4567-e89b-12d3-a456-426614174000",
                "external_id": "store_existing",
                "email": "existing@example.com",
                "name": "Existing Store",
                "is_active": True,
            },
        )
    )

    subscriber = await client.create_subscriber(
        external_id="store_existing",
        email="existing@example.com",
    )
    assert subscriber.external_id == "store_existing"


@pytest.mark.asyncio
async def test_get_subscriber(client: GrowFlowBillingClient, mock_api):
    """Test getting subscriber by external ID."""
    mock_api.get("/subscribers/store_123").mock(
        return_value=Response(
            200,
            json={
                "id": "123e4567-e89b-12d3-a456-426614174000",
                "external_id": "store_123",
                "email": "test@example.com",
                "name": "Test Store",
                "company_name": "Test Corp",
                "vat_number": "IT12345678901",
                "billing_address": {
                    "line1": "123 Main St",
                    "city": "Rome",
                    "postal_code": "00100",
                    "country": "IT",
                },
                "stripe_customer_id": "cus_test",
                "custom_data": {"role": "admin"},
                "is_active": True,
            },
        )
    )

    subscriber = await client.get_subscriber("store_123")
    assert subscriber.name == "Test Store"
    assert subscriber.company_name == "Test Corp"
    assert subscriber.billing_address is not None
    assert subscriber.billing_address.city == "Rome"
    assert subscriber.custom_data == {"role": "admin"}


@pytest.mark.asyncio
async def test_update_subscriber(client: GrowFlowBillingClient, mock_api):
    """Test updating subscriber."""
    mock_api.patch("/subscribers/store_123").mock(
        return_value=Response(
            200,
            json={
                "id": "123e4567-e89b-12d3-a456-426614174000",
                "external_id": "store_123",
                "email": "new@example.com",
                "name": "Updated Store",
                "is_active": True,
            },
        )
    )

    subscriber = await client.update_subscriber(
        external_id="store_123",
        email="new@example.com",
        name="Updated Store",
    )
    assert subscriber.email == "new@example.com"
    assert subscriber.name == "Updated Store"


# =============================================================================
# Subscriptions
# =============================================================================


@pytest.mark.asyncio
async def test_get_subscription(client: GrowFlowBillingClient, mock_api):
    """Test getting active subscription."""
    mock_api.get("/subscriptions").mock(
        return_value=Response(
            200,
            json=[
                {
                    "id": "123e4567-e89b-12d3-a456-426614174000",
                    "subscriber_external_id": "store_123",
                    "plan_slug": "pro",
                    "status": "active",
                    "billing_cycle": "monthly",
                    "current_period_end": "2025-02-15T00:00:00Z",
                }
            ],
        )
    )

    subscription = await client.get_subscription("store_123")
    assert subscription is not None
    assert subscription.plan_slug == "pro"
    assert subscription.status.value == "active"


@pytest.mark.asyncio
async def test_get_subscription_none(client: GrowFlowBillingClient, mock_api):
    """Test getting subscription when none exists."""
    mock_api.get("/subscriptions").mock(
        return_value=Response(200, json=[])
    )

    subscription = await client.get_subscription("no_sub_user")
    assert subscription is None


@pytest.mark.asyncio
async def test_list_subscriptions(client: GrowFlowBillingClient, mock_api):
    """Test listing subscriptions."""
    mock_api.get("/subscriptions").mock(
        return_value=Response(
            200,
            json=[
                {
                    "id": "a23e4567-e89b-12d3-a456-426614174001",
                    "plan_slug": "pro",
                    "status": "active",
                    "billing_cycle": "monthly",
                },
                {
                    "id": "a23e4567-e89b-12d3-a456-426614174002",
                    "plan_slug": "base",
                    "status": "canceled",
                    "billing_cycle": "monthly",
                },
            ],
        )
    )

    subs = await client.list_subscriptions("store_123")
    assert len(subs) == 2


@pytest.mark.asyncio
async def test_get_subscription_by_id(client: GrowFlowBillingClient, mock_api):
    """Test getting subscription by ID."""
    sub_uuid = "a23e4567-e89b-12d3-a456-426614174001"
    mock_api.get(f"/subscriptions/{sub_uuid}").mock(
        return_value=Response(
            200,
            json={
                "id": sub_uuid,
                "plan_slug": "pro",
                "status": "active",
                "billing_cycle": "monthly",
            },
        )
    )

    sub = await client.get_subscription_by_id(sub_uuid)
    assert sub.plan_slug == "pro"


@pytest.mark.asyncio
async def test_cancel_subscription(client: GrowFlowBillingClient, mock_api):
    """Test canceling a subscription."""
    sub_uuid = "a23e4567-e89b-12d3-a456-426614174001"
    mock_api.post(f"/subscriptions/{sub_uuid}/cancel").mock(
        return_value=Response(
            200,
            json={
                "id": sub_uuid,
                "plan_slug": "pro",
                "status": "canceled",
                "billing_cycle": "monthly",
                "canceled_at": "2024-01-15T00:00:00Z",
            },
        )
    )

    sub = await client.cancel_subscription(sub_uuid)
    assert sub.status == SubscriptionStatus.CANCELED


@pytest.mark.asyncio
async def test_change_plan(client: GrowFlowBillingClient, mock_api):
    """Test changing subscription plan."""
    sub_uuid = "a23e4567-e89b-12d3-a456-426614174001"
    mock_api.post(f"/subscriptions/{sub_uuid}/change-plan").mock(
        return_value=Response(
            200,
            json={
                "id": sub_uuid,
                "plan_slug": "enterprise",
                "status": "active",
                "billing_cycle": "yearly",
            },
        )
    )

    sub = await client.change_plan(sub_uuid, "enterprise")
    assert sub.plan_slug == "enterprise"


@pytest.mark.asyncio
async def test_resume_subscription(client: GrowFlowBillingClient, mock_api):
    """Test resuming a subscription."""
    sub_uuid = "a23e4567-e89b-12d3-a456-426614174001"
    mock_api.post(f"/subscriptions/{sub_uuid}/resume").mock(
        return_value=Response(
            200,
            json={
                "id": sub_uuid,
                "plan_slug": "pro",
                "status": "active",
                "billing_cycle": "monthly",
            },
        )
    )

    sub = await client.resume_subscription(sub_uuid)
    assert sub.status == SubscriptionStatus.ACTIVE


@pytest.mark.asyncio
async def test_reactivate_subscription(client: GrowFlowBillingClient, mock_api):
    """Test reactivating a canceled subscription."""
    sub_uuid = "a23e4567-e89b-12d3-a456-426614174001"
    mock_api.post(f"/subscriptions/{sub_uuid}/reactivate").mock(
        return_value=Response(
            200,
            json={
                "id": sub_uuid,
                "plan_slug": "pro",
                "status": "active",
                "billing_cycle": "monthly",
            },
        )
    )

    sub = await client.reactivate_subscription(sub_uuid)
    assert sub.status == SubscriptionStatus.ACTIVE


def test_subscription_properties():
    """Test Subscription model properties."""
    active_sub = Subscription(status=SubscriptionStatus.ACTIVE, billing_cycle=BillingCycle.MONTHLY)
    assert active_sub.is_active is True
    assert active_sub.is_trialing is False
    assert active_sub.will_cancel is False

    trialing_sub = Subscription(status=SubscriptionStatus.TRIALING, billing_cycle=BillingCycle.MONTHLY)
    assert trialing_sub.is_active is True
    assert trialing_sub.is_trialing is True

    canceled_sub = Subscription(
        status=SubscriptionStatus.CANCELED,
        billing_cycle=BillingCycle.MONTHLY,
        cancel_at="2024-02-01T00:00:00Z",
    )
    assert canceled_sub.is_active is False
    assert canceled_sub.will_cancel is True


# =============================================================================
# Checkout
# =============================================================================


@pytest.mark.asyncio
async def test_create_checkout_session(client: GrowFlowBillingClient, mock_api):
    """Test creating checkout session."""
    mock_api.post("/public/checkout").mock(
        return_value=Response(
            200,
            json={
                "checkout_url": "https://checkout.stripe.com/pay/cs_xxx",
                "session_id": "cs_xxx",
            },
        )
    )

    session = await client.create_checkout_session(
        customer_external_id="store_123",
        plan_slug="pro",
        billing_cycle="monthly",
        success_url="https://app.com/success",
        cancel_url="https://app.com/cancel",
    )
    assert session.checkout_url.startswith("https://checkout.stripe.com")
    assert session.session_id == "cs_xxx"


@pytest.mark.asyncio
async def test_create_checkout_session_with_options(client: GrowFlowBillingClient, mock_api):
    """Test creating checkout session with all options."""
    mock_api.post("/public/checkout").mock(
        return_value=Response(
            200,
            json={
                "checkout_url": "https://checkout.stripe.com/pay/cs_full",
                "session_id": "cs_full",
                "is_new_subscriber": True,
            },
        )
    )

    session = await client.create_checkout_session(
        customer_external_id="store_123",
        plan_slug="pro",
        billing_cycle="yearly",
        success_url="https://app.com/success",
        cancel_url="https://app.com/cancel",
        email="test@example.com",
        name="Test User",
        company_name="Test Corp",
        vat_number="IT123",
        coupon_code="SAVE20",
        custom_data={"ref": "promo"},
    )
    assert session.session_id == "cs_full"
    assert session.is_new_subscriber is True


@pytest.mark.asyncio
async def test_create_portal_session(client: GrowFlowBillingClient, mock_api):
    """Test creating portal session."""
    mock_api.post("/checkout/portal").mock(
        return_value=Response(
            200,
            json={
                "portal_url": "https://billing.stripe.com/p/session/bps_xxx",
                "session_id": "bps_xxx",
                "return_url": "https://app.com/billing",
            },
        )
    )

    portal = await client.create_portal_session(
        customer_external_id="store_123",
        return_url="https://app.com/billing",
    )
    assert portal.portal_url.startswith("https://billing.stripe.com")
    assert portal.session_id == "bps_xxx"


# =============================================================================
# Invoices
# =============================================================================


@pytest.mark.asyncio
async def test_get_invoices(client: GrowFlowBillingClient, mock_api):
    """Test getting invoices."""
    mock_api.get("/invoices").mock(
        return_value=Response(
            200,
            json=[
                {
                    "id": "in_xxx",
                    "number": "INV-0001",
                    "status": "paid",
                    "amount_due": 34900,
                    "amount_paid": 34900,
                    "currency": "eur",
                    "invoice_pdf": "https://stripe.com/invoice.pdf",
                }
            ],
        )
    )

    invoices = await client.get_invoices("store_123")
    assert len(invoices) == 1
    assert invoices[0].number == "INV-0001"
    assert invoices[0].is_paid
    assert invoices[0].amount_paid_decimal == 349.00


@pytest.mark.asyncio
async def test_get_invoice_by_id(client: GrowFlowBillingClient, mock_api):
    """Test getting invoice by ID."""
    mock_api.get("/invoices/in_001").mock(
        return_value=Response(
            200,
            json={
                "id": "in_001",
                "number": "INV-2024-001",
                "status": "paid",
                "amount_due": 2900,
                "amount_paid": 2900,
                "total": 2900,
                "currency": "eur",
            },
        )
    )

    invoice = await client.get_invoice("in_001")
    assert invoice.id == "in_001"
    assert invoice.number == "INV-2024-001"
    assert invoice.total_decimal == 29.00


@pytest.mark.asyncio
async def test_get_upcoming_invoice(client: GrowFlowBillingClient, mock_api):
    """Test getting upcoming invoice."""
    mock_api.get("/invoices/upcoming").mock(
        return_value=Response(
            200,
            json={
                "id": "in_upcoming",
                "status": "draft",
                "amount_due": 2900,
                "total": 2900,
                "currency": "eur",
            },
        )
    )

    invoice = await client.get_upcoming_invoice("store_123")
    assert invoice is not None
    assert invoice.id == "in_upcoming"
    assert invoice.status == "draft"


@pytest.mark.asyncio
async def test_get_upcoming_invoice_none(client: GrowFlowBillingClient, mock_api):
    """Test getting upcoming invoice when none exists."""
    mock_api.get("/invoices/upcoming").mock(
        return_value=Response(404, json={"detail": "Not found"})
    )

    invoice = await client.get_upcoming_invoice("no_sub_user")
    assert invoice is None


@pytest.mark.asyncio
async def test_get_invoice_pdf_url(client: GrowFlowBillingClient, mock_api):
    """Test getting invoice PDF URL."""
    mock_api.get("/invoices/in_001/pdf").mock(
        return_value=Response(
            200,
            json={"url": "https://invoice.stripe.com/i/inv_123/pdf"},
        )
    )

    url = await client.get_invoice_pdf_url("in_001")
    assert url == "https://invoice.stripe.com/i/inv_123/pdf"


@pytest.mark.asyncio
async def test_get_invoice_pdf_url_prefers_pdf_url(client: GrowFlowBillingClient, mock_api):
    """Test get_invoice_pdf_url prefers pdf_url over url."""
    mock_api.get("/invoices/in_001/pdf").mock(
        return_value=Response(
            200,
            json={
                "url": "https://fallback.com/pdf",
                "pdf_url": "https://preferred.com/pdf",
            },
        )
    )

    url = await client.get_invoice_pdf_url("in_001")
    assert url == "https://preferred.com/pdf"


@pytest.mark.asyncio
async def test_send_invoice(client: GrowFlowBillingClient, mock_api):
    """Test sending invoice."""
    mock_api.post("/invoices/in_001/send").mock(
        return_value=Response(200, json={"success": True})
    )

    result = await client.send_invoice("in_001")
    assert result is True


def test_invoice_properties():
    """Test Invoice model properties."""
    paid = Invoice(id="in_1", status="paid", amount_due=5000, amount_paid=5000, total=5000)
    assert paid.is_paid is True
    assert paid.is_open is False
    assert paid.amount_paid_decimal == 50.00
    assert paid.amount_due_decimal == 50.00
    assert paid.total_decimal == 50.00

    open_inv = Invoice(id="in_2", status="open", amount_due=3000, total=3000)
    assert open_inv.is_paid is False
    assert open_inv.is_open is True


# =============================================================================
# Products
# =============================================================================


@pytest.mark.asyncio
async def test_get_products(client: GrowFlowBillingClient, mock_api):
    """Test getting products."""
    mock_api.get("/products").mock(
        return_value=Response(
            200,
            json=[
                {
                    "id": "prod_001",
                    "name": "Extra Conversations",
                    "description": "100 additional conversations",
                    "active": True,
                    "prices": [
                        {
                            "id": "price_001",
                            "product_id": "prod_001",
                            "unit_amount": 999,
                            "currency": "eur",
                        }
                    ],
                    "default_price_id": "price_001",
                }
            ],
        )
    )

    products = await client.get_products()
    assert len(products) == 1
    assert products[0].name == "Extra Conversations"
    assert products[0].prices is not None
    assert len(products[0].prices) == 1


@pytest.mark.asyncio
async def test_get_product(client: GrowFlowBillingClient, mock_api):
    """Test getting a specific product."""
    mock_api.get("/products/prod_001").mock(
        return_value=Response(
            200,
            json={
                "id": "prod_001",
                "name": "Extra Conversations",
                "active": True,
                "prices": [
                    {
                        "id": "price_001",
                        "product_id": "prod_001",
                        "unit_amount": 999,
                    }
                ],
                "default_price_id": "price_001",
            },
        )
    )

    product = await client.get_product("prod_001")
    assert product.id == "prod_001"
    assert product.default_price is not None
    assert product.default_price.id == "price_001"


@pytest.mark.asyncio
async def test_get_product_prices(client: GrowFlowBillingClient, mock_api):
    """Test getting product prices."""
    mock_api.get("/products/prod_001/prices").mock(
        return_value=Response(
            200,
            json=[
                {
                    "id": "price_001",
                    "product_id": "prod_001",
                    "unit_amount": 999,
                    "currency": "eur",
                    "recurring": False,
                },
                {
                    "id": "price_002",
                    "product_id": "prod_001",
                    "unit_amount": 1999,
                    "currency": "eur",
                    "recurring": True,
                    "interval": "month",
                },
            ],
        )
    )

    prices = await client.get_product_prices("prod_001")
    assert len(prices) == 2
    assert prices[0].id == "price_001"
    assert prices[0].amount_decimal == 9.99
    assert prices[1].recurring is True
    assert prices[1].interval == "month"


@pytest.mark.asyncio
async def test_create_product_checkout(client: GrowFlowBillingClient, mock_api):
    """Test creating product checkout session."""
    mock_api.post("/checkout/product-sessions").mock(
        return_value=Response(
            200,
            json={
                "checkout_url": "https://checkout.stripe.com/pay/cs_prod_123",
                "session_id": "cs_prod_123",
            },
        )
    )

    session = await client.create_product_checkout(
        customer_external_id="store_123",
        price_id="price_001",
        success_url="https://app.com/success",
        cancel_url="https://app.com/cancel",
    )
    assert session.checkout_url.startswith("https://checkout.stripe.com")
    assert session.session_id == "cs_prod_123"


@pytest.mark.asyncio
async def test_get_customer_products(client: GrowFlowBillingClient, mock_api):
    """Test getting customer purchased products."""
    mock_api.get("/subscribers/store_123/products").mock(
        return_value=Response(
            200,
            json={
                "items": [
                    {
                        "product_id": "prod_001",
                        "product_name": "Extra Conversations",
                        "price_id": "price_001",
                        "quantity": 2,
                        "purchased_at": "2024-01-10T00:00:00Z",
                    }
                ]
            },
        )
    )

    products = await client.get_customer_products("store_123")
    assert len(products) == 1
    assert products[0]["product_id"] == "prod_001"
    assert products[0]["quantity"] == 2


@pytest.mark.asyncio
async def test_get_customer_products_empty(client: GrowFlowBillingClient, mock_api):
    """Test getting empty customer products."""
    mock_api.get("/subscribers/empty_user/products").mock(
        return_value=Response(200, json={"items": []})
    )

    products = await client.get_customer_products("empty_user")
    assert len(products) == 0


def test_product_properties():
    """Test Product model properties."""
    product = Product(
        id="prod_001",
        name="Test",
        prices=[
            Price(id="price_a", product_id="prod_001", unit_amount=500),
            Price(id="price_b", product_id="prod_001", unit_amount=1000),
        ],
        default_price_id="price_b",
    )
    assert product.default_price is not None
    assert product.default_price.id == "price_b"
    assert product.default_price.amount_decimal == 10.00

    # Product with no default_price_id returns None (requires explicit default_price_id)
    product_no_default = Product(
        id="prod_002",
        name="Test2",
        prices=[Price(id="price_c", product_id="prod_002", unit_amount=300)],
    )
    assert product_no_default.default_price is None

    # Product with default_price_id that doesn't match any price returns first price
    product_fallback = Product(
        id="prod_002b",
        name="Test2b",
        prices=[Price(id="price_c", product_id="prod_002b", unit_amount=300)],
        default_price_id="nonexistent",
    )
    assert product_fallback.default_price is not None
    assert product_fallback.default_price.id == "price_c"

    # Product with no prices
    product_empty = Product(id="prod_003", name="Test3")
    assert product_empty.default_price is None


# =============================================================================
# Error handling
# =============================================================================


@pytest.mark.asyncio
async def test_authentication_error(client: GrowFlowBillingClient, mock_api):
    """Test authentication error handling."""
    mock_api.get("/plans").mock(
        return_value=Response(401, json={"detail": "Invalid API key"})
    )

    with pytest.raises(AuthenticationError):
        await client.get_plans()


@pytest.mark.asyncio
async def test_not_found_error(client: GrowFlowBillingClient, mock_api):
    """Test not found error handling."""
    mock_api.get("/subscribers/nonexistent").mock(
        return_value=Response(404, json={"detail": "Subscriber not found"})
    )

    with pytest.raises(NotFoundError):
        await client.get_subscriber("nonexistent")


@pytest.mark.asyncio
async def test_validation_error(client: GrowFlowBillingClient, mock_api):
    """Test validation error handling."""
    mock_api.post("/subscribers").mock(
        return_value=Response(422, json={"detail": "Invalid email format"})
    )

    with pytest.raises(ValidationError) as exc_info:
        await client.create_subscriber(external_id="test", email="bad")
    assert "Invalid email" in str(exc_info.value)


@pytest.mark.asyncio
async def test_rate_limit_error(client: GrowFlowBillingClient, mock_api):
    """Test rate limit error handling."""
    mock_api.get("/plans").mock(
        return_value=Response(429, json={"detail": "Rate limit exceeded"})
    )

    with pytest.raises(RateLimitError):
        await client.get_plans()


@pytest.mark.asyncio
async def test_server_error(client: GrowFlowBillingClient, mock_api):
    """Test server error handling."""
    mock_api.get("/plans").mock(
        return_value=Response(500, json={"detail": "Internal server error"})
    )

    with pytest.raises(ServerError):
        await client.get_plans()


@pytest.mark.asyncio
async def test_generic_error(client: GrowFlowBillingClient, mock_api):
    """Test generic 4xx error handling."""
    mock_api.get("/plans").mock(
        return_value=Response(400, json={"detail": "Bad request"})
    )

    with pytest.raises(GrowFlowBillingError):
        await client.get_plans()


def test_error_string_representation():
    """Test error __str__ output."""
    error_with_code = GrowFlowBillingError("test error", status_code=400)
    assert str(error_with_code) == "[400] test error"

    error_without_code = GrowFlowBillingError("plain error")
    assert str(error_without_code) == "plain error"


# =============================================================================
# Webhooks
# =============================================================================


WEBHOOK_SECRET = "whsec_test_secret_key"


def _create_signature(payload: str, secret: str) -> str:
    return "sha256=" + hmac.new(
        secret.encode(), payload.encode("utf-8"), hashlib.sha256
    ).hexdigest()


VALID_PAYLOAD = json.dumps({
    "event_type": "subscription.created",
    "event_id": "evt_001",
    "timestamp": "2024-01-15T10:30:00Z",
    "data": {
        "subscription": {
            "id": "sub_001",
            "status": "active",
            "billing_cycle": "monthly",
            "stripe_subscription_id": "sub_stripe",
        },
        "subscriber": {
            "id": "subscriber_001",
            "external_id": "store_123",
            "email": "test@example.com",
            "name": "Test",
        },
        "plan": {
            "id": "plan_pro",
            "slug": "pro",
            "name": "Pro",
        },
    },
})


def test_valid_event_types():
    """Test VALID_EVENT_TYPES contains all expected types."""
    assert "subscription.created" in VALID_EVENT_TYPES
    assert "subscription.updated" in VALID_EVENT_TYPES
    assert "subscription.canceled" in VALID_EVENT_TYPES
    assert "subscription.trial_ending" in VALID_EVENT_TYPES
    assert "invoice.paid" in VALID_EVENT_TYPES
    assert "invoice.payment_failed" in VALID_EVENT_TYPES
    assert "subscriber.created" in VALID_EVENT_TYPES
    assert "subscriber.updated" in VALID_EVENT_TYPES
    assert len(VALID_EVENT_TYPES) == 8


def test_verify_webhook_signature_valid():
    """Test valid webhook signature verification."""
    payload_bytes = VALID_PAYLOAD.encode("utf-8")
    signature = _create_signature(VALID_PAYLOAD, WEBHOOK_SECRET)
    assert verify_webhook_signature(payload_bytes, signature, WEBHOOK_SECRET) is True


def test_verify_webhook_signature_invalid():
    """Test invalid webhook signature."""
    payload_bytes = VALID_PAYLOAD.encode("utf-8")
    assert verify_webhook_signature(payload_bytes, "sha256=invalid", WEBHOOK_SECRET) is False


def test_verify_webhook_signature_wrong_secret():
    """Test signature with wrong secret."""
    payload_bytes = VALID_PAYLOAD.encode("utf-8")
    signature = _create_signature(VALID_PAYLOAD, WEBHOOK_SECRET)
    assert verify_webhook_signature(payload_bytes, signature, "wrong_secret") is False


def test_parse_webhook_event_string():
    """Test parsing webhook event from string."""
    event = parse_webhook_event(VALID_PAYLOAD)
    assert event.event_type == "subscription.created"
    assert event.event_id == "evt_001"


def test_parse_webhook_event_bytes():
    """Test parsing webhook event from bytes."""
    event = parse_webhook_event(VALID_PAYLOAD.encode("utf-8"))
    assert event.event_type == "subscription.created"


def test_parse_webhook_event_dict():
    """Test parsing webhook event from dict."""
    event = parse_webhook_event(json.loads(VALID_PAYLOAD))
    assert event.event_type == "subscription.created"


def test_parse_webhook_event_invalid_json():
    """Test parsing invalid JSON raises WebhookParseError."""
    with pytest.raises(WebhookParseError, match="Invalid JSON"):
        parse_webhook_event("not json")


def test_parse_webhook_event_missing_event_type():
    """Test missing event_type raises WebhookParseError."""
    with pytest.raises(WebhookParseError, match="Missing 'event_type'"):
        parse_webhook_event(json.dumps({"event_id": "e", "timestamp": "t"}))


def test_parse_webhook_event_unknown_type():
    """Test unknown event type raises WebhookParseError."""
    with pytest.raises(WebhookParseError, match="Unknown event type"):
        parse_webhook_event(json.dumps({
            "event_type": "unknown.type",
            "event_id": "e",
            "timestamp": "t",
        }))


def test_parse_webhook_event_missing_event_id():
    """Test missing event_id raises WebhookParseError."""
    with pytest.raises(WebhookParseError, match="Missing 'event_id'"):
        parse_webhook_event(json.dumps({
            "event_type": "subscription.created",
            "timestamp": "t",
        }))


def test_parse_webhook_event_missing_timestamp():
    """Test missing timestamp raises WebhookParseError."""
    with pytest.raises(WebhookParseError, match="Missing 'timestamp'"):
        parse_webhook_event(json.dumps({
            "event_type": "subscription.created",
            "event_id": "e",
        }))


def test_construct_event_valid():
    """Test construct_event with valid signature."""
    signature = _create_signature(VALID_PAYLOAD, WEBHOOK_SECRET)
    event = construct_event(VALID_PAYLOAD.encode("utf-8"), signature, WEBHOOK_SECRET)
    assert event.event_type == "subscription.created"


def test_construct_event_invalid_signature():
    """Test construct_event with invalid signature raises error."""
    with pytest.raises(WebhookSignatureError):
        construct_event(VALID_PAYLOAD.encode("utf-8"), "sha256=invalid", WEBHOOK_SECRET)


def test_webhook_event_subscription_data():
    """Test extracting subscription data from event."""
    event = parse_webhook_event(VALID_PAYLOAD)
    sub = event.subscription
    assert sub is not None
    assert sub.id == "sub_001"
    assert sub.status == "active"
    assert sub.billing_cycle == "monthly"


def test_webhook_event_subscriber_data():
    """Test extracting subscriber data from event."""
    event = parse_webhook_event(VALID_PAYLOAD)
    subscriber = event.subscriber
    assert subscriber is not None
    assert subscriber.external_id == "store_123"
    assert subscriber.email == "test@example.com"


def test_webhook_event_plan_data():
    """Test extracting plan data from event."""
    event = parse_webhook_event(VALID_PAYLOAD)
    plan = event.plan
    assert plan is not None
    assert plan.slug == "pro"


def test_webhook_event_invoice_data():
    """Test extracting invoice data from event."""
    payload = json.dumps({
        "event_type": "invoice.paid",
        "event_id": "evt_inv",
        "timestamp": "2024-01-15T10:30:00Z",
        "data": {
            "invoice": {
                "stripe_invoice_id": "in_stripe_001",
                "number": "INV-001",
                "status": "paid",
                "amount_paid": 2900,
            },
        },
    })
    event = parse_webhook_event(payload)
    invoice = event.invoice
    assert invoice is not None
    assert invoice.number == "INV-001"
    assert invoice.amount_paid == 2900


def test_webhook_event_no_data():
    """Test extracting data when not present returns None."""
    event = WebhookEvent(
        event_type="subscriber.created",
        event_id="evt_empty",
        timestamp="2024-01-01T00:00:00Z",
        data={},
    )
    assert event.subscription is None
    assert event.subscriber is None
    assert event.plan is None
    assert event.invoice is None


def test_webhook_event_type_checks():
    """Test event type checking methods."""
    sub_event = WebhookEvent(
        event_type="subscription.created",
        event_id="e",
        timestamp="t",
    )
    assert sub_event.is_subscription_event() is True
    assert sub_event.is_invoice_event() is False
    assert sub_event.is_subscriber_event() is False

    inv_event = WebhookEvent(
        event_type="invoice.paid",
        event_id="e",
        timestamp="t",
    )
    assert inv_event.is_invoice_event() is True
    assert inv_event.is_subscription_event() is False

    subscriber_event = WebhookEvent(
        event_type="subscriber.updated",
        event_id="e",
        timestamp="t",
    )
    assert subscriber_event.is_subscriber_event() is True
    assert subscriber_event.is_subscription_event() is False
