"""Интеграционные проверки обработки вебхуков PaymentService."""

from __future__ import annotations

import sys
from pathlib import Path
from types import ModuleType, SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock

import pytest


ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
    sys.path.insert(0, str(ROOT_DIR))

import app.services.payment.cryptobot as cryptobot_module
import app.services.payment_service as payment_service_module
from app.config import settings
from app.database.models import PaymentMethod
from app.services.payment_service import PaymentService


class DummyBot:
    def __init__(self) -> None:
        self.sent_messages: list[dict[str, Any]] = []

    async def send_message(
        self, *args: Any, **kwargs: Any
    ) -> None:  # pragma: no cover - бизнес-логика тестируется через вызов
        self.sent_messages.append({'args': args, 'kwargs': kwargs})


class FakeScalarResult:
    def __init__(self, items: list[Any]) -> None:
        self._items = list(items)

    def all(self) -> list[Any]:  # pragma: no cover - утилитарный метод
        return list(self._items)

    def first(self) -> Any:  # pragma: no cover - утилитарный метод
        return self._items[0] if self._items else None

    def one(self) -> Any:  # pragma: no cover - утилитарный метод
        if len(self._items) != 1:
            raise ValueError('Expected exactly one result')
        return self._items[0]

    def one_or_none(self) -> Any:  # pragma: no cover - утилитарный метод
        if not self._items:
            return None
        if len(self._items) > 1:
            raise ValueError('Expected zero or one result')
        return self._items[0]

    def __iter__(self):  # pragma: no cover - утилитарный метод
        return iter(self._items)


class FakeResult:
    def __init__(self, value: Any = None) -> None:
        self._value = value

    def _as_iterable(self) -> list[Any]:
        if isinstance(self._value, list):
            return self._value
        if self._value is None:
            return []
        return [self._value]

    def scalar(self) -> Any:
        items = self._as_iterable()
        return items[0] if items else None

    def scalar_one_or_none(self) -> Any:
        items = self._as_iterable()
        if not items:
            return None
        if len(items) > 1:
            raise ValueError('Expected zero or one result')
        return items[0]

    def first(self) -> Any:  # pragma: no cover - утилитарный метод
        items = self._as_iterable()
        return items[0] if items else None

    def all(self) -> list[Any]:  # pragma: no cover - утилитарный метод
        return list(self._as_iterable())

    def one_or_none(self) -> Any:  # pragma: no cover - утилитарный метод
        items = self._as_iterable()
        if not items:
            return None
        if len(items) > 1:
            raise ValueError('Expected zero or one result')
        return items[0]

    def scalars(self) -> FakeScalarResult:  # pragma: no cover - утилитарный метод
        return FakeScalarResult(self._as_iterable())


class FakeSession:
    def __init__(self) -> None:
        self.commits = 0
        self.refreshed: list[Any] = []
        self.added: list[Any] = []
        self.execute_statements: list[Any] = []
        self.execute_results: list[Any] = []

    async def commit(self) -> None:
        self.commits += 1

    async def rollback(self) -> None:  # pragma: no cover
        return None

    async def refresh(self, obj: Any) -> None:
        self.refreshed.append(obj)

    def add(self, obj: Any) -> None:  # pragma: no cover - используется при создании транзакций
        self.added.append(obj)

    async def execute(self, statement: Any, *args: Any, **kwargs: Any) -> FakeResult:
        self.execute_statements.append(statement)
        if self.execute_results:
            result = self.execute_results.pop(0)
            if callable(result):  # pragma: no cover - гибкость для будущих тестов
                result = result(statement, *args, **kwargs)
        else:
            result = None

        if isinstance(result, FakeResult):
            return result

        return FakeResult(result)


def _make_service(bot: DummyBot) -> PaymentService:
    service = PaymentService.__new__(PaymentService)  # type: ignore[call-arg]
    service.bot = bot
    service.yookassa_service = None
    service.stars_service = None
    service.mulenpay_service = None
    service.pal24_service = None
    service.cryptobot_service = None
    service.heleket_service = None
    return service


@pytest.fixture
def anyio_backend() -> str:
    return 'asyncio'


@pytest.mark.anyio('asyncio')
@pytest.mark.parametrize('status_field', ['payment_status', 'status', 'paymentStatus'])
async def test_process_mulenpay_callback_success(monkeypatch: pytest.MonkeyPatch, status_field: str) -> None:
    bot = DummyBot()
    service = _make_service(bot)
    fake_session = FakeSession()
    payment = SimpleNamespace(
        uuid='mulen_uuid',
        mulen_payment_id=123,
        amount_kopeks=5000,
        user_id=42,
        transaction_id=None,
        is_paid=False,
    )

    async def fake_get_by_uuid(db, uuid):
        return payment

    async def fake_get_by_id(db, mid):
        return None

    monkeypatch.setattr(payment_service_module, 'get_mulenpay_payment_by_uuid', fake_get_by_uuid)
    monkeypatch.setattr(payment_service_module, 'get_mulenpay_payment_by_mulen_id', fake_get_by_id)

    transactions: list[dict[str, Any]] = []

    async def fake_create_transaction(db, **kwargs):
        transactions.append(kwargs)
        return SimpleNamespace(id=777, **kwargs)

    monkeypatch.setattr(payment_service_module, 'create_transaction', fake_create_transaction)

    updated_status: dict[str, Any] = {}

    async def fake_update_status(db, payment=None, status=None, **kwargs):
        payment.status = status
        payment.is_paid = status == 'success'
        updated_status.update({'status': status, 'kwargs': kwargs})

    monkeypatch.setattr(payment_service_module, 'update_mulenpay_payment_status', fake_update_status)

    async def fake_link(db, payment=None, transaction_id=None):
        payment.transaction_id = transaction_id

    monkeypatch.setattr(payment_service_module, 'link_mulenpay_payment_to_transaction', fake_link)

    user = SimpleNamespace(
        id=42,
        telegram_id=100500,
        balance_kopeks=0,
        has_made_first_topup=False,
        promo_group=None,
        subscription=None,
        referred_by_id=None,
        referrer=None,
    )
    user.get_primary_promo_group = lambda: getattr(user, 'promo_group', None)

    async def fake_get_user(db, user_id):
        return user

    monkeypatch.setattr(payment_service_module, 'get_user_by_id', fake_get_user)
    monkeypatch.setattr(type(settings), 'format_price', lambda self, amount: f'{amount / 100:.2f}₽', raising=False)

    referral_mock = SimpleNamespace(process_referral_topup=AsyncMock())
    monkeypatch.setitem(sys.modules, 'app.services.referral_service', referral_mock)

    class DummyAdminService:
        def __init__(self, bot):
            self.bot = bot
            self.calls: list[Any] = []

        async def send_balance_topup_notification(self, *args, **kwargs):
            self.calls.append((args, kwargs))

    admin_service = DummyAdminService(bot)
    monkeypatch.setitem(
        sys.modules,
        'app.services.admin_notification_service',
        SimpleNamespace(AdminNotificationService=lambda bot: admin_service),
    )

    service.build_topup_success_keyboard = AsyncMock(return_value=None)

    payload = {
        'uuid': 'mulen_uuid',
        'id': 123,
        'amount': '50.00',
    }
    payload[status_field] = 'success'

    result = await service.process_mulenpay_callback(fake_session, payload)

    assert result is True
    assert transactions and transactions[0]['user_id'] == 42
    assert payment.transaction_id == 777
    assert updated_status['status'] == 'success'
    assert user.balance_kopeks == 5000
    assert fake_session.commits >= 1
    assert bot.sent_messages  # сообщение пользователю отправлено


@pytest.mark.anyio('asyncio')
async def test_process_cryptobot_webhook_success(monkeypatch: pytest.MonkeyPatch) -> None:
    bot = DummyBot()
    service = _make_service(bot)
    fake_session = FakeSession()
    payment = SimpleNamespace(
        invoice_id='inv_1',
        user_id=7,
        status='pending',
        transaction_id=None,
        amount='12.50',
        asset='USDT',
        amount_float=12.5,
    )

    async def fake_get_crypto(db, invoice_id):
        return payment

    async def fake_update_status(db, invoice_id, status, paid_at):
        payment.status = status
        payment.paid_at = paid_at
        return payment

    async def fake_link(db, invoice_id, transaction_id):
        payment.transaction_id = transaction_id

    fake_cryptobot_module = ModuleType('app.database.crud.cryptobot')
    fake_cryptobot_module.get_cryptobot_payment_by_invoice_id = fake_get_crypto
    fake_cryptobot_module.update_cryptobot_payment_status = fake_update_status
    fake_cryptobot_module.link_cryptobot_payment_to_transaction = fake_link
    monkeypatch.setitem(sys.modules, 'app.database.crud.cryptobot', fake_cryptobot_module)

    transactions: list[dict[str, Any]] = []
    created_transaction: SimpleNamespace | None = None

    async def fake_create_transaction(db, **kwargs):
        nonlocal created_transaction
        transactions.append(kwargs)
        created_transaction = SimpleNamespace(id=888, **kwargs)
        return created_transaction

    fake_transaction_module = ModuleType('app.database.crud.transaction')
    fake_transaction_module.create_transaction = fake_create_transaction

    async def fake_get_transaction_by_id(db, transaction_id):
        return created_transaction

    fake_transaction_module.get_transaction_by_id = fake_get_transaction_by_id
    monkeypatch.setitem(sys.modules, 'app.database.crud.transaction', fake_transaction_module)
    monkeypatch.setattr(payment_service_module, 'create_transaction', fake_create_transaction)

    user = SimpleNamespace(
        id=7,
        telegram_id=700,
        balance_kopeks=0,
        has_made_first_topup=False,
        promo_group=None,
        subscription=None,
        referred_by_id=None,
        referrer=None,
    )
    user.get_primary_promo_group = lambda: getattr(user, 'promo_group', None)

    async def fake_get_user_crypto(db, user_id):
        return user

    monkeypatch.setattr(payment_service_module, 'get_user_by_id', fake_get_user_crypto)

    fake_user_module = ModuleType('app.database.crud.user')
    fake_user_module.get_user_by_id = fake_get_user_crypto
    monkeypatch.setitem(sys.modules, 'app.database.crud.user', fake_user_module)

    referral_crypto = SimpleNamespace(process_referral_topup=AsyncMock())
    monkeypatch.setitem(sys.modules, 'app.services.referral_service', referral_crypto)

    admin_calls: list[Any] = []

    class DummyAdminService2:
        def __init__(self, bot):
            self.bot = bot

        async def send_balance_topup_notification(self, *args, **kwargs):
            admin_calls.append((args, kwargs))

    monkeypatch.setitem(
        sys.modules,
        'app.services.admin_notification_service',
        SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService2(bot)),
    )

    class DummyAsyncSession:
        async def __aenter__(self):
            return self

        async def __aexit__(self, exc_type, exc, tb):
            return False

        async def rollback(self):  # pragma: no cover - defensive stub
            return None

    monkeypatch.setattr(cryptobot_module, 'AsyncSessionLocal', lambda: DummyAsyncSession())
    monkeypatch.setattr(payment_service_module.currency_converter, 'usd_to_rub', AsyncMock(return_value=140.0))
    monkeypatch.setattr(type(settings), 'format_price', lambda self, amount: f'{amount / 100:.2f}₽', raising=False)
    service.build_topup_success_keyboard = AsyncMock(return_value=None)

    payload = {
        'update_type': 'invoice_paid',
        'payload': {
            'invoice_id': 'inv_1',
            'paid_at': '2024-01-01T12:00:00Z',
        },
    }

    result = await service.process_cryptobot_webhook(fake_session, payload)

    assert result is True
    assert transactions and transactions[0]['amount_kopeks'] == 14000
    assert user.balance_kopeks == 14000
    assert payment.transaction_id == 888
    assert bot.sent_messages
    assert admin_calls


@pytest.mark.anyio('asyncio')
async def test_process_heleket_webhook_success(monkeypatch: pytest.MonkeyPatch) -> None:
    bot = DummyBot()
    service = _make_service(bot)
    fake_session = FakeSession()

    payment = SimpleNamespace(
        uuid='heleket-uuid',
        order_id='heleket-order',
        user_id=77,
        amount='150.00',
        amount_float=150.0,
        amount_kopeks=15000,
        status='check',
        payer_amount=None,
        payer_currency=None,
        exchange_rate=None,
        discount_percent=None,
        payment_url=None,
        transaction_id=None,
    )

    async def fake_get_by_uuid(db, uuid):
        return payment if uuid == payment.uuid else None

    async def fake_get_by_order(db, order_id):
        return payment if order_id == payment.order_id else None

    async def fake_update(
        db,
        uuid,
        *,
        status=None,
        payer_amount=None,
        payer_currency=None,
        exchange_rate=None,
        discount_percent=None,
        paid_at=None,
        payment_url=None,
        metadata=None,
    ):
        if status is not None:
            payment.status = status
        if payer_amount is not None:
            payment.payer_amount = payer_amount
        if payer_currency is not None:
            payment.payer_currency = payer_currency
        if exchange_rate is not None:
            payment.exchange_rate = exchange_rate
        if discount_percent is not None:
            payment.discount_percent = discount_percent
        if payment_url is not None:
            payment.payment_url = payment_url
        payment.paid_at = paid_at
        if metadata:
            payment.metadata_json = metadata
        return payment

    async def fake_link(db, uuid, transaction_id):
        payment.transaction_id = transaction_id
        return payment

    heleket_module = ModuleType('app.database.crud.heleket')
    heleket_module.get_heleket_payment_by_uuid = fake_get_by_uuid
    heleket_module.get_heleket_payment_by_order_id = fake_get_by_order
    heleket_module.update_heleket_payment = fake_update
    heleket_module.link_heleket_payment_to_transaction = fake_link
    monkeypatch.setitem(sys.modules, 'app.database.crud.heleket', heleket_module)

    transactions: list[dict[str, Any]] = []

    async def fake_create_transaction(db, **kwargs):
        transactions.append(kwargs)
        return SimpleNamespace(id=321, **kwargs)

    monkeypatch.setattr(payment_service_module, 'create_transaction', fake_create_transaction)

    user = SimpleNamespace(
        id=77,
        telegram_id=7700,
        balance_kopeks=0,
        has_made_first_topup=False,
        promo_group=None,
        subscription=None,
        referred_by_id=None,
        referrer=None,
        language='ru',
    )
    user.get_primary_promo_group = lambda: getattr(user, 'promo_group', None)

    async def fake_get_user(db, user_id):
        return user if user_id == user.id else None

    monkeypatch.setattr(payment_service_module, 'get_user_by_id', fake_get_user)
    monkeypatch.setattr('app.services.payment.heleket.format_referrer_info', lambda u: '')

    monkeypatch.setattr(type(settings), 'format_price', lambda self, amount: f'{amount / 100:.2f}₽', raising=False)

    referral_stub = SimpleNamespace(process_referral_topup=AsyncMock())
    monkeypatch.setitem(sys.modules, 'app.services.referral_service', referral_stub)

    admin_calls: list[Any] = []

    class DummyAdminService:
        def __init__(self, bot):
            self.bot = bot

        async def send_balance_topup_notification(self, *args, **kwargs):
            admin_calls.append((args, kwargs))

    monkeypatch.setitem(
        sys.modules,
        'app.services.admin_notification_service',
        SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
    )

    service.build_topup_success_keyboard = AsyncMock(return_value=None)

    payload = {
        'uuid': 'heleket-uuid',
        'status': 'paid',
        'payer_amount': '2.50',
        'payer_currency': 'USDT',
        'discount_percent': -5,
        'payer_amount_exchange_rate': '0.0166',
        'paid_at': '2024-01-02T12:00:00Z',
        'url': 'https://pay.example',
    }

    result = await service.process_heleket_webhook(fake_session, payload)

    assert result is True
    assert transactions and transactions[0]['payment_method'] == PaymentMethod.HELEKET
    assert payment.transaction_id == 321
    assert user.balance_kopeks == 15000
    assert user.has_made_first_topup is True
    assert fake_session.commits >= 1
    assert bot.sent_messages
    assert admin_calls
    referral_stub.process_referral_topup.assert_awaited_once()


@pytest.mark.anyio('asyncio')
async def test_process_yookassa_webhook_success(monkeypatch: pytest.MonkeyPatch) -> None:
    bot = DummyBot()
    service = _make_service(bot)
    fake_session = FakeSession()
    payment = SimpleNamespace(
        yookassa_payment_id='yk_123',
        user_id=21,
        amount_kopeks=10000,
        transaction_id=None,
        status='pending',
        is_paid=False,
    )

    async def fake_get_payment(db, payment_id):
        return payment

    async def fake_update(db, payment_id, status, is_paid, is_captured, captured_at, payment_method_type):
        payment.status = status
        payment.is_paid = is_paid
        payment.captured_at = captured_at
        return payment

    async def fake_link(db, payment_id, transaction_id):
        payment.transaction_id = transaction_id

    yk_module = ModuleType('app.database.crud.yookassa')
    yk_module.get_yookassa_payment_by_id = fake_get_payment
    yk_module.update_yookassa_payment_status = fake_update
    yk_module.link_yookassa_payment_to_transaction = fake_link
    monkeypatch.setitem(sys.modules, 'app.database.crud.yookassa', yk_module)

    transactions: list[dict[str, Any]] = []

    async def fake_create_transaction(db, **kwargs):
        transactions.append(kwargs)
        return SimpleNamespace(id=999, **kwargs)

    trx_module = ModuleType('app.database.crud.transaction')
    trx_module.create_transaction = fake_create_transaction
    monkeypatch.setitem(sys.modules, 'app.database.crud.transaction', trx_module)
    monkeypatch.setattr(payment_service_module, 'create_transaction', fake_create_transaction)
    monkeypatch.setattr(payment_service_module, 'create_transaction', fake_create_transaction)
    monkeypatch.setattr(payment_service_module, 'create_transaction', fake_create_transaction)

    user = SimpleNamespace(
        id=21,
        telegram_id=2100,
        balance_kopeks=0,
        has_made_first_topup=False,
        promo_group=None,
        subscription=None,
        referred_by_id=None,
        referrer=None,
    )
    user.get_primary_promo_group = lambda: getattr(user, 'promo_group', None)

    async def fake_get_user(db, user_id):
        return user

    monkeypatch.setattr(payment_service_module, 'get_user_by_id', fake_get_user)
    monkeypatch.setattr(type(settings), 'format_price', lambda self, amount: f'{amount / 100:.2f}₽', raising=False)

    referral_mock = SimpleNamespace(process_referral_topup=AsyncMock())
    monkeypatch.setitem(sys.modules, 'app.services.referral_service', referral_mock)

    admin_calls: list[Any] = []

    class DummyAdminService:
        def __init__(self, bot):
            self.bot = bot

        async def send_balance_topup_notification(self, *args, **kwargs):
            admin_calls.append((args, kwargs))

    monkeypatch.setitem(
        sys.modules,
        'app.services.admin_notification_service',
        SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
    )
    service.build_topup_success_keyboard = AsyncMock(return_value=None)

    payload = {
        'object': {
            'id': 'yk_123',
            'status': 'succeeded',
            'paid': True,
            'payment_method': {'type': 'bank_card'},
        }
    }

    result = await service.process_yookassa_webhook(fake_session, payload)

    assert result is True
    assert transactions and transactions[0]['amount_kopeks'] == 10000
    assert payment.transaction_id == 999
    assert payment.is_paid is True
    assert user.balance_kopeks == 10000
    assert bot.sent_messages
    assert admin_calls


@pytest.mark.anyio('asyncio')
async def test_process_yookassa_webhook_uses_remote_status(monkeypatch: pytest.MonkeyPatch) -> None:
    bot = DummyBot()
    service = _make_service(bot)
    fake_session = FakeSession()
    payment = SimpleNamespace(
        yookassa_payment_id='yk_789',
        user_id=42,
        amount_kopeks=20000,
        transaction_id=None,
        status='pending',
        is_paid=False,
    )

    async def fake_get_payment(db, payment_id):
        return payment

    async def fake_update(db, payment_id, status, is_paid, is_captured, captured_at, payment_method_type):
        payment.status = status
        payment.is_paid = is_paid
        payment.captured_at = captured_at
        payment.payment_method_type = payment_method_type
        return payment

    async def fake_link(db, payment_id, transaction_id):
        payment.transaction_id = transaction_id

    yk_module = ModuleType('app.database.crud.yookassa')
    yk_module.get_yookassa_payment_by_id = fake_get_payment
    yk_module.update_yookassa_payment_status = fake_update
    yk_module.link_yookassa_payment_to_transaction = fake_link
    monkeypatch.setitem(sys.modules, 'app.database.crud.yookassa', yk_module)

    transactions: list[dict[str, Any]] = []

    async def fake_create_transaction(db, **kwargs):
        transactions.append(kwargs)
        return SimpleNamespace(id=555, **kwargs)

    monkeypatch.setattr(payment_service_module, 'create_transaction', fake_create_transaction)

    user = SimpleNamespace(
        id=42,
        telegram_id=4200,
        balance_kopeks=0,
        has_made_first_topup=False,
        promo_group=None,
        subscription=None,
        referred_by_id=None,
        referrer=None,
    )

    async def fake_get_user(db, user_id):
        return user

    monkeypatch.setattr(payment_service_module, 'get_user_by_id', fake_get_user)
    monkeypatch.setattr(type(settings), 'format_price', lambda self, amount: f'{amount / 100:.2f}₽', raising=False)

    referral_mock = SimpleNamespace(process_referral_topup=AsyncMock())
    monkeypatch.setitem(sys.modules, 'app.services.referral_service', referral_mock)

    admin_calls: list[Any] = []

    class DummyAdminService:
        def __init__(self, bot):
            self.bot = bot

        async def send_balance_topup_notification(self, *args, **kwargs):
            admin_calls.append((args, kwargs))

    monkeypatch.setitem(
        sys.modules,
        'app.services.admin_notification_service',
        SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
    )

    service.build_topup_success_keyboard = AsyncMock(return_value=None)

    remote_payload = {
        'id': 'yk_789',
        'status': 'succeeded',
        'paid': True,
        'amount_value': 200.0,
        'amount_currency': 'rub',
        'payment_method_type': 'bank_card',
        'refundable': True,
    }

    get_info_mock = AsyncMock(return_value=remote_payload)
    service.yookassa_service = SimpleNamespace(get_payment_info=get_info_mock)

    payload = {
        'object': {
            'id': 'yk_789',
            'status': 'pending',
            'paid': False,
        }
    }

    result = await service.process_yookassa_webhook(fake_session, payload)

    assert result is True
    assert payment.status == 'succeeded'
    assert payment.is_paid is True
    assert transactions and transactions[0]['amount_kopeks'] == 20000
    assert payment.transaction_id == 555
    get_info_mock.assert_awaited_once_with('yk_789')
    assert admin_calls


@pytest.mark.anyio('asyncio')
async def test_process_yookassa_webhook_handles_cancellation(monkeypatch: pytest.MonkeyPatch) -> None:
    bot = DummyBot()
    service = _make_service(bot)
    fake_session = FakeSession()
    payment = SimpleNamespace(
        yookassa_payment_id='yk_cancel',
        user_id=77,
        amount_kopeks=5000,
        transaction_id=None,
        status='pending',
        is_paid=False,
        captured_at=None,
        payment_method_type=None,
    )

    async def fake_get_payment(db, payment_id):
        return payment

    monkeypatch.setattr(
        payment_service_module,
        'get_yookassa_payment_by_id',
        fake_get_payment,
    )

    get_info_mock = AsyncMock(
        return_value={
            'id': 'yk_cancel',
            'status': 'canceled',
            'paid': False,
            'amount_value': 50.0,
            'amount_currency': 'RUB',
        }
    )
    service.yookassa_service = SimpleNamespace(get_payment_info=get_info_mock)

    payload = {
        'object': {
            'id': 'yk_cancel',
            'status': 'pending',
            'paid': False,
        }
    }

    result = await service.process_yookassa_webhook(fake_session, payload)

    assert result is True
    assert payment.status == 'canceled'
    assert payment.is_paid is False
    assert fake_session.commits == 1
    assert fake_session.refreshed and fake_session.refreshed[0] is payment
    assert bot.sent_messages == []
    get_info_mock.assert_awaited_once_with('yk_cancel')


@pytest.mark.anyio('asyncio')
async def test_process_yookassa_webhook_restores_missing_payment(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    bot = DummyBot()
    service = _make_service(bot)
    fake_session = FakeSession()

    restored_payment = SimpleNamespace(
        yookassa_payment_id='yk_456',
        user_id=21,
        amount_kopeks=0,
        status='pending',
        is_paid=False,
        transaction_id=None,
        description='',
        payment_method_type=None,
        confirmation_url=None,
        metadata_json=None,
        test_mode=False,
        refundable=False,
    )

    get_calls = {'count': 0}

    async def fake_get_payment(db, payment_id):
        get_calls['count'] += 1
        if get_calls['count'] == 1:
            return None
        return restored_payment

    async def fake_create_payment(**kwargs: Any):
        restored_payment.user_id = kwargs['user_id']
        restored_payment.amount_kopeks = kwargs['amount_kopeks']
        restored_payment.status = kwargs['status']
        restored_payment.description = kwargs['description']
        restored_payment.payment_method_type = kwargs['payment_method_type']
        restored_payment.confirmation_url = kwargs['confirmation_url']
        restored_payment.metadata_json = kwargs['metadata_json']
        restored_payment.test_mode = kwargs['test_mode']
        restored_payment.yookassa_payment_id = kwargs['yookassa_payment_id']
        restored_payment.yookassa_created_at = kwargs['yookassa_created_at']
        return restored_payment

    async def fake_update_status(
        db,
        yookassa_payment_id,
        status,
        is_paid,
        is_captured,
        captured_at,
        payment_method_type,
    ):
        restored_payment.status = status
        restored_payment.is_paid = is_paid
        restored_payment.is_captured = is_captured
        restored_payment.captured_at = captured_at
        restored_payment.payment_method_type = payment_method_type
        return restored_payment

    async def fake_link(db, yookassa_payment_id, transaction_id):
        restored_payment.transaction_id = transaction_id

    monkeypatch.setattr(payment_service_module, 'get_yookassa_payment_by_id', fake_get_payment)
    monkeypatch.setattr(payment_service_module, 'create_yookassa_payment', fake_create_payment)
    monkeypatch.setattr(payment_service_module, 'update_yookassa_payment_status', fake_update_status)
    monkeypatch.setattr(payment_service_module, 'link_yookassa_payment_to_transaction', fake_link)

    transactions: list[dict[str, Any]] = []

    async def fake_create_transaction(db, **kwargs):
        transactions.append(kwargs)
        return SimpleNamespace(id=555, **kwargs)

    monkeypatch.setattr(payment_service_module, 'create_transaction', fake_create_transaction)

    user = SimpleNamespace(
        id=21,
        telegram_id=2100,
        balance_kopeks=0,
        has_made_first_topup=False,
        promo_group=None,
        subscription=None,
        referred_by_id=None,
        referrer=None,
    )
    user.get_primary_promo_group = lambda: getattr(user, 'promo_group', None)

    async def fake_get_user(db, user_id):
        return user

    monkeypatch.setattr(payment_service_module, 'get_user_by_id', fake_get_user)
    monkeypatch.setattr(type(settings), 'format_price', lambda self, amount: f'{amount / 100:.2f}₽', raising=False)

    referral_mock = SimpleNamespace(process_referral_topup=AsyncMock())
    monkeypatch.setitem(sys.modules, 'app.services.referral_service', referral_mock)

    admin_calls: list[Any] = []

    class DummyAdminService:
        def __init__(self, bot):
            self.bot = bot

        async def send_balance_topup_notification(self, *args, **kwargs):
            admin_calls.append((args, kwargs))

    monkeypatch.setitem(
        sys.modules,
        'app.services.admin_notification_service',
        SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
    )
    service.build_topup_success_keyboard = AsyncMock(return_value=None)

    payload = {
        'object': {
            'id': 'yk_456',
            'status': 'succeeded',
            'paid': True,
            'amount': {'value': '150.00', 'currency': 'RUB'},
            'metadata': {'user_id': '21', 'payment_purpose': 'balance_topup'},
            'description': 'Пополнение',
            'payment_method': {'type': 'bank_card'},
            'created_at': '2024-01-02T12:00:00Z',
            'captured_at': '2024-01-02T12:05:00Z',
            'confirmation': {'confirmation_url': 'https://pay.example'},
        }
    }

    result = await service.process_yookassa_webhook(fake_session, payload)

    assert result is True
    assert get_calls['count'] >= 2  # повторный запрос после восстановления
    assert restored_payment.amount_kopeks == 15000
    assert restored_payment.is_paid is True
    assert transactions and transactions[0]['amount_kopeks'] == 15000
    assert restored_payment.transaction_id == 555
    assert user.balance_kopeks == 15000
    assert bot.sent_messages
    assert admin_calls


@pytest.mark.anyio('asyncio')
async def test_process_yookassa_webhook_missing_metadata(monkeypatch: pytest.MonkeyPatch) -> None:
    service = _make_service(DummyBot())
    db = FakeSession()

    async def fake_get_payment(db_session, payment_id):
        return None

    create_mock = AsyncMock()
    update_mock = AsyncMock()

    monkeypatch.setattr(payment_service_module, 'get_yookassa_payment_by_id', fake_get_payment)
    monkeypatch.setattr(payment_service_module, 'create_yookassa_payment', create_mock)
    monkeypatch.setattr(payment_service_module, 'update_yookassa_payment_status', update_mock)

    payload = {'object': {'id': 'yk_missing', 'status': 'succeeded', 'paid': True}}

    result = await service.process_yookassa_webhook(db, payload)

    assert result is False
    create_mock.assert_not_awaited()
    update_mock.assert_not_awaited()


@pytest.mark.anyio('asyncio')
async def test_process_yookassa_webhook_missing_id(monkeypatch: pytest.MonkeyPatch) -> None:
    bot = DummyBot()
    service = _make_service(bot)
    db = FakeSession()

    result = await service.process_yookassa_webhook(db, {'object': {}})
    assert result is False


@pytest.mark.anyio('asyncio')
async def test_process_pal24_callback_success(monkeypatch: pytest.MonkeyPatch) -> None:
    bot = DummyBot()
    service = _make_service(bot)
    service.pal24_service = SimpleNamespace(is_configured=True)
    fake_session = FakeSession()
    payment = SimpleNamespace(
        bill_id='BILL-1',
        order_id='order-1',
        amount_kopeks=5000,
        user_id=33,
        transaction_id=None,
        is_paid=False,
        status='NEW',
        metadata_json={},
        payment_method=None,
        paid_at=None,
    )

    async def fake_get_by_order(db, order_id):
        return payment

    async def fake_get_by_bill(db, bill_id):
        return payment

    async def fake_update(db, payment_obj, **kwargs):
        payment.status = kwargs.get('status', payment.status)
        payment.is_paid = kwargs.get('is_paid', payment.is_paid)
        payment.payment_status = kwargs.get('payment_status', payment.status)
        payment.callback_payload = kwargs.get('callback_payload')
        return payment

    async def fake_link(db, payment_obj, transaction_id):
        payment.transaction_id = transaction_id

    pal_module = ModuleType('app.database.crud.pal24')
    pal_module.get_pal24_payment_by_order_id = fake_get_by_order
    pal_module.get_pal24_payment_by_bill_id = fake_get_by_bill
    pal_module.update_pal24_payment_status = fake_update
    pal_module.link_pal24_payment_to_transaction = fake_link
    monkeypatch.setitem(sys.modules, 'app.database.crud.pal24', pal_module)
    monkeypatch.setattr(payment_service_module, 'get_pal24_payment_by_order_id', fake_get_by_order)
    monkeypatch.setattr(payment_service_module, 'get_pal24_payment_by_bill_id', fake_get_by_bill)
    monkeypatch.setattr(payment_service_module, 'update_pal24_payment_status', fake_update)
    monkeypatch.setattr(payment_service_module, 'link_pal24_payment_to_transaction', fake_link)

    async def fake_create_transaction(db, **kwargs):
        payment.transaction_id = 654
        return SimpleNamespace(id=654, **kwargs)

    trx_module = ModuleType('app.database.crud.transaction')
    trx_module.create_transaction = fake_create_transaction
    monkeypatch.setitem(sys.modules, 'app.database.crud.transaction', trx_module)
    monkeypatch.setattr(payment_service_module, 'create_transaction', fake_create_transaction)

    user = SimpleNamespace(
        id=33,
        telegram_id=3300,
        balance_kopeks=0,
        has_made_first_topup=False,
        promo_group=None,
        subscription=None,
        referred_by_id=None,
        referrer=None,
        language='ru',
    )
    user.get_primary_promo_group = lambda: getattr(user, 'promo_group', None)

    async def fake_get_user(db, user_id):
        return user

    monkeypatch.setattr(payment_service_module, 'get_user_by_id', fake_get_user)
    monkeypatch.setattr(type(settings), 'format_price', lambda self, amount: f'{amount / 100:.2f}₽', raising=False)

    referral_pal = SimpleNamespace(process_referral_topup=AsyncMock())
    monkeypatch.setitem(sys.modules, 'app.services.referral_service', referral_pal)

    admin_calls: list[Any] = []

    class DummyAdminServicePal:
        def __init__(self, bot):
            self.bot = bot

        async def send_balance_topup_notification(self, *args, **kwargs):
            admin_calls.append((args, kwargs))

    monkeypatch.setitem(
        sys.modules,
        'app.services.admin_notification_service',
        SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminServicePal(bot)),
    )

    user_cart_stub = SimpleNamespace(user_cart_service=SimpleNamespace(has_user_cart=AsyncMock(return_value=True)))
    monkeypatch.setitem(sys.modules, 'app.services.user_cart_service', user_cart_stub)

    class DummyTypes:
        class InlineKeyboardMarkup:
            def __init__(self, inline_keyboard=None, **kwargs):
                self.inline_keyboard = inline_keyboard or []
                self.kwargs = kwargs

        class InlineKeyboardButton:
            def __init__(self, *args, **kwargs):
                self.args = args
                self.kwargs = kwargs

    monkeypatch.setitem(sys.modules, 'aiogram', SimpleNamespace(types=DummyTypes))
    monkeypatch.setitem(
        sys.modules,
        'app.localization.texts',
        SimpleNamespace(get_texts=lambda language: SimpleNamespace(t=lambda key, default=None: default)),
    )

    service.build_topup_success_keyboard = AsyncMock(return_value=None)

    payload = {
        'InvId': 'order-1',
        'OutSum': '50.00',
        'Status': 'SUCCESS',
        'TrsId': 'trs-1',
    }

    result = await service.process_pal24_callback(fake_session, payload)

    assert result is True
    assert payment.transaction_id == 654
    assert user.balance_kopeks == 5000
    assert bot.sent_messages
    saved_cart_message = bot.sent_messages[-1]
    reply_markup = saved_cart_message['kwargs'].get('reply_markup')
    assert reply_markup is not None
    assert reply_markup.inline_keyboard[0][0].kwargs['callback_data'] == 'return_to_saved_cart'
    assert admin_calls


@pytest.mark.anyio('asyncio')
async def test_get_pal24_payment_status_auto_finalize(monkeypatch: pytest.MonkeyPatch) -> None:
    bot = DummyBot()
    service = _make_service(bot)

    class DummyPal24Service:
        BILL_SUCCESS_STATES = {'SUCCESS', 'OVERPAID'}
        BILL_FAILED_STATES = {'FAIL'}
        BILL_PENDING_STATES = {'NEW', 'PROCESS', 'UNDERPAID'}

        async def get_bill_status(self, bill_id: str) -> dict[str, Any]:
            return {
                'status': 'SUCCESS',
                'bill': {
                    'status': 'SUCCESS',
                    'payments': [
                        {
                            'id': 'trs-auto-1',
                            'status': 'SUCCESS',
                            'method': 'SBP',
                            'balance_amount': '50.00',
                            'balance_currency': 'RUB',
                        }
                    ],
                },
            }

        async def get_payment_status(self, payment_id: str) -> dict[str, Any] | None:
            return None

        async def get_bill_payments(self, bill_id: str) -> dict[str, Any] | None:
            return {
                'data': [
                    {
                        'id': 'trs-auto-1',
                        'bill_id': bill_id,
                        'status': 'SUCCESS',
                        'payment_method': 'SBP',
                    }
                ]
            }

    service.pal24_service = DummyPal24Service()

    fake_session = FakeSession()
    payment = SimpleNamespace(
        id=77,
        bill_id='BILL-AUTO',
        order_id='order-auto',
        amount_kopeks=5000,
        user_id=91,
        transaction_id=None,
        is_paid=False,
        status='NEW',
        metadata_json={},
        payment_id=None,
        payment_method=None,
        paid_at=None,
    )

    async def fake_get_payment_by_id(db, local_id):
        return payment

    async def fake_update_payment(db, payment_obj, **kwargs):
        for key, value in kwargs.items():
            setattr(payment, key, value)
        return payment

    async def fake_link_payment(db, payment_obj, transaction_id):
        payment.transaction_id = transaction_id
        return payment

    monkeypatch.setattr(payment_service_module, 'get_pal24_payment_by_id', fake_get_payment_by_id)
    monkeypatch.setattr(payment_service_module, 'update_pal24_payment_status', fake_update_payment)
    monkeypatch.setattr(payment_service_module, 'link_pal24_payment_to_transaction', fake_link_payment)

    transactions: list[dict[str, Any]] = []

    async def fake_create_transaction(db, **kwargs):
        transactions.append(kwargs)
        payment.transaction_id = 999
        return SimpleNamespace(id=999, **kwargs)

    monkeypatch.setattr(payment_service_module, 'create_transaction', fake_create_transaction)

    user = SimpleNamespace(
        id=91,
        telegram_id=9100,
        balance_kopeks=0,
        has_made_first_topup=False,
        promo_group=None,
        subscription=None,
        referred_by_id=None,
        referrer=None,
        language='ru',
    )
    user.get_primary_promo_group = lambda: getattr(user, 'promo_group', None)

    async def fake_get_user(db, user_id):
        return user

    monkeypatch.setattr(payment_service_module, 'get_user_by_id', fake_get_user)
    monkeypatch.setattr(type(settings), 'format_price', lambda self, amount: f'{amount / 100:.2f}₽', raising=False)

    referral_stub = SimpleNamespace(process_referral_topup=AsyncMock())
    monkeypatch.setitem(sys.modules, 'app.services.referral_service', referral_stub)

    admin_notifications: list[Any] = []

    class DummyAdminService:
        def __init__(self, bot):
            self.bot = bot

        async def send_balance_topup_notification(self, *args, **kwargs):
            admin_notifications.append((args, kwargs))

    monkeypatch.setitem(
        sys.modules,
        'app.services.admin_notification_service',
        SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
    )

    user_cart_stub = SimpleNamespace(user_cart_service=SimpleNamespace(has_user_cart=AsyncMock(return_value=False)))
    monkeypatch.setitem(sys.modules, 'app.services.user_cart_service', user_cart_stub)

    class DummyTypes:
        class InlineKeyboardMarkup:
            def __init__(self, inline_keyboard=None, **kwargs):
                self.inline_keyboard = inline_keyboard or []
                self.kwargs = kwargs

        class InlineKeyboardButton:
            def __init__(self, *args, **kwargs):
                self.args = args
                self.kwargs = kwargs

    monkeypatch.setitem(sys.modules, 'aiogram', SimpleNamespace(types=DummyTypes))

    service.build_topup_success_keyboard = AsyncMock(return_value=None)

    result = await service.get_pal24_payment_status(fake_session, payment.id)

    assert result is not None
    assert payment.transaction_id == 999
    assert user.balance_kopeks == 5000
    assert bot.sent_messages
    assert admin_notifications
    assert transactions and transactions[0]['user_id'] == 91


@pytest.mark.anyio('asyncio')
async def test_process_pal24_callback_payment_not_found(monkeypatch: pytest.MonkeyPatch) -> None:
    bot = DummyBot()
    service = _make_service(bot)
    service.pal24_service = SimpleNamespace(is_configured=True)
    db = FakeSession()

    async def fake_get_by_order(db, order_id):
        return None

    async def fake_get_by_bill(db, bill_id):
        return None

    pal_module = ModuleType('app.database.crud.pal24')
    pal_module.get_pal24_payment_by_order_id = fake_get_by_order
    pal_module.get_pal24_payment_by_bill_id = fake_get_by_bill
    pal_module.update_pal24_payment_status = AsyncMock()
    pal_module.link_pal24_payment_to_transaction = AsyncMock()
    monkeypatch.setitem(sys.modules, 'app.database.crud.pal24', pal_module)
    monkeypatch.setattr(payment_service_module, 'get_pal24_payment_by_order_id', fake_get_by_order)
    monkeypatch.setattr(payment_service_module, 'get_pal24_payment_by_bill_id', fake_get_by_bill)

    payload = {
        'InvId': 'order-unknown',
        'OutSum': '10.00',
        'Status': 'SUCCESS',
    }

    result = await service.process_pal24_callback(db, payload)
    assert result is False
