# pragma pylint: disable=missing-docstring, C0103, protected-access, unused-argument

from unittest.mock import MagicMock

import pytest
from requests.exceptions import ContentDecodingError

import freqtrade.exchange.bittrex as btx
from freqtrade.exchange.bittrex import Bittrex


# Eat this flake8
# +------------------+
# |  bittrex.Bittrex |
# +------------------+
#         |
#       (mock Fake_bittrex)
#         |
# +-----------------------------+
# |  freqtrade.exchange.Bittrex |
# +-----------------------------+
# Call into Bittrex will flow up to the
# external package bittrex.Bittrex.
# By inserting a mock, we redirect those
# calls.
# The faked bittrex API is called just 'fb'
# The freqtrade.exchange.Bittrex is a
# wrapper, and is called 'wb'


def _stub_config():
    return {'key': '',
            'secret': ''}


class FakeBittrex():
    def __init__(self, success=True):
        self.success = True  # Believe in yourself
        self.result = None
        self.get_ticker_call_count = 0
        # This is really ugly, doing side-effect during instance creation
        # But we're allowed to in testing-code
        btx._API = MagicMock()
        btx._API.buy_limit = self.fake_buysell_limit
        btx._API.sell_limit = self.fake_buysell_limit
        btx._API.get_balance = self.fake_get_balance
        btx._API.get_balances = self.fake_get_balances
        btx._API.get_ticker = self.fake_get_ticker
        btx._API.get_order = self.fake_get_order
        btx._API.cancel = self.fake_cancel_order
        btx._API.get_markets = self.fake_get_markets
        btx._API.get_market_summaries = self.fake_get_market_summaries
        btx._API_V2 = MagicMock()
        btx._API_V2.get_candles = self.fake_get_candles
        btx._API_V2.get_wallet_health = self.fake_get_wallet_health

    def fake_buysell_limit(self, pair, amount, limit):
        return {'success': self.success,
                'result': {'uuid': '1234'},
                'message': 'barter'}

    def fake_get_balance(self, cur):
        return {'success': self.success,
                'result': {'Balance': 1234},
                'message': 'unbalanced'}

    def fake_get_balances(self):
        return {'success': self.success,
                'result': [{'BTC_ETH': 1234}],
                'message': 'no balances'}

    def fake_get_ticker(self, pair):
        self.get_ticker_call_count += 1
        return self.result or {'success': self.success,
                               'result': {'Bid': 1, 'Ask': 1, 'Last': 1},
                               'message': 'NO_API_RESPONSE'}

    def fake_get_candles(self, pair, interval):
        return self.result or {'success': self.success,
                               'result': [{'C': 0, 'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}],
                               'message': 'candles lit'}

    def fake_get_order(self, uuid):
        return {'success': self.success,
                'result': {'OrderUuid': 'ABC123',
                           'Type': 'Type',
                           'Exchange': 'BTC_ETH',
                           'Opened': True,
                           'PricePerUnit': 1,
                           'Quantity': 1,
                           'QuantityRemaining': 1,
                           'Closed': True},
                'message': 'lost'}

    def fake_cancel_order(self, uuid):
        return self.result or {'success': self.success,
                               'message': 'no such order'}

    def fake_get_markets(self):
        return self.result or {'success': self.success,
                               'message': 'market gone',
                               'result': [{'MarketName': '-_'}]}

    def fake_get_market_summaries(self):
        return self.result or {'success': self.success,
                               'message': 'no summary',
                               'result': ['sum']}

    def fake_get_wallet_health(self):
        return self.result or {'success': self.success,
                               'message': 'bad health',
                               'result': [{'Health': {'Currency': 'BTC_ETH',
                                                      'IsActive': True,
                                                      'LastChecked': 0},
                                           'Currency': {'Notice': True}}]}


# The freqtrade.exchange.bittrex is called wrap_bittrex
# to not confuse naming with bittrex.bittrex
def make_wrap_bittrex():
    conf = _stub_config()
    wb = btx.Bittrex(conf)
    return wb


def test_exchange_bittrex_class():
    conf = _stub_config()
    b = Bittrex(conf)
    assert isinstance(b, Bittrex)
    slots = dir(b)
    for name in ['fee', 'buy', 'sell', 'get_balance', 'get_balances',
                 'get_ticker', 'get_ticker_history', 'get_order',
                 'cancel_order', 'get_pair_detail_url', 'get_markets',
                 'get_market_summaries', 'get_wallet_health']:
        assert name in slots
        # FIX: ensure that the slot is also a method in the class
        # getattr(b, name) => bound method Bittrex.buy
        # type(getattr(b, name)) => class 'method'


def test_exchange_bittrex_fee():
    fee = Bittrex.fee.__get__(Bittrex)
    assert fee >= 0 and fee < 0.1  # Fee is 0-10 %


def test_exchange_bittrex_buy_good():
    wb = make_wrap_bittrex()
    fb = FakeBittrex()
    uuid = wb.buy('BTC_ETH', 1, 1)
    assert uuid == fb.fake_buysell_limit(1, 2, 3)['result']['uuid']

    fb.success = False
    with pytest.raises(btx.OperationalException, match=r'barter.*'):
        wb.buy('BAD', 1, 1)


def test_exchange_bittrex_sell_good():
    wb = make_wrap_bittrex()
    fb = FakeBittrex()
    uuid = wb.sell('BTC_ETH', 1, 1)
    assert uuid == fb.fake_buysell_limit(1, 2, 3)['result']['uuid']

    fb.success = False
    with pytest.raises(btx.OperationalException, match=r'barter.*'):
        uuid = wb.sell('BAD', 1, 1)


def test_exchange_bittrex_get_balance():
    wb = make_wrap_bittrex()
    fb = FakeBittrex()
    bal = wb.get_balance('BTC_ETH')
    assert bal == fb.fake_get_balance(1)['result']['Balance']

    fb.success = False
    with pytest.raises(btx.OperationalException, match=r'unbalanced'):
        wb.get_balance('BTC_ETH')


def test_exchange_bittrex_get_balances():
    wb = make_wrap_bittrex()
    fb = FakeBittrex()
    bals = wb.get_balances()
    assert bals == fb.fake_get_balances()['result']

    fb.success = False
    with pytest.raises(btx.OperationalException, match=r'no balances'):
        wb.get_balances()


def test_exchange_bittrex_get_ticker():
    wb = make_wrap_bittrex()
    fb = FakeBittrex()

    # Poll ticker, which updates the cache
    tick = wb.get_ticker('BTC_ETH')
    for x in ['bid', 'ask', 'last']:
        assert x in tick
    # Ensure the side-effect was made (update the ticker cache)
    assert 'BTC_ETH' in wb.cached_ticker.keys()

    # taint the cache, so we can recognize the cache wall utilized
    wb.cached_ticker['BTC_ETH']['bid'] = 1234
    # Poll again, getting the cached result
    fb.get_ticker_call_count = 0
    tick = wb.get_ticker('BTC_ETH', False)
    # Ensure the result was from the cache, and that we didn't call exchange
    assert wb.cached_ticker['BTC_ETH']['bid'] == 1234
    assert fb.get_ticker_call_count == 0


def test_exchange_bittrex_get_ticker_bad():
    wb = make_wrap_bittrex()
    fb = FakeBittrex()
    fb.result = {'success': True, 'result': {'Bid': 1, 'Ask': 0}}  # incomplete result

    with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'):
        wb.get_ticker('BTC_ETH')
    fb.result = {'success': False, 'message': 'gone bad'}
    with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'):
        wb.get_ticker('BTC_ETH')

    fb.result = {'success': True, 'result': {}}  # incomplete result
    with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'):
        wb.get_ticker('BTC_ETH')
    fb.result = {'success': False, 'message': 'gone bad'}
    with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'):
        wb.get_ticker('BTC_ETH')

    fb.result = {'success': True,
                 'result': {'Bid': 1, 'Ask': 0, 'Last': None}}  # incomplete result
    with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'):
        wb.get_ticker('BTC_ETH')


def test_exchange_bittrex_get_ticker_history_intervals():
    wb = make_wrap_bittrex()
    FakeBittrex()
    for tick_interval in [1, 5, 30, 60, 1440]:
        assert ([{'C': 0, 'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}] ==
                wb.get_ticker_history('BTC_ETH', tick_interval))


def test_exchange_bittrex_get_ticker_history():
    wb = make_wrap_bittrex()
    fb = FakeBittrex()
    assert wb.get_ticker_history('BTC_ETH', 5)
    with pytest.raises(ValueError, match=r'.*Unknown tick_interval.*'):
        wb.get_ticker_history('BTC_ETH', 2)

    fb.success = False
    with pytest.raises(btx.OperationalException, match=r'candles lit.*'):
        wb.get_ticker_history('BTC_ETH', 5)

    fb.success = True
    with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex.*'):
        fb.result = {'bad': 0}
        wb.get_ticker_history('BTC_ETH', 5)

    with pytest.raises(ContentDecodingError, match=r'.*Required property C not present.*'):
        fb.result = {'success': True,
                     'result': [{'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}],  # close is missing
                     'message': 'candles lit'}
        wb.get_ticker_history('BTC_ETH', 5)


def test_exchange_bittrex_get_order():
    wb = make_wrap_bittrex()
    fb = FakeBittrex()
    order = wb.get_order('someUUID')
    assert order['id'] == 'ABC123'
    fb.success = False
    with pytest.raises(btx.OperationalException, match=r'lost'):
        wb.get_order('someUUID')


def test_exchange_bittrex_cancel_order():
    wb = make_wrap_bittrex()
    fb = FakeBittrex()
    wb.cancel_order('someUUID')
    with pytest.raises(btx.OperationalException, match=r'no such order'):
        fb.success = False
        wb.cancel_order('someUUID')
    # Note: this can be a bug in exchange.bittrex._validate_response
    with pytest.raises(KeyError):
        fb.result = {'success': False}  # message is missing!
        wb.cancel_order('someUUID')
    with pytest.raises(btx.OperationalException, match=r'foo'):
        fb.result = {'success': False, 'message': 'foo'}
        wb.cancel_order('someUUID')


def test_exchange_get_pair_detail_url():
    wb = make_wrap_bittrex()
    assert wb.get_pair_detail_url('BTC_ETH')


def test_exchange_get_markets():
    wb = make_wrap_bittrex()
    fb = FakeBittrex()
    x = wb.get_markets()
    assert x == ['__']
    with pytest.raises(btx.OperationalException, match=r'market gone'):
        fb.success = False
        wb.get_markets()


def test_exchange_get_market_summaries():
    wb = make_wrap_bittrex()
    fb = FakeBittrex()
    assert ['sum'] == wb.get_market_summaries()
    with pytest.raises(btx.OperationalException, match=r'no summary'):
        fb.success = False
        wb.get_market_summaries()


def test_exchange_get_wallet_health():
    wb = make_wrap_bittrex()
    fb = FakeBittrex()
    x = wb.get_wallet_health()
    assert x[0]['Currency'] == 'BTC_ETH'
    with pytest.raises(btx.OperationalException, match=r'bad health'):
        fb.success = False
        wb.get_wallet_health()


def test_validate_response_success():
    response = {
        'message': '',
        'result': [],
    }
    Bittrex._validate_response(response)


def test_validate_response_no_api_response():
    response = {
        'message': 'NO_API_RESPONSE',
        'result': None,
    }
    with pytest.raises(ContentDecodingError, match=r'.*NO_API_RESPONSE.*'):
        Bittrex._validate_response(response)


def test_validate_response_min_trade_requirement_not_met():
    response = {
        'message': 'MIN_TRADE_REQUIREMENT_NOT_MET',
        'result': None,
    }
    with pytest.raises(ContentDecodingError, match=r'.*MIN_TRADE_REQUIREMENT_NOT_MET.*'):
        Bittrex._validate_response(response)