"""Tests for backend.billing.webhook_handler."""

from __future__ import annotations

import hashlib
import hmac
import time
from datetime import UTC, datetime, timedelta
from typing import Any
from unittest.mock import AsyncMock, patch

import pytest
from backend.billing.webhook_handler import (
    handle_webhook_event,
    verify_paddle_signature,
)

# ---------------------------------------------------------------------------
# Signature verification
# ---------------------------------------------------------------------------


class TestPaddleSignature:
    def _make_signature(self, body: bytes, secret: str) -> str:
        ts = str(int(time.time()))
        message = f"{ts}:{body.decode('utf-8')}"
        h1 = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
        return f"ts={ts};h1={h1}"

    def test_valid_signature(self) -> None:
        body = b'{"event_id": "evt_1"}'
        secret = "test_secret"
        sig = self._make_signature(body, secret)
        assert verify_paddle_signature(body, sig, secret) is True

    def test_invalid_signature(self) -> None:
        body = b'{"event_id": "evt_1"}'
        assert verify_paddle_signature(body, "ts=123;h1=bad", "secret") is False

    def test_expired_timestamp(self) -> None:
        ts = str(int(time.time()) - 600)
        h1 = hmac.new(
            b"secret",
            f"{ts}:body".encode(),
            hashlib.sha256,
        ).hexdigest()
        sig = f"ts={ts};h1={h1}"
        assert verify_paddle_signature(b"body", sig, "secret") is False

    def test_missing_parts(self) -> None:
        assert verify_paddle_signature(b"body", "invalid", "secret") is False
        assert verify_paddle_signature(b"body", "", "secret") is False


# ---------------------------------------------------------------------------
# Webhook handler (parametrized)
# ---------------------------------------------------------------------------


def _base_event(event_type: str, data: dict[str, Any]) -> dict[str, Any]:
    return {
        "event_id": f"evt_test_{event_type}",
        "event_type": event_type,
        "data": data,
    }


def _checkout_data(
    *,
    price_id: str = "pri_pro_annual",
    subscription_id: str = "sub_123",
    customer_id: str = "ctm_456",
    email: str = "user@example.com",
) -> dict[str, Any]:
    return {
        "subscription_id": subscription_id,
        "customer_id": customer_id,
        "customer": {"email": email},
        "items": [{"price": {"id": price_id}}],
        "address": {"country_code": "US"},
        "custom_data": {
            "tos_accepted_at": "2026-03-16T00:00:00Z",
            "tos_version": "1.0",
            "privacy_version": "1.0",
        },
    }


# Patches applied to all handler tests
_FS_PATCH = "backend.billing.webhook_handler"


@pytest.fixture()
def _mock_firestore() -> Any:
    """Patch all Firestore and email calls."""
    patches = [
        patch(
            f"{_FS_PATCH}.is_event_processed",
            new_callable=AsyncMock,
            return_value=False,
        ),
        patch(f"{_FS_PATCH}.mark_event_processed", new_callable=AsyncMock),
        patch(f"{_FS_PATCH}.create_customer", new_callable=AsyncMock),
        patch(f"{_FS_PATCH}.update_customer", new_callable=AsyncMock),
        patch(f"{_FS_PATCH}.create_license_key", new_callable=AsyncMock),
        patch(f"{_FS_PATCH}.update_license_key", new_callable=AsyncMock),
        patch(f"{_FS_PATCH}.create_api_key", new_callable=AsyncMock),
        patch(
            f"{_FS_PATCH}.find_license_key_by_customer",
            new_callable=AsyncMock,
            return_value=None,
        ),
        patch(
            f"{_FS_PATCH}.get_customer_by_subscription",
            new_callable=AsyncMock,
            return_value=None,
        ),
        patch(
            f"{_FS_PATCH}.send_template_email",
            new_callable=AsyncMock,
            return_value=True,
        ),
        patch("backend.shared.firestore.record_consent", new_callable=AsyncMock),
    ]
    mocks = [p.start() for p in patches]
    yield {
        "is_event_processed": mocks[0],
        "mark_event_processed": mocks[1],
        "create_customer": mocks[2],
        "update_customer": mocks[3],
        "create_license_key": mocks[4],
        "update_license_key": mocks[5],
        "create_api_key": mocks[6],
        "find_license_key_by_customer": mocks[7],
        "get_customer_by_subscription": mocks[8],
        "send_template_email": mocks[9],
        "record_consent": mocks[10],
    }
    for p in patches:
        p.stop()


class TestTransactionCompletedCheckout:
    async def test_creates_customer_and_license(self, _mock_firestore: Any) -> None:
        mocks = _mock_firestore
        event = _base_event("transaction.completed", _checkout_data())
        await handle_webhook_event(event)

        mocks["create_customer"].assert_called_once()
        mocks["create_license_key"].assert_called_once()
        mocks["send_template_email"].assert_called_once()
        mocks["mark_event_processed"].assert_called_once()

    async def test_team_generates_api_key(self, _mock_firestore: Any) -> None:
        mocks = _mock_firestore
        data = _checkout_data(price_id="pri_team_annual")
        event = _base_event("transaction.completed", data)
        await handle_webhook_event(event)

        mocks["create_api_key"].assert_called_once()

    async def test_pro_no_api_key(self, _mock_firestore: Any) -> None:
        mocks = _mock_firestore
        event = _base_event("transaction.completed", _checkout_data())
        await handle_webhook_event(event)

        mocks["create_api_key"].assert_not_called()


class TestTransactionCompletedRenewal:
    async def test_extends_expiry_no_email(self, _mock_firestore: Any) -> None:
        mocks = _mock_firestore
        now = datetime.now(UTC)
        expiry = (now + timedelta(days=30)).isoformat()

        mocks["get_customer_by_subscription"].return_value = (
            "ctm_456",
            {"subscription_expires_at": expiry, "tier": "pro"},
        )
        mocks["find_license_key_by_customer"].return_value = (
            "hash123",
            {"status": "active"},
        )

        event = _base_event("transaction.completed", _checkout_data())
        await handle_webhook_event(event)

        mocks["update_customer"].assert_called_once()
        mocks["update_license_key"].assert_called_once()
        # Renewal is silent — no email
        mocks["send_template_email"].assert_not_called()
        mocks["create_customer"].assert_not_called()


class TestTransactionPastDue:
    async def test_subscription_past_due_no_action(self, _mock_firestore: Any) -> None:
        mocks = _mock_firestore
        data = {
            "subscription_id": "sub_123",
            "items": [{"price": {"id": "pri_pro_annual"}}],
        }
        mocks["get_customer_by_subscription"].return_value = (
            "ctm_456",
            {"tier": "pro", "overage_past_due_since": None, "email": "u@e.com"},
        )

        event = _base_event("transaction.past_due", data)
        await handle_webhook_event(event)

        # No overage item, so no update or email
        mocks["update_customer"].assert_not_called()
        mocks["send_template_email"].assert_not_called()

    async def test_overage_past_due_first_occurrence(
        self, _mock_firestore: Any
    ) -> None:
        mocks = _mock_firestore
        data = {
            "subscription_id": "sub_123",
            "items": [
                {"price": {"id": "pri_agency_overage"}, "totals": {"total": "125.00"}}
            ],
        }
        mocks["get_customer_by_subscription"].return_value = (
            "ctm_456",
            {"tier": "agency", "overage_past_due_since": None, "email": "u@e.com"},
        )

        event = _base_event("transaction.past_due", data)
        await handle_webhook_event(event)

        mocks["update_customer"].assert_called_once()
        call_args = mocks["update_customer"].call_args
        assert "overage_past_due_since" in call_args[0][1]
        mocks["send_template_email"].assert_called_once()

    async def test_overage_past_due_already_flagged(self, _mock_firestore: Any) -> None:
        mocks = _mock_firestore
        data = {
            "subscription_id": "sub_123",
            "items": [{"price": {"id": "pri_agency_overage"}}],
        }
        mocks["get_customer_by_subscription"].return_value = (
            "ctm_456",
            {
                "tier": "agency",
                "overage_past_due_since": "2026-01-01T00:00:00Z",
                "email": "u@e.com",
            },
        )

        event = _base_event("transaction.past_due", data)
        await handle_webhook_event(event)

        mocks["update_customer"].assert_not_called()
        mocks["send_template_email"].assert_not_called()


class TestSubscriptionUpdated:
    async def test_upgrade_updates_tier(self, _mock_firestore: Any) -> None:
        mocks = _mock_firestore
        mocks["get_customer_by_subscription"].return_value = (
            "ctm_456",
            {"tier": "pro", "email": "u@e.com"},
        )
        mocks["find_license_key_by_customer"].return_value = (
            "hash123",
            {"status": "active"},
        )

        data = {
            "id": "sub_123",
            "previous_attributes": {"items": [{"price": {"id": "pri_pro_annual"}}]},
            "items": [{"price": {"id": "pri_team_annual"}}],
        }
        event = _base_event("subscription.updated", data)
        await handle_webhook_event(event)

        mocks["update_customer"].assert_called()
        mocks["update_license_key"].assert_called_once()
        mocks["send_template_email"].assert_called_once()

    async def test_downgrade_rejected(self, _mock_firestore: Any) -> None:
        mocks = _mock_firestore
        mocks["get_customer_by_subscription"].return_value = (
            "ctm_456",
            {"tier": "team", "email": "u@e.com"},
        )

        data = {
            "id": "sub_123",
            "previous_attributes": {"items": [{"price": {"id": "pri_team_annual"}}]},
            "items": [{"price": {"id": "pri_pro_annual"}}],
        }
        event = _base_event("subscription.updated", data)
        await handle_webhook_event(event)

        mocks["update_customer"].assert_not_called()
        mocks["update_license_key"].assert_not_called()

    async def test_metadata_only_change_ignored(self, _mock_firestore: Any) -> None:
        mocks = _mock_firestore
        data = {
            "id": "sub_123",
            "previous_attributes": {"description": "old"},
        }
        event = _base_event("subscription.updated", data)
        await handle_webhook_event(event)

        mocks["get_customer_by_subscription"].assert_not_called()

    async def test_dashboard_boundary_crossing(self, _mock_firestore: Any) -> None:
        mocks = _mock_firestore
        mocks["get_customer_by_subscription"].return_value = (
            "ctm_456",
            {"tier": "pro", "email": "u@e.com"},
        )
        mocks["find_license_key_by_customer"].return_value = (
            "hash123",
            {"status": "active"},
        )

        data = {
            "id": "sub_123",
            "previous_attributes": {"items": [{"price": {"id": "pri_pro_annual"}}]},
            "items": [{"price": {"id": "pri_team_annual"}}],
        }
        event = _base_event("subscription.updated", data)
        await handle_webhook_event(event)

        # Pro -> Team crosses dashboard boundary
        mocks["create_api_key"].assert_called_once()


class TestSubscriptionCanceled:
    async def test_deactivates_license_sends_feedback(
        self, _mock_firestore: Any
    ) -> None:
        mocks = _mock_firestore
        mocks["get_customer_by_subscription"].return_value = (
            "ctm_456",
            {"tier": "pro", "email": "u@e.com"},
        )
        mocks["find_license_key_by_customer"].return_value = (
            "hash123",
            {"status": "active"},
        )

        data = {"id": "sub_123"}
        event = _base_event("subscription.canceled", data)
        await handle_webhook_event(event)

        # Verify customer status set to cancelled
        update_args = mocks["update_customer"].call_args_list[0]
        assert update_args[0][1]["status"] == "cancelled"

        # Verify license revoked
        mocks["update_license_key"].assert_called_once()
        lk_args = mocks["update_license_key"].call_args
        assert lk_args[0][1]["status"] == "revoked"

        # Verify feedback email
        mocks["send_template_email"].assert_called_once()


# ---------------------------------------------------------------------------
# Idempotency
# ---------------------------------------------------------------------------


class TestIdempotency:
    async def test_duplicate_event_skipped(self, _mock_firestore: Any) -> None:
        mocks = _mock_firestore
        mocks["is_event_processed"].return_value = True

        event = _base_event("transaction.completed", _checkout_data())
        await handle_webhook_event(event)

        mocks["create_customer"].assert_not_called()
        mocks["mark_event_processed"].assert_not_called()


# ---------------------------------------------------------------------------
# Consent recording
# ---------------------------------------------------------------------------


class TestConsentRecording:
    async def test_consent_recorded_on_checkout(self, _mock_firestore: Any) -> None:
        mocks = _mock_firestore
        event = _base_event("transaction.completed", _checkout_data())
        await handle_webhook_event(event)

        mocks["record_consent"].assert_called_once()
        consent_kwargs = mocks["record_consent"].call_args[1]
        assert consent_kwargs["consent_source"] == "paddle_checkout"
        assert consent_kwargs["tos_version"] == "1.0"

    async def test_missing_custom_data_still_processes(
        self, _mock_firestore: Any
    ) -> None:
        mocks = _mock_firestore
        data = _checkout_data()
        data["custom_data"] = None
        event = _base_event("transaction.completed", data)

        await handle_webhook_event(event)

        # Should still create customer
        mocks["create_customer"].assert_called_once()


# ---------------------------------------------------------------------------
# Email failure
# ---------------------------------------------------------------------------


class TestEmailFailure:
    async def test_email_failure_does_not_fail_webhook(
        self, _mock_firestore: Any
    ) -> None:
        mocks = _mock_firestore
        mocks["send_template_email"].return_value = False

        event = _base_event("transaction.completed", _checkout_data())
        await handle_webhook_event(event)

        # Customer still created
        mocks["create_customer"].assert_called_once()
        # email_delivery_failed set
        update_calls = mocks["update_customer"].call_args_list
        assert any(
            call[0][1].get("email_delivery_failed") is True for call in update_calls
        )


# ---------------------------------------------------------------------------
# Parametrized summary (covers Done When checklist)
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    ("event_type", "expected_behavior"),
    [
        ("transaction.completed_checkout", "creates customer + license + sends email"),
        ("transaction.completed_renewal", "extends expiry, no email"),
        ("transaction.past_due_subscription", "logs event, no action"),
        ("transaction.past_due_overage", "sets overage_past_due_since, sends warning"),
        ("subscription.updated_upgrade", "updates tier, sends confirmation"),
        ("subscription.updated_downgrade", "rejects, logs warning"),
        ("subscription.updated_metadata", "returns without changes"),
        ("subscription.canceled", "deactivates license, sends feedback"),
    ],
    ids=[
        "checkout",
        "renewal",
        "past_due_sub",
        "past_due_overage",
        "upgrade",
        "downgrade",
        "metadata",
        "canceled",
    ],
)
def test_event_types_documented(event_type: str, expected_behavior: str) -> None:
    """Ensure all event types are accounted for — test existence only."""
    assert isinstance(event_type, str)
    assert isinstance(expected_behavior, str)
