import base64
import binascii
import decimal
import json
import os
import time
import typing
import unicodedata
import urllib.parse
import uuid

import bitcoin
import requests
import retrying
import rsa
from aws_lambda_tools.test.client import AWSTestClient
from aws_lambda_tools.test.resources_mapper import YamlDevelopmentFileResourcesMapper
from bittrade_client.generated_protobuf import bittrade_pb2
from cards_client.generated_protobuf import base_pb2
from core_wallet_client.client.client import CoreWalletClientImpl
from merchant_wallet_libs.logging import requests as safe_requests
from requests import HTTPError

from conio_sdk.common import ACCEPTANCE_TYPE_NAMES_BY_PB2
from conio_sdk.generated_protobuf import v1_pb2
from conio_sdk.ioc import ioc_bithustler, ioc_cards, ioc_vault, ioc_core_wallet, ioc_bittrade
from conio_sdk.services.conio.wallet.bitcoin_wallet_service import BtcAddressID
from etc import settings
from test.integration import VersionedAWSTestClient, AuthenticationHeaders, BIP32PrivateKey, USER_LEVEL, ONE_MINUTE, \
    HYPE_PRIVATE_KEY, BUY_LIMIT, SELL_LIMIT, BearerToken, Satoshi, BTCAddress
from test.integration.BaseIntegrationTest import BaseIntegrationTest

SignupWithNewUser = typing.NamedTuple(
    'SignupWithNewUser',
    (
        ('authentication_headers', AuthenticationHeaders),
        ('bip32_private_key', BIP32PrivateKey),
        ('signup_message', v1_pb2.Signup)
    )
)


class BaseV1IntegrationTest(BaseIntegrationTest):
    @classmethod
    def get_api_version(cls) -> typing.Optional[str]:
        return None

    def setUp(self):
        super(BaseV1IntegrationTest, self).setUp()
        resource_mapper = YamlDevelopmentFileResourcesMapper(
            os.path.join(
                os.path.dirname(__file__),
                '..',
                '..',
                'template-serverless.yaml'
            )
        )

        version = self.get_api_version()
        if version:
            self.client = VersionedAWSTestClient(resource_mapper, version)
        else:
            self.client = AWSTestClient(resource_mapper)

    @classmethod
    def create_signup_message(
            cls, expired: bool = False,
            first_name: typing.Optional[str] = None,
            last_name: typing.Optional[str] = None,
            email: typing.Optional[str] = None,
            external_user_id: typing.Optional[str] = None,
            hashed_conio_password: typing.Optional[str] = None
    ) -> v1_pb2.Signup:

        cls.create_explicit_fees()

        signup = v1_pb2.Signup()

        signup.conioCredentials.externalUserID = external_user_id or str(uuid.uuid4())
        signup.conioCredentials.hashedConioPassword = hashed_conio_password or binascii.hexlify(os.urandom(32)).decode()

        signup.tc.acceptances.add(
            acceptance_type=ACCEPTANCE_TYPE_NAMES_BY_PB2[v1_pb2.ACCEPTANCE_CLIENT_SUPPORT],
            acceptance_choice=v1_pb2.ACC_CHOICE_ACCEPTED
        )
        signup.tc.acceptances.add(
            acceptance_type=ACCEPTANCE_TYPE_NAMES_BY_PB2[v1_pb2.ACCEPTANCE_APP_IMPROVEMENT],
            acceptance_choice=v1_pb2.ACC_CHOICE_ACCEPTED
        )

        seed = os.urandom(16)
        bip32_private_key = bitcoin.bip32_master_key(seed, vbytes=bitcoin.TESTNET_PRIVATE)
        signup.encryptedUserKey.encryptedMnemonic = b'encryptedMnemonic'
        # BEWARE: such data should be encrypted
        signup.encryptedUserKey.encryptedSeed = b'encryptedSeed'
        signup.encryptedUserKey.encryptedPrivateKey = bip32_private_key.encode()
        signup.encryptedUserKey.bip32PublicKey = bitcoin.bip32_privtopub(bip32_private_key)

        signup.cryptoRequest.proofID = str(uuid.uuid4())
        signup.cryptoRequest.proofExpiration = int(time.time() * 1000) + (-ONE_MINUTE if expired else ONE_MINUTE)
        signup.cryptoRequest.iban = cls.random_iban()
        signup.cryptoRequest.email = \
            email or '{}@test_lambda_bff_sdk.conio.com'.format(signup.conioCredentials.externalUserID)
        signup.cryptoRequest.userLevel = USER_LEVEL
        if first_name:
            signup.cryptoRequest.firstName = first_name
        if last_name:
            signup.cryptoRequest.lastName = last_name
        signup.cryptoRequest.externalUserID = signup.conioCredentials.externalUserID

        data = '|'.join(
            filter(
                bool,
                (
                    signup.cryptoRequest.proofID,
                    'SIGNUP',
                    signup.cryptoRequest.externalUserID,
                    signup.cryptoRequest.userLevel,
                    str(signup.cryptoRequest.proofExpiration),
                    signup.cryptoRequest.iban,
                    signup.cryptoRequest.email,
                    signup.cryptoRequest.firstName,
                    signup.cryptoRequest.lastName
                )
            )
        )
        data = unicodedata.normalize('NFC', data).encode('utf-8')

        signup.cryptoRequest.cryptoProof = rsa.pkcs1.sign(data, HYPE_PRIVATE_KEY, 'SHA-256')
        return signup

    @classmethod
    def create_login_message(cls, external_user_id: str, hashed_conio_password: str) -> v1_pb2.Login:
        login = v1_pb2.Login()
        login.conioCredentials.externalUserID = external_user_id
        login.conioCredentials.hashedConioPassword = hashed_conio_password
        return login

    @classmethod
    def _gestpay_post(cls, url: str, json: dict, headers: typing.Optional[dict] = None):
        result = requests.post(
            'http://cards_mock:8080/api/v1/{}'.format(url), json=json, headers=headers,
            verify=False
        )
        result.raise_for_status()
        return result.json()

    def verify_user_limits(self, external_user_id: str):
        user_info = ioc_vault.vault_client.get_user_info_by_attributes(external_reference=external_user_id)
        msg = base_pb2.MsgGetFiatLimits(payer_id=user_info.cards_integration_id, currency=base_pb2.EUR)
        limits = ioc_cards.cards_client.get_fiat_limits(msg).data.all_limits
        self.assertEqual(2, len(limits), msg='Payer {}'.format(user_info.cards_integration_id))
        self.assertEqual(BUY_LIMIT, limits[0].limit, msg='Payer {}'.format(user_info.cards_integration_id))
        self.assertEqual(BUY_LIMIT, limits[1].limit, msg='Payer {}'.format(user_info.cards_integration_id))

        limits = ioc_bithustler.bithustler_client.get_limits_by_seller_id(user_info.seller_id).data.all_limits
        self.assertEqual(1, len(limits), msg='Seller {}'.format(user_info.seller_id))
        self.assertEqual(SELL_LIMIT, limits[0].limit, msg='Seller {}'.format(user_info.seller_id))

    def signin_with_new_user(self) -> SignupWithNewUser:
        signup = self.create_signup_message()
        resp = self.client.post(
            '/v1/signup',
            data=signup.SerializeToString(),
            headers={'Content-Type': 'application/octet-stream'})
        self.assertEqual(200, resp.status_code)
        signup_response = v1_pb2.SignupResponse()
        signup_response.ParseFromString(base64.b64decode(resp.content))
        login = self.create_login_message(
            signup.conioCredentials.externalUserID,
            signup.conioCredentials.hashedConioPassword)
        resp = self.client.post(
            '/v1/login',
            data=login.SerializeToString(),
            headers={'Content-Type': 'application/octet-stream'})
        self.assertEqual(200, resp.status_code)
        login_response = v1_pb2.LoginResponse()
        login_response.ParseFromString(base64.b64decode(resp.content))
        return SignupWithNewUser(
            AuthenticationHeaders(BearerToken('Bearer {}'.format(resp.headers['X-ConioAccessToken']))),
            BIP32PrivateKey(signup_response.encryptedUserKey.encryptedPrivateKey.decode()),
            signup
        )

    def get_wallet_info(self, authentication_headers: AuthenticationHeaders) -> v1_pb2.WalletInfo:
        resp = self.client.get('/v1/wallet_info', headers=authentication_headers._asdict())
        self.assertEqual(200, resp.status_code)
        resp_message = v1_pb2.WalletInfo()
        resp_message.ParseFromString(base64.b64decode(resp.content))
        return resp_message

    def get_activities(
            self, authentication_headers: AuthenticationHeaders,
            from_index: int, get_previous: bool,
            *activity_types: v1_pb2.ActivityType) -> typing.Sequence[v1_pb2.SimpleActivity]:
        msg = v1_pb2.MsgActivitiesInfo(
            fiat='EUR',
            from_index=from_index,
            get_previous=get_previous
        )
        if activity_types:
            msg.activity_types.extend(activity_types)

        resp = self.client.post(
            '/v1/activities',
            data=msg.SerializeToString(),
            headers=authentication_headers._asdict())
        self.assertEqual(200, resp.status_code)
        resp_message = v1_pb2.MsgActivitiesInfoResponse()
        resp_message.ParseFromString(base64.b64decode(resp.content))
        return resp_message.activities

    def get_activity(self, activity_id: str, authentication_headers: AuthenticationHeaders) -> v1_pb2.Activity:
        msg = v1_pb2.MsgActivityDetails(
            activity_id=activity_id,
            fiat_currency='EUR'
        )
        resp = self.client.post('/v1/activity', data=msg.SerializeToString(), headers=authentication_headers._asdict())
        self.assertEqual(200, resp.status_code)
        resp_message = v1_pb2.Activity()
        resp_message.ParseFromString(base64.b64decode(resp.content))
        return resp_message

    def get_btc_withdrawal_fees(
            self, authentication_headers: AuthenticationHeaders,
            btc_address: BtcAddressID,
            transaction_speed_type: typing.Optional[int] = None,
            satoshi_amount: typing.Optional[Satoshi] = None) -> v1_pb2.MsgRequestBtcWithdrawalFeesResponse:
        msg = v1_pb2.MsgRequestBtcWithdrawalFees()
        msg.dest_address = btc_address
        if satoshi_amount:
            msg.amount = satoshi_amount
        if transaction_speed_type:
            msg.speed = transaction_speed_type
        else:
            msg.get_all_fees_info = True

        resp = self.client.post(
            '/v1/get_btc_withdrawal_fees',
            data=msg.SerializeToString(),
            headers=authentication_headers._asdict()
        )
        self.assertEqual(200, resp.status_code)
        resp_message = v1_pb2.MsgRequestBtcWithdrawalFeesResponse()
        resp_message.ParseFromString(base64.b64decode(resp.content))
        return resp_message

    def get_current_btc_address(self, authentication_headers: AuthenticationHeaders) -> BTCAddress:
        resp = self.client.get('/v1/current_btc_address', headers=authentication_headers._asdict())
        self.assertEqual(200, resp.status_code)
        resp_message = v1_pb2.CurrentBtcAddress()
        resp_message.ParseFromString(base64.b64decode(resp.content))
        return resp_message.btc_address_id

    def receive_satoshi(self, satoshi: Satoshi, authentication_headers: AuthenticationHeaders) -> str:
        btc_address = self.get_current_btc_address(authentication_headers)
        print('Sending {} satoshi to address {}'.format(satoshi, btc_address))
        return json.loads(CoreWalletClientImpl(settings.CORE_WALLET_URL, settings.CORE_WALLET_SECRET_KEY).bitcoinrpc(
            'sendtoaddress', btc_address, satoshi / 10**8
        ).decode())['response']

    def ensure_receive_satoshi(self, satoshi: Satoshi, authentication_headers: AuthenticationHeaders) -> str:
        btc_tx = self.receive_satoshi(satoshi, authentication_headers)
        current_blocks = ioc_core_wallet.core_wallet_client.get_best_height()
        self.generate_blocks()

        @retrying.retry(
            wait_exponential_multiplier=200,
            wait_exponential_max=2000, stop_max_delay=60000,
            retry_on_exception=lambda e: isinstance(e, AssertionError)
        )
        def _f():
            latest_blocks = ioc_core_wallet.core_wallet_client.get_best_height()
            print('Attempting to get {} satoshi, blocks {} -> {}'.format(
                satoshi, current_blocks, latest_blocks)
            )

            self.assertGreaterEqual(latest_blocks, current_blocks + 6)
            print('Attempting to get {} satoshi, ok height'.format(satoshi))
            cur_wallet_info = self.get_wallet_info(authentication_headers)
            print(
                'Attempting to get {} satoshi, confirmed satoshi: {}, unconfirmed_satoshi'.format(
                    cur_wallet_info.confirmed_satoshi_amount,
                    cur_wallet_info.unconfirmed_satoshi_amount)
            )
            self.assertEqual(satoshi, cur_wallet_info.confirmed_satoshi_amount)

            self.assertEqual(0, cur_wallet_info.unconfirmed_satoshi_amount)

        _f()
        return btc_tx

    @classmethod
    def generate_blocks(cls, blocks: int = 6):
        # noinspection PyTypeChecker
        CoreWalletClientImpl(settings.CORE_WALLET_URL, settings.CORE_WALLET_SECRET_KEY).bitcoinrpc(
            'generate', blocks
        )

    @retrying.retry(
        wait_exponential_multiplier=200,
        wait_exponential_max=2000, stop_max_delay=60000,
        retry_on_exception=lambda e: not isinstance(e, AssertionError) or 'Link disappeared at iteration ' not in e.args[0]
    )
    def get_mfa_code_from_email(self, expected_email: str):
        resp = requests.get('http://chatter_flask:8080/backoffice/email?_limit=50')
        resp.raise_for_status()
        emails = resp.json()['rows']
        emails = list(filter(lambda d: d['recipient'] == expected_email and d['type'] == 24, emails))
        self.assertTrue(emails)
        email = emails[0]
        link = email['meta']['meta']['mfa_link']
        # link = urlparse(link)  # type: ParseResult
        # vault = urlparse(settings.VAULT_URL)
        # noinspection PyArgumentList
        # link = ParseResult(vault.scheme, vault.netloc, link.path, link.params, link.query, link.fragment).geturl()
        mfa_code = None
        for i in range(10):
            resp = requests.get(link, verify=False, headers={'Accept': 'application/json'})
            try:
                resp.raise_for_status()
            except HTTPError as e:
                if e.response.status_code == 404:
                    self.fail('Link disappeared at iteration {}'.format(i + 1))
                raise e
            cur_mfa_code = resp.json()['mfa_code']
            if mfa_code is None:
                mfa_code = cur_mfa_code
            else:
                self.assertEqual(mfa_code, cur_mfa_code)
            resp = requests.get(link, verify=False, headers={'Accept': 'text/html'})
            resp.raise_for_status()
            html_content = resp.content.decode()
            self.assertTrue(mfa_code in html_content, msg=html_content)
        self.assertIsNotNone(mfa_code)
        return mfa_code

    def attempt_to_alter_2fa_withdraw_btc(
            self,
            authentication_headers: AuthenticationHeaders,
            email: str,
            satoshi_amount: typing.Optional[int] = None):
        msg_req = v1_pb2.MsgRequestBtcWithdrawal(
            dest_address=json.loads(
                CoreWalletClientImpl(settings.CORE_WALLET_URL, settings.CORE_WALLET_SECRET_KEY)
                .bitcoinrpc('getnewaddress').decode()
            )['response']
        )
        if satoshi_amount:
            msg_req.amount = satoshi_amount
        resp = self.client.post(
            '/v1/request_btc_withdrawal',
            data=msg_req.SerializeToString(),
            headers=authentication_headers._asdict())

        self.assertEqual(403, resp.status_code)
        # Here we expect MFA
        mfa_code = self.get_mfa_code_from_email(email)
        mfa_token = json.loads(resp.content)['mfa_token']

        # Intentionally alter message
        msg_req.dest_address = 'mhdTsHCDK5ZcAoKZcxYekwZzRe1TkV7hJn'
        resp = self.client.post(
            '/v1/request_btc_withdrawal',
            data=msg_req.SerializeToString(),
            headers={
                'X-CONIOMFATOKEN': mfa_token,
                'X-CONIOMFACODE': mfa_code,
                **authentication_headers._asdict()
            }
        )
        self.assertEqual(403, resp.status_code, msg=resp.content)

    def withdraw_btc(
            self, bip32_private_key: BIP32PrivateKey,
            authentication_headers: AuthenticationHeaders,
            email: str,
            satoshi_amount: typing.Optional[int] = None,
            expected_response: int = 200):
        msg_req = v1_pb2.MsgRequestBtcWithdrawal(
            dest_address=json.loads(
                CoreWalletClientImpl(settings.CORE_WALLET_URL, settings.CORE_WALLET_SECRET_KEY)
                .bitcoinrpc('getnewaddress').decode()
            )['response']
        )
        if satoshi_amount:
            msg_req.amount = satoshi_amount
        resp = self.client.post(
            '/v1/request_btc_withdrawal',
            data=msg_req.SerializeToString(),
            headers=authentication_headers._asdict())

        self.assertEqual(403, resp.status_code)
        # Here we expect MFA
        mfa_code = self.get_mfa_code_from_email(email)
        mfa_token = json.loads(resp.content)['mfa_token']
        resp = self.client.post(
            '/v1/request_btc_withdrawal',
            data=msg_req.SerializeToString(),
            headers={
                'X-CONIOMFATOKEN': mfa_token,
                'X-CONIOMFACODE': mfa_code,
                **authentication_headers._asdict()
            }
        )

        self.assertEqual(expected_response, resp.status_code, msg=resp.content)
        if expected_response != 200:
            return resp
        resp_msg = v1_pb2.MsgRequestBtcWithdrawalResponse()
        resp_msg.ParseFromString(base64.b64decode(resp.content))

        withdraw_msg = v1_pb2.MsgWithdrawBtc()
        withdraw_msg.tx = resp_msg.tx
        withdraw_msg.date = resp_msg.date
        withdraw_msg.request_signature = resp_msg.signature
        withdraw_msg.tx_signatures.extend(
            [
                v1_pb2.Signature(
                    path=tsd.path,
                    signature=self.sign_with_derived_key(bip32_private_key, tsd.path, tsd.data_to_sign)
                )
                for tsd in resp_msg.sign_data
            ]
        )
        resp = self.client.post(
            '/v1/withdraw_btc',
            data=withdraw_msg.SerializeToString(),
            headers=authentication_headers._asdict())
        self.assertEqual(200, resp.status_code)
        resp_msg = v1_pb2.MsgWithdrawBtcResponse()
        resp_msg.ParseFromString(base64.b64decode(resp.content))
        return resp_msg.activity_id

    @classmethod
    def derive_bip32_private_key(cls, bip32_private_key: BIP32PrivateKey, path: typing.List[int]) -> BIP32PrivateKey:
        derived = bip32_private_key
        for p in path:
            derived = bitcoin.bip32_ckd(derived, p)
        return derived

    @classmethod
    def sign_with_derived_key(
            cls, bip32_private_key: BIP32PrivateKey, path: typing.List[int], data_to_sign: bytes):
        return \
            binascii.unhexlify(
                bitcoin.der_encode_sig(
                    *bitcoin.ecdsa_raw_sign(
                        data_to_sign,
                        bitcoin.bip32_extract_key(cls.derive_bip32_private_key(bip32_private_key, path))
                    )
                )
            )

    def get_current_price(
            self, authentication_headers: AuthenticationHeaders, satoshi_amount: typing.Optional[Satoshi] = None) \
            -> v1_pb2.PricePoint:
        msg = v1_pb2.MsgGetCurrentPrice(currency=v1_pb2.EUR)
        if satoshi_amount:
            msg.satoshi_amount = satoshi_amount

        resp = self.client.post('/v1/current_price', data=msg.SerializeToString(),
                                headers=authentication_headers._asdict())
        self.assertEqual(200, resp.status_code)
        resp_message = v1_pb2.PricePoint()
        resp_message.ParseFromString(base64.b64decode(resp.content))
        return resp_message

    def get_historical_prices(
            self, authentication_headers: AuthenticationHeaders,
            start_timestamp: int, end_timestamp: int, interval: int = 0) \
            -> v1_pb2.MsgHistoryPricesResponse:
        msg = v1_pb2.MsgHistoryPrices(
            start_timestamp=start_timestamp,
            end_timestamp=end_timestamp,
            interval=interval,
            currency=v1_pb2.EUR
        )
        resp = self.client.post('/v1/historical_prices', data=msg.SerializeToString(),
                                headers=authentication_headers._asdict())
        self.assertEqual(200, resp.status_code)
        resp_message = v1_pb2.MsgHistoryPricesResponse()
        resp_message.ParseFromString(base64.b64decode(resp.content))
        return resp_message

    def buy_btc(
            self,
            user_external_reference_id: str,
            authentication_headers: AuthenticationHeaders,
            external_payment_method_reference_id: str,
            satoshi_amount: typing.Optional[Satoshi] = None,
            fiat_amount: typing.Optional[decimal.Decimal] = None,
            refresh_bid: bool = False,
            expected_create_response: int = 200,
            expected_pay_response: int = 200
    ):
        self.assertFalse(bool(satoshi_amount) and bool(fiat_amount))

        if refresh_bid:
            msg = v1_pb2.MsgCreateOrRefreshBid(currency=v1_pb2.EUR)
            if satoshi_amount:
                msg.satoshi = satoshi_amount // 2
            else:
                msg.fiat_amount = float(fiat_amount) / 2
            resp = self.client.post(
                '/v1/bids/create', data=msg.SerializeToString(),
                headers=authentication_headers._asdict())
            self.assertEqual(expected_create_response, resp.status_code, msg=resp.content)
            if expected_create_response != 200:
                return resp
            resp_create_message = v1_pb2.MsgCreateOrRefreshBidResponse()
            resp_create_message.ParseFromString(base64.b64decode(resp.content))
            bid_id = resp_create_message.bid_id
        else:
            bid_id = None

        msg = v1_pb2.MsgCreateOrRefreshBid(currency=v1_pb2.EUR)
        if bid_id:
            msg.bid_id = bid_id
        if satoshi_amount:
            msg.satoshi = satoshi_amount
        elif fiat_amount:
            msg.fiat_amount = float(fiat_amount)
        resp = self.client.post(
            '/v1/bids/create', data=msg.SerializeToString(),
            headers=authentication_headers._asdict())
        self.assertEqual(expected_create_response, resp.status_code)
        if expected_create_response != 200:
            return resp
        resp_create_message = v1_pb2.MsgCreateOrRefreshBidResponse()
        resp_create_message.ParseFromString(base64.b64decode(resp.content))

        msg = v1_pb2.MsgPayForBid(bid_id=resp_create_message.bid_id)
        crypto_proof = v1_pb2.PayForBidCryptoRequest()
        crypto_proof.proofID = str(uuid.uuid4())
        crypto_proof.proofExpiration = int(time.time() * 1000) + ONE_MINUTE
        crypto_proof.external_reference_id = external_payment_method_reference_id
        data = '|'.join(
            (
                crypto_proof.proofID,
                'PAY_FOR_BID',
                user_external_reference_id,
                str(crypto_proof.proofExpiration),
                external_payment_method_reference_id
            )
        )
        data = unicodedata.normalize('NFC', data).encode('utf-8')
        crypto_proof.cryptoProof = rsa.pkcs1.sign(data, HYPE_PRIVATE_KEY, 'SHA-256')
        msg.crypto_proof.MergeFrom(crypto_proof)

        resp = self.client.post(
            '/v1/bids/pay', data=msg.SerializeToString(),
            headers=authentication_headers._asdict())
        self.assertEqual(expected_pay_response, resp.status_code,
                         msg='Error: {} ({})'.format(resp.status_code, resp.content))
        if expected_pay_response != 200:
            return resp
        resp_pay_message = v1_pb2.MsgPayForBidResponse()
        resp_pay_message.ParseFromString(base64.b64decode(resp.content))
        self.assertEqual('https://sandbox.gestpay.net/api/v1/payment/submit/', resp_pay_message.gp_url)
        self.assertEqual('GESPAY64499', resp_pay_message.gp_id)
        if resp_pay_message.maskedpan:
            creditcard = {'token': resp_pay_message.maskedpan}
        else:
            creditcard = {
                'number': '4775718800002026',
                'expMonth': '05',
                'expYear': '27',
                'CVV': '123'
            }

        resp = self._gestpay_post(
            'payment/submit/',
            json={
                'shopLogin': resp_pay_message.gp_id,
                'paymentType': 'CREDITCARD',
                'paymentTypeDetails': {
                    'creditcard': creditcard
                }
            },
            headers={
                'paymentToken': resp_pay_message.payment_token
            }
        )
        msg = v1_pb2.MsgFinalizePaymentForBid(bid_id=resp_pay_message.bid_id)
        if not resp_pay_message.maskedpan:
            msg.maskedpan = resp['payload']['maskedPAN']
        msg.crypto_proof.MergeFrom(crypto_proof)
        resp = self.client.post(
            '/v1/bids/finalize', data=msg.SerializeToString(),
            headers=authentication_headers._asdict())
        self.assertEqual(200, resp.status_code)
        resp_pay_message = v1_pb2.MsgFinalizePaymentForBidResponse()
        resp_pay_message.ParseFromString(base64.b64decode(resp.content))
        return resp_pay_message.bid_id

    def buy_btc_using_wiretransfer(
            self,
            user_external_reference_id: str,
            _authentication_headers: AuthenticationHeaders,
            satoshi_amount: typing.Optional[Satoshi] = None,
            fiat_amount: typing.Optional[decimal.Decimal] = None,
            refresh_bid: bool = False,
            expected_create_response: int = 200,
            expected_pay_response: int = 200
    ):
        self.assertFalse(bool(satoshi_amount) and bool(fiat_amount))
        headers = _authentication_headers._asdict()
        # Since 1.1 version we adopt wiretransfers as payment methods
        headers['versioncode'] = '1.1'

        if refresh_bid:
            msg = v1_pb2.MsgCreateOrRefreshBid(currency=v1_pb2.EUR)
            if satoshi_amount:
                msg.satoshi = satoshi_amount // 2
            else:
                msg.fiat_amount = float(fiat_amount) / 2
            resp = self.client.post(
                '/v1/bids/create', data=msg.SerializeToString(),
                headers=headers)
            self.assertEqual(expected_create_response, resp.status_code, msg=resp.content)
            if expected_create_response != 200:
                return resp
            resp_create_message = v1_pb2.MsgCreateOrRefreshBidResponse()
            resp_create_message.ParseFromString(base64.b64decode(resp.content))
            bid_id = resp_create_message.bid_id
            self.assertTrue(
                resp_create_message.wiretransfer_payee_info.description.startswith('CN'))
            self.assertEqual('Conio tests', resp_create_message.wiretransfer_payee_info.holder)
            self.assertEqual('IT28X0300203280418743513827', resp_create_message.wiretransfer_payee_info.holder_iban)
        else:
            bid_id = None

        msg = v1_pb2.MsgCreateOrRefreshBid(currency=v1_pb2.EUR)
        if bid_id:
            msg.bid_id = bid_id
        if satoshi_amount:
            msg.satoshi = satoshi_amount
        elif fiat_amount:
            msg.fiat_amount = float(fiat_amount)
        resp = self.client.post(
            '/v1/bids/create', data=msg.SerializeToString(),
            headers=headers)
        self.assertEqual(expected_create_response, resp.status_code)
        if expected_create_response != 200:
            return resp
        resp_create_message = v1_pb2.MsgCreateOrRefreshBidResponse()
        resp_create_message.ParseFromString(base64.b64decode(resp.content))
        self.assertTrue(
            resp_create_message.wiretransfer_payee_info.description.startswith('CN'))
        self.assertEqual('Conio tests', resp_create_message.wiretransfer_payee_info.holder)
        self.assertEqual('IT28X0300203280418743513827', resp_create_message.wiretransfer_payee_info.holder_iban)

        self._create_fake_money_transfer(
            resp_create_message.fiat_amount,
            user_external_reference_id,
            resp_create_message.wiretransfer_payee_info.description
        )
        msg = v1_pb2.MsgPayForBidUsingWiretransfer(
            bid_id=resp_create_message.bid_id
        )
        crypto_proof = v1_pb2.PayForBidWTCryptoRequest()
        crypto_proof.proofID = str(uuid.uuid4())
        crypto_proof.proofExpiration = int(time.time() * 1000) + ONE_MINUTE
        data = '|'.join(
            (
                crypto_proof.proofID,
                'PAY_FOR_BID_WT',
                msg.bid_id,
                user_external_reference_id,
                str(crypto_proof.proofExpiration)
            )
        )
        data = unicodedata.normalize('NFC', data).encode('utf-8')
        crypto_proof.cryptoProof = rsa.pkcs1.sign(data, HYPE_PRIVATE_KEY, 'SHA-256')
        msg.crypto_proof.MergeFrom(crypto_proof)

        resp = self.client.post(
            '/v1/bids/pay_wt', data=msg.SerializeToString(),
            headers=headers)
        self.assertEqual(expected_pay_response, resp.status_code,
                         msg='Error: {} ({})'.format(resp.status_code, resp.content))
        if expected_pay_response != 200:
            return resp
        resp_pay_message = v1_pb2.MsgPayForBidUsingWiretransferResponse()
        resp_pay_message.ParseFromString(base64.b64decode(resp.content))

        msg = v1_pb2.MsgFinalizePaymentForBidUsingWiretransferResponse(
            bid_id=resp_pay_message.bid_id
        )

        resp = self.client.post(
            '/v1/bids/finalize_wt', data=msg.SerializeToString(),
            headers=headers)
        self.assertEqual(200, resp.status_code, msg=resp.content)
        resp_pay_message = v1_pb2.MsgFinalizePaymentForBidUsingWiretransferResponse()
        resp_pay_message.ParseFromString(base64.b64decode(resp.content))
        return resp_pay_message.bid_id

    def sell_btc(
            self, authentication_headers: AuthenticationHeaders,
            bip32_private_key: BIP32PrivateKey,
            satoshi_amount: typing.Optional[Satoshi] = None,
            fiat_amount: typing.Optional[decimal.Decimal] = None,
            refresh_ask: bool = False,
            expected_create_response: int = 200,
            expected_pay_response: int = 200):
        self.assertFalse(bool(satoshi_amount) and bool(fiat_amount))

        if refresh_ask:
            msg = v1_pb2.MsgCreateOrRefreshAsk(currency=v1_pb2.EUR)
            if satoshi_amount:
                msg.satoshi = satoshi_amount // 2
            else:
                msg.fiat_amount = float(fiat_amount) / 2
            resp = self.client.post(
                '/v1/asks/create', data=msg.SerializeToString(),
                headers=authentication_headers._asdict())
            self.assertEqual(200, resp.status_code)
            resp_create_message = v1_pb2.MsgCreateOrRefreshAskResponse()
            resp_create_message.ParseFromString(base64.b64decode(resp.content))
            self.assertTrue(resp_create_message.miner_fee > 0, str(resp_create_message))
            ask_id = resp_create_message.ask_id
        else:
            ask_id = None

        msg = v1_pb2.MsgCreateOrRefreshAsk(currency=v1_pb2.EUR)
        if ask_id:
            msg.ask_id = ask_id
        if satoshi_amount:
            msg.satoshi = satoshi_amount
        elif fiat_amount:
            msg.fiat_amount = float(fiat_amount)
        resp = self.client.post(
            '/v1/asks/create', data=msg.SerializeToString(),
            headers=authentication_headers._asdict())
        self.assertEqual(expected_create_response, resp.status_code)
        if expected_create_response != 200:
            return resp
        resp_create_message = v1_pb2.MsgCreateOrRefreshAskResponse()
        resp_create_message.ParseFromString(base64.b64decode(resp.content))
        if expected_pay_response == 200:
            self.assertTrue(resp_create_message.miner_fee > 0, str(resp_create_message))

        msg = v1_pb2.MsgPayForAsk(ask_id=resp_create_message.ask_id)
        resp = self.client.post(
            '/v1/asks/pay', data=msg.SerializeToString(),
            headers=authentication_headers._asdict())
        self.assertEqual(expected_pay_response, resp.status_code, msg='Error: {} ({})'.format(resp.status_code, resp.content))
        if expected_pay_response != 200:
            return resp
        resp_pay_message = v1_pb2.MsgPayForAskResponse()
        resp_pay_message.ParseFromString(base64.b64decode(resp.content))

        msg = v1_pb2.MsgFinalizePaymentForAsk(
            ask_id=resp_pay_message.ask_id,
            tx=resp_pay_message.tx,
            date=resp_pay_message.date,
            request_signature=resp_pay_message.request_signature
        )
        msg.signatures.extend(
            [
                v1_pb2.Signature(
                    path=tsd.path,
                    signature=self.sign_with_derived_key(bip32_private_key, tsd.path, tsd.data_to_sign)
                )
                for tsd in resp_pay_message.sign_data
            ]
        )
        resp = self.client.post(
            '/v1/asks/finalize', data=msg.SerializeToString(),
            headers=authentication_headers._asdict())
        self.assertEqual(200, resp.status_code)
        resp_pay_message = v1_pb2.MsgFinalizePaymentForAskResponse()
        resp_pay_message.ParseFromString(base64.b64decode(resp.content))
        return resp_pay_message.ask_id

    @classmethod
    def charge_bitcoinrpc_wallet(cls):
        def _get_bitcoind_balance() -> float:
            return float(json.loads(
                ioc_core_wallet.core_wallet_client.bitcoinrpc('getwalletinfo').decode()
            )['response']['balance'])

        if _get_bitcoind_balance() < 1:
            ioc_core_wallet.core_wallet_client.bitcoinrpc('generate', 106)

    @classmethod
    def charge_wallet(cls, wallet_id: str, btc_amount: float) -> None:
        ioc_core_wallet.core_wallet_client.bitcoinrpc(
            'sendtoaddress', ioc_core_wallet.core_wallet_client.get_wallet_info(wallet_id).recv_address,
            btc_amount
        )

    @classmethod
    def charge_deposit_wallet(cls):
        cls.charge_wallet(ioc_bittrade.bittrade_client.get_deposit_wallet().wallet_id, 1)

    @classmethod
    def ensure_deposit_wallet(cls):
        @retrying.retry(
            wait_exponential_multiplier=200,
            wait_exponential_max=2000, stop_max_delay=60000
        )
        def _check_confirmed_balance():
            wallet_info = ioc_core_wallet.core_wallet_client.get_wallet_info(
                ioc_bittrade.bittrade_client.get_deposit_wallet().wallet_id,
                invalidate_cache=True
            )
            if wallet_info.balance < 100000000:
                raise ValueError
            wallet_info = ioc_core_wallet.core_wallet_client.get_wallet_info(
                ioc_bittrade.bittrade_client.get_deposit_wallet().wallet_id,
                invalidate_cache=False
            )
            if wallet_info.balance < 100000000:
                raise ValueError

        cur_wallet_info = ioc_core_wallet.core_wallet_client.get_wallet_info(
                ioc_bittrade.bittrade_client.get_deposit_wallet().wallet_id,
                invalidate_cache=True
        )
        if cur_wallet_info.balance >= 100000000:
            return

        if cur_wallet_info.incoming_balance >= 100000000:
            ioc_core_wallet.core_wallet_client.bitcoinrpc('generate', 6)
            _check_confirmed_balance()
            return

        cls.charge_bitcoinrpc_wallet()
        cls.charge_deposit_wallet()

        @retrying.retry(
            wait_exponential_multiplier=200,
            wait_exponential_max=2000, stop_max_delay=60000
        )
        def _check_unconfirmed_balance():
            wallet_info = ioc_core_wallet.core_wallet_client.get_wallet_info(
                ioc_bittrade.bittrade_client.get_deposit_wallet().wallet_id,
                invalidate_cache=True
            )
            if wallet_info.incoming_balance < 100000000:
                raise ValueError

        _check_unconfirmed_balance()
        ioc_core_wallet.core_wallet_client.bitcoinrpc('generate', 6)
        _check_confirmed_balance()

    def verify_limits(
            self, authentication_headers: AuthenticationHeaders,
            buy_limits: float = BUY_LIMIT, sell_limits: float = SELL_LIMIT):
        self._ensure_min_fiat_limits()
        if buy_limits < 0:
            buy_limits += BUY_LIMIT
        if sell_limits < 0:
            sell_limits += SELL_LIMIT

        resp = self.client.get(
            '/v1/trading_limits', headers=authentication_headers._asdict())
        self.assertEqual(200, resp.status_code)
        resp_msg = v1_pb2.MsgGetTradingLimitsResponse()
        resp_msg.ParseFromString(base64.b64decode(resp.content))
        
        self.assertEqual(
            1, len(resp_msg.buy_fiat_limits.current_limits_by_type),
            str(resp_msg.buy_fiat_limits.current_limits_by_type))
        self.assertEqual(
            1, len(resp_msg.sell_fiat_limits.current_limits_by_type), 
            str(resp_msg.sell_fiat_limits.current_limits_by_type))
        self.assertGreater(resp_msg.buy_min_fiat_limits, 0)
        self.assertGreater(resp_msg.sell_min_fiat_limits, 0)
        self.assertGreater(resp_msg.sell_fiat_limits.current_limit, 0)
        self.assertGreater(resp_msg.buy_fiat_limits.current_limit, 0)

        self.assertAlmostEqual(buy_limits, resp_msg.buy_fiat_limits.current_limit, delta=0.01)
        self.assertAlmostEqual(sell_limits, resp_msg.sell_fiat_limits.current_limit, delta=0.01)
        
        self.assertAlmostEqual(buy_limits, resp_msg.buy_fiat_limits.current_limits_by_type[0].limit, delta=0.01)
        self.assertAlmostEqual(sell_limits, resp_msg.sell_fiat_limits.current_limits_by_type[0].limit, delta=0.01)
        
        self.assertEqual(v1_pb2.DAILY, resp_msg.buy_fiat_limits.current_limits_by_type[0].limit_type)

        self.assertEqual(v1_pb2.DAILY, resp_msg.sell_fiat_limits.current_limits_by_type[0].limit_type)

    @classmethod
    def get_credit_card_payment_methods(cls, payer_id: str) -> typing.Sequence[base_pb2.MsgPaymentMethod]:
        return list(
                filter(
                    lambda p: p.family != 'wiretransfer',
                    ioc_cards.cards_client.get_payment_methods(
                        base_pb2.MsgGetPaymentMethods(payer_id=payer_id)
                    ).data.payment_methods
                )
            )

    @classmethod
    def get_wiretransfer_payment_methods(cls, payer_id: str) -> typing.Sequence[base_pb2.MsgPaymentMethod]:
        return list(
            filter(
                lambda p: p.family == 'wiretransfer',
                ioc_cards.cards_client.get_payment_methods(
                    base_pb2.MsgGetPaymentMethods(payer_id=payer_id)
                ).data.payment_methods
            )
        )

    def _ensure_min_fiat_limits(self):
        resp = safe_requests.get(
            urllib.parse.urljoin(settings.BITTRADE_URL, 'trading_settings'),
            hmac_key=settings.BITTRADE_SECRET_KEY
        )
        self.assertEqual(200, resp.status_code)
        trading_settings = bittrade_pb2.TradingSettings()
        trading_settings.ParseFromString(resp.content)
        changed = False
        min_bid = min_ask = False
        for t in trading_settings.trading_min_fiat:
            min_bid |= t.trading_min_fiat_type == bittrade_pb2.MIN_FIAT_TYPE_BID
            min_ask |= t.trading_min_fiat_type == bittrade_pb2.MIN_FIAT_TYPE_ASK
            if t.value.fiat_value == 0:
                t.value.fiat_value = 1000
                t.value.decimal_digits = 2
                changed = True

        if not min_bid:
            trading_settings.trading_min_fiat.add(
                currency='EUR',
                value=bittrade_pb2.FiatAmount(
                    fiat_value=1000,
                    decimal_digits=2
                ),
                trading_min_fiat_type=bittrade_pb2.MIN_FIAT_TYPE_BID
            )
            changed = True

        if not min_ask:
            trading_settings.trading_min_fiat.add(
                currency='EUR',
                value=bittrade_pb2.FiatAmount(
                    fiat_value=1000,
                    decimal_digits=2
                ),
                trading_min_fiat_type=bittrade_pb2.MIN_FIAT_TYPE_ASK
            )
            changed = True

        if changed:
            resp = safe_requests.put(
                urllib.parse.urljoin(settings.BITTRADE_URL, 'trading_settings'),
                data=trading_settings.SerializeToString(),
                hmac_key=settings.BITTRADE_SECRET_KEY
            )
            self.assertEqual(200, resp.status_code)

    def _checkUSER_LEVEL(self, external_user_id: str):
        user = ioc_vault.vault_client.get_user_info_by_attributes(external_reference=external_user_id)

        level = set(
            map(
                lambda limit: limit.description,
                ioc_cards.cards_client.get_fiat_limits(
                    base_pb2.MsgGetFiatLimits(
                        payer_id=user.cards_integration_id,
                        currency=base_pb2.EUR
                    )
                ).data.all_limits
            )
        )
        self.assertEqual(1, len(level))
        level = next(iter(level))
        self.assertEqual(USER_LEVEL, level)
        user_explicit_fees_ids = user.explicit_fees_ids
        explicit_fees_level = set(
            map(
                lambda explicit_fee: explicit_fee.description,
                filter(
                    lambda explicit_fee_2: explicit_fee_2.explicit_fees_id in user_explicit_fees_ids,
                    ioc_bittrade.bittrade_client.get_explicit_fees()
                )
            )
        )
        explicit_fees_level = next(iter(explicit_fees_level))
        self.assertEqual(USER_LEVEL, explicit_fees_level)

    @classmethod
    def _create_fake_money_transfer(
            cls, amount: decimal.Decimal,
            external_user_id: str,
            description: str) -> dict:
        iban = ioc_cards.cards_client.get_wiretransfer_payee_info().iban
        payer_id = ioc_vault.vault_client.get_user_info_by_attributes(
            external_reference=external_user_id
        ).cards_integration_id
        debtor_iban = next(filter(
            lambda d: d.family == 'wiretransfer',
            ioc_cards.cards_client.get_payment_methods(
                base_pb2.MsgGetPaymentMethods(payer_id=payer_id)
            ).data.payment_methods
        )
        ).iban_data.iban
        debtor = 'Unrelevant debtor'
        resp = requests.post(
            'http://cards_flask:8080/test/create_wiretransfer',
            json={
                'iban': iban,
                'amount': float(amount),
                'debtor': debtor,
                'debtor_iban': debtor_iban,
                'description': description
            }
        )
        resp.raise_for_status()
        return resp.json()['payload']