"""Regression test for the YooKassa webhook 500 caused by a missing class attr.

A real production crash looked like::

    YooKassa Webhook Parsed: Event='payment.succeeded'
    AttributeError: 'SubscriptionService' object has no attribute '_PROVIDER_LABELS'

The fix wires ``_PROVIDER_LABELS`` onto ``PaymentContextMixin`` so the
payment-success email lookup can never blow up the webhook handler again.
These tests pin the contract end-to-end.
"""

import unittest
from types import SimpleNamespace
from typing import List
from unittest.mock import patch

from bot.services.subscription_service_impl import payments as payments_module
from bot.services.subscription_service_impl.payments import PaymentContextMixin

# Provider strings that flow into ``Subscription.provider`` — every value
# present at a real call site must be representable here.
_PROVIDERS_FROM_CALLERS = {
    "yookassa",
    "freekassa",
    "platega",
    "severpay",
    "wata",
    "cryptopay",
    "telegram_stars",
}


class ProviderLabelsTests(unittest.TestCase):
    def test_attribute_is_defined_on_mixin(self):
        # The crash was specifically ``AttributeError`` — guard against that.
        self.assertTrue(hasattr(PaymentContextMixin, "_PROVIDER_LABELS"))
        self.assertIsInstance(PaymentContextMixin._PROVIDER_LABELS, dict)

    def test_every_runtime_provider_has_a_label(self):
        labels = PaymentContextMixin._PROVIDER_LABELS
        for provider in _PROVIDERS_FROM_CALLERS:
            with self.subTest(provider=provider):
                self.assertIn(provider, labels)
                self.assertTrue(labels[provider])  # non-empty display text

    def test_labels_are_keyed_by_lowercase_string(self):
        # Lookup site does ``self._PROVIDER_LABELS.get((provider or '').lower())``
        # so keys must already be lowercase to ever resolve.
        for key in PaymentContextMixin._PROVIDER_LABELS:
            self.assertEqual(key, key.lower())


class _FakeUser(SimpleNamespace):
    pass


class _FakeEmailService:
    instances: List["_FakeEmailService"] = []

    def __init__(self, settings, i18n=None):
        self.settings = settings
        self.i18n = i18n
        self.sent: List[dict] = []
        _FakeEmailService.instances.append(self)

    async def send_rendered_email(self, *, email, content):
        self.sent.append({"email": email, "content": content})


class SendPaymentSuccessEmailTests(unittest.IsolatedAsyncioTestCase):
    """Exercise the exact code path that hit the AttributeError in prod."""

    def setUp(self) -> None:
        _FakeEmailService.instances = []

    async def _invoke(self, provider: str) -> dict:
        captured: dict = {}

        def fake_render(settings, **kwargs):
            captured.update(kwargs)
            return SimpleNamespace(subject="s", text="t", html="<p>h</p>")

        mixin = PaymentContextMixin()
        mixin.settings = SimpleNamespace(
            email_auth_configured=True,
            DEFAULT_LANGUAGE="ru",
            DEFAULT_CURRENCY_SYMBOL="RUB",
            SUBSCRIPTION_MINI_APP_URL="https://app.example.com/",
        )
        user = _FakeUser(user_id=42, email="buyer@example.com", language_code="en")

        with (
            patch.object(payments_module, "render_payment_success", fake_render),
            patch.object(payments_module, "EmailAuthService", _FakeEmailService),
        ):
            await mixin._send_payment_success_email(
                db_user=user,
                sale_mode="subscription",
                months=3,
                traffic_gb=None,
                payment_amount=450.0,
                end_date=None,
                provider=provider,
            )
        return captured

    async def test_yookassa_resolves_to_human_label(self):
        kwargs = await self._invoke("yookassa")
        self.assertEqual(kwargs.get("provider_label"), "YooKassa")
        self.assertEqual(len(_FakeEmailService.instances), 1)
        self.assertEqual(len(_FakeEmailService.instances[0].sent), 1)

    async def test_unknown_provider_renders_as_none_without_raising(self):
        # The email template treats ``None`` as "skip the provider row" — we
        # need that fallback because new providers may ship before labels do.
        kwargs = await self._invoke("paypal_someday")
        self.assertIsNone(kwargs.get("provider_label"))

    async def test_provider_lookup_is_case_insensitive(self):
        kwargs = await self._invoke("YooKassa")
        self.assertEqual(kwargs.get("provider_label"), "YooKassa")

    async def test_skips_email_when_smtp_unconfigured(self):
        mixin = PaymentContextMixin()
        mixin.settings = SimpleNamespace(email_auth_configured=False)
        user = _FakeUser(user_id=1, email="x@y.z", language_code="en")

        with patch.object(payments_module, "EmailAuthService", _FakeEmailService):
            await mixin._send_payment_success_email(
                db_user=user,
                sale_mode="subscription",
                months=1,
                traffic_gb=None,
                payment_amount=10.0,
                end_date=None,
                provider="yookassa",
            )

        self.assertEqual(_FakeEmailService.instances, [])

    async def test_skips_email_when_user_has_no_email(self):
        mixin = PaymentContextMixin()
        mixin.settings = SimpleNamespace(
            email_auth_configured=True,
            DEFAULT_LANGUAGE="ru",
            DEFAULT_CURRENCY_SYMBOL="RUB",
            SUBSCRIPTION_MINI_APP_URL="",
        )
        user = _FakeUser(user_id=1, email="   ", language_code="en")

        with patch.object(payments_module, "EmailAuthService", _FakeEmailService):
            await mixin._send_payment_success_email(
                db_user=user,
                sale_mode="subscription",
                months=1,
                traffic_gb=None,
                payment_amount=10.0,
                end_date=None,
                provider="yookassa",
            )

        self.assertEqual(_FakeEmailService.instances, [])

    async def test_render_exceptions_are_swallowed(self):
        # The mail path must never block payment processing — this is exactly
        # the contract that the original crash violated for a different reason.
        def boom(*args, **kwargs):
            raise RuntimeError("smtp on fire")

        mixin = PaymentContextMixin()
        mixin.settings = SimpleNamespace(
            email_auth_configured=True,
            DEFAULT_LANGUAGE="ru",
            DEFAULT_CURRENCY_SYMBOL="RUB",
            SUBSCRIPTION_MINI_APP_URL="",
        )
        user = _FakeUser(user_id=1, email="x@y.z", language_code="en")

        with patch.object(payments_module, "render_payment_success", boom):
            # No assertion needed — completion without raising IS the assertion.
            await mixin._send_payment_success_email(
                db_user=user,
                sale_mode="subscription",
                months=1,
                traffic_gb=None,
                payment_amount=10.0,
                end_date=None,
                provider="yookassa",
            )


if __name__ == "__main__":  # pragma: no cover
    unittest.main()
