import os
import sys

root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
sys.path.append(root)

# ----------------------------------------------------------------------------

# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code

# ----------------------------------------------------------------------------
# -*- coding: utf-8 -*-

from ccxt.base.decimal_to_precision import TICK_SIZE  # noqa E402
from ccxt.base.decimal_to_precision import TRUNCATE  # noqa E402
from ccxt.base.decimal_to_precision import ROUND  # noqa E402
from ccxt.base.decimal_to_precision import ROUND_UP  # noqa E402
from ccxt.base.decimal_to_precision import decimal_to_precision  # noqa E402
from ccxt.base.decimal_to_precision import number_to_string  # noqa E402
from ccxt.base.precise import Precise  # noqa E402
from ccxt.test.exchange.base import test_shared_methods  # noqa E402
from ccxt.test.exchange.base import test_order  # noqa E402

# ----------------------------------------------------------------------------
def tco_debug(exchange, symbol, message):
    # just for debugging purposes
    debug_create_order = True
    if debug_create_order:
        # for c# fix, extra step to convert them to string
        print(' >>>>> testCreateOrder [', str((exchange['id'])), ' : ', symbol, '] ', message)
    return True


# ----------------------------------------------------------------------------
def test_create_order(exchange, skipped_properties, symbol):
    log_prefix = test_shared_methods.log_template(exchange, 'createOrder', [symbol])
    assert exchange.has['cancelOrder'] or exchange.has['cancelOrders'] or exchange.has['cancelAllOrders'], log_prefix + ' does not have cancelOrder|cancelOrders|canelAllOrders method, which is needed to make tests for `createOrder` method. Skipping the test...'
    # pre-define some coefficients, which will be used down below
    limit_price_safety_multiplier_from_median = 1.045  # todo: when this https://github.com/ccxt/ccxt/issues/22442 is implemented, we'll remove hardcoded value. atm 5% is enough
    market = exchange.market(symbol)
    is_swap_future = market['swap'] or market['future']
    assert exchange.has['fetchBalance'], log_prefix + ' does not have fetchBalance() method, which is needed to make tests for `createOrder` method. Skipping the test...'
    balance = exchange.fetch_balance()
    initial_base_balance = balance[market['base']]['free']
    initial_quote_balance = balance[market['quote']]['free']
    assert initial_quote_balance is not None, log_prefix + ' - testing account not have balance of' + market['quote'] + ' in fetchBalance() which is required to test'
    tco_debug(exchange, symbol, 'fetched balance for ' + symbol + ' : ' + str(initial_base_balance) + ' ' + market['base'] + '/' + initial_quote_balance + ' ' + market['quote'])
    [best_bid, best_ask] = test_shared_methods.fetch_best_bid_ask(exchange, 'createOrder', symbol)
    # **************** [Scenario 1 - START] **************** #
    tco_debug(exchange, symbol, '### SCENARIO 1 ###')
    # create a "limit order" which IS GUARANTEED not to have a fill (i.e. being far from the real price)
    tco_create_unfillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, 'buy', None)
    if is_swap_future:
        # for swap markets, we test sell orders too
        tco_create_unfillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, 'sell', None)
    tco_debug(exchange, symbol, '### SCENARIO 1 PASSED ###')
    # **************** [Scenario 2 - START] **************** #
    tco_debug(exchange, symbol, '### SCENARIO 2 ###')
    # create an order which IS GUARANTEED to have a fill (full or partial)
    tco_create_fillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, 'buy', None)
    if is_swap_future:
        # for swap markets, we test sell orders too
        tco_create_fillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, 'sell', None)
    tco_debug(exchange, symbol, '### SCENARIO 2 PASSED ###')
    # **************** [Scenario 3 - START] **************** #
    return True


# ----------------------------------------------------------------------------
def tco_create_unfillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, buy_or_sell, predefined_amount=None):
    try:
        symbol = market['symbol']
        minimun_prices = exchange.safe_dict(market['limits'], 'price', {})
        minimum_price = minimun_prices['min']
        maximum_price = minimun_prices['max']
        # below we set limit price, where the order will not be completed.
        # We do not use the extreme "limits" values for that market, because, even though min purchase amount for BTC/USDT can be 0.01 BTC, it means with 10$ you can buy 1000 BTC, which leads to unrealistic outcome. So, we just use around 5%-10% far price from the current price.
        limit_buy_price_non_fillable = best_bid / limit_price_safety_multiplier_from_median
        if minimum_price is not None and limit_buy_price_non_fillable < minimum_price:
            limit_buy_price_non_fillable = minimum_price
        limit_sell_price_non_fillable = best_ask * limit_price_safety_multiplier_from_median
        if maximum_price is not None and limit_sell_price_non_fillable > maximum_price:
            limit_sell_price_non_fillable = maximum_price
        created_order = None
        if buy_or_sell == 'buy':
            order_amount = tco_get_minimum_amount_for_limit_price(exchange, market, limit_buy_price_non_fillable, predefined_amount)
            created_order = tco_create_order_safe(exchange, symbol, 'limit', 'buy', order_amount, limit_buy_price_non_fillable, {}, skipped_properties)
        else:
            order_amount = tco_get_minimum_amount_for_limit_price(exchange, market, limit_sell_price_non_fillable, predefined_amount)
            created_order = tco_create_order_safe(exchange, symbol, 'limit', 'sell', order_amount, limit_sell_price_non_fillable, {}, skipped_properties)
        fetched_order = test_shared_methods.fetch_order(exchange, symbol, created_order['id'], skipped_properties)
        # test fetched order object
        if fetched_order is not None:
            test_order(exchange, skipped_properties, 'createOrder', fetched_order, symbol, exchange.milliseconds())
        # ensure that order is not filled
        test_shared_methods.assert_order_state(exchange, skipped_properties, 'createdOrder', created_order, 'open', False)
        test_shared_methods.assert_order_state(exchange, skipped_properties, 'fetchedOrder', fetched_order, 'open', True)
        # ensure that order side matches
        test_shared_methods.assert_in_array(exchange, skipped_properties, 'createdOrder', created_order, 'side', [None, buy_or_sell])
        test_shared_methods.assert_in_array(exchange, skipped_properties, 'fetchedOrder', fetched_order, 'side', [None, buy_or_sell])
        tco_cancel_order(exchange, symbol, created_order['id'])
    except Exception as e:
        raise Error(log_prefix + ' failed for Scenario 1: ' + str(e))
    return True


def tco_create_fillable_order(exchange, market, log_prefix, skipped_properties, best_bid, best_ask, limit_price_safety_multiplier_from_median, buy_or_sell_string, predefined_amount=None):
    try:
        is_swap_future = market['swap'] or market['future']
        is_buy = (buy_or_sell_string == 'buy')
        entry_side = 'buy' if is_buy else 'sell'
        exit_side = 'sell' if is_buy else 'buy'
        entryorder_price = best_ask * limit_price_safety_multiplier_from_median if is_buy else best_bid / limit_price_safety_multiplier_from_median
        exitorder_price = best_bid / limit_price_safety_multiplier_from_median if is_buy else best_ask * limit_price_safety_multiplier_from_median  # todo revise: (tcoMininumCost (exchange, market) / amountToClose) / limitPriceSafetyMultiplierFromMedian;
        #
        #
        symbol = market['symbol']
        entry_amount = tco_get_minimum_amount_for_limit_price(exchange, market, entryorder_price)
        entryorder_filled = tco_create_order_safe(exchange, symbol, 'limit', entry_side, entry_amount, entryorder_price, {}, skipped_properties)
        # just for case, cancel any possible unfilled amount (though it is not be expected because the order was fillable)
        tco_try_cancel_order(exchange, symbol, entryorder_filled, skipped_properties)
        # now, as order is closed/canceled, we can reliably fetch the order information
        entryorder_fetched = test_shared_methods.fetch_order(exchange, symbol, entryorder_filled['id'], skipped_properties)
        tco_assert_filled_order(exchange, market, log_prefix, skipped_properties, entryorder_filled, entryorder_fetched, entry_side, entry_amount)
        #
        # ### close the traded position ###
        #
        amount_to_close = exchange.parse_to_numeric(exchange.safe_string(entryorder_fetched, 'filled'))
        params = {}
        # as we want to close position, we should use 'reduceOnly' to ensure we don't open a margined position accidentally, because some exchanges might have automatically enabled margin-mode (on spot) or hedge-mode (on contracts)
        if is_swap_future:
            params['reduceOnly'] = True
        exitorder_filled = tco_create_order_safe(exchange, symbol, 'market', exit_side, amount_to_close, (None if market['spot'] else exitorder_price), params, skipped_properties)
        exitorder_fetched = test_shared_methods.fetch_order(exchange, symbol, exitorder_filled['id'], skipped_properties)
        tco_assert_filled_order(exchange, market, log_prefix, skipped_properties, exitorder_filled, exitorder_fetched, exit_side, amount_to_close)
    except Exception as e:
        raise Error('failed for Scenario 2: ' + str(e))
    return True


def tco_assert_filled_order(exchange, market, log_prefix, skipped_properties, created_order, fetched_order, requested_side, requested_amount):
    # test filled amount
    precision_amount = exchange.safe_string(market['precision'], 'amount')
    entryorder_amount_string = exchange.number_to_string(requested_amount)
    filled_string = exchange.safe_string(fetched_order, 'filled')
    assert filled_string is not None, log_prefix + ' order should be filled, but it is not. ' + exchange.json(fetched_order)
    # filled amount should be whithin the expected range i.e. if you buy 100 DOGECOIN and amount-precision is 1,
    # and also considering possible roundings in implementation, then filled amount should be between 99 and 101
    max_expected_filled_amount = Precise.string_add(entryorder_amount_string, precision_amount)
    min_expected_filled_amount = Precise.string_sub(entryorder_amount_string, precision_amount)
    assert Precise.string_le(filled_string, max_expected_filled_amount), log_prefix + ' filled amount is more than expected, possibly some implementation issue. ' + exchange.json(fetched_order)
    assert Precise.string_ge(filled_string, min_expected_filled_amount), log_prefix + ' filled amount is less than expected, possibly some implementation issue. ' + exchange.json(fetched_order)
    # order state should be "closed"
    test_shared_methods.assert_order_state(exchange, skipped_properties, 'createdOrder', created_order, 'closed', False)
    test_shared_methods.assert_order_state(exchange, skipped_properties, 'fetchedOrder', fetched_order, 'closed', True)
    # ensure that order side matches
    test_shared_methods.assert_in_array(exchange, skipped_properties, 'createdOrder', created_order, 'side', [None, requested_side])
    test_shared_methods.assert_in_array(exchange, skipped_properties, 'fetchedOrder', fetched_order, 'side', [None, requested_side])
    return True


# ----------------------------------------------------------------------------
def tco_cancel_order(exchange, symbol, order_id=None):
    log_prefix = test_shared_methods.log_template(exchange, 'createOrder', [symbol])
    used_method = ''
    cancel_result = None
    if exchange.has['cancelOrder'] and order_id is not None:
        used_method = 'cancelOrder'
        cancel_result = exchange.cancel_order(order_id, symbol)
    elif exchange.has['cancelAllOrders']:
        used_method = 'cancelAllOrders'
        cancel_result = exchange.cancel_all_orders(symbol)
    elif exchange.has['cancelOrders']:
        raise Error(log_prefix + ' cancelOrders method is not unified yet, coming soon...')
    tco_debug(exchange, symbol, 'canceled order using ' + used_method + ':' + cancel_result['id'])
    # todo:
    # testSharedMethods.assertOrderState (exchange, skippedProperties, 'cancelOrder', cancelResult, 'canceled', false);
    # testSharedMethods.assertOrderState (exchange, skippedProperties, 'cancelOrder', cancelResult, 'closed', true);
    return True


# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
def tco_create_order_safe(exchange, symbol, order_type, side, amount, price=None, params={}, skipped_properties={}):
    tco_debug(exchange, symbol, 'Executing createOrder ' + order_type + ' ' + side + ' ' + amount + ' ' + price + ' ' + exchange.json(params))
    order = exchange.create_order(symbol, order_type, side, amount, price, params)
    try:
        test_order(exchange, skipped_properties, 'createOrder', order, symbol, int(time.time() * 1000))
    except Exception as e:
        if order_type != 'market':
            # if it was limit order, try to cancel it before exiting the script
            tco_try_cancel_order(exchange, symbol, order, skipped_properties)
        raise e
    return order


def tco_mininum_amount(exchange, market):
    amount_values = exchange.safe_dict(market['limits'], 'amount', {})
    amount_min = exchange.safe_number(amount_values, 'min')
    assert amount_min is not None, exchange.id + ' ' + market['symbol'] + ' can not determine minimum amount for order'
    return amount_min


def tco_mininum_cost(exchange, market):
    cost_values = exchange.safe_dict(market['limits'], 'cost', {})
    cost_min = exchange.safe_number(cost_values, 'min')
    assert cost_min is not None, exchange.id + ' ' + market['symbol'] + ' can not determine minimum cost for order'
    return cost_min


def tco_get_minimum_amount_for_limit_price(exchange, market, price, predefined_amount=None):
    # this method calculates the minimum realistic order amount:
    # at first it checks the "minimum hardcap limit" (i.e. 7 DOGE), however, if exchange also has "minimum cost" limits,
    # then we need to calculate the amount using cost, because of price is volatile, today's 7 DOGE cost could be 1$
    # but "minimum cost" requirement could be 5$ (which translates to 35 DOGE amount)
    minimum_amount = tco_mininum_amount(exchange, market)
    minimum_cost = tco_mininum_cost(exchange, market)
    final_amount = minimum_amount
    if minimum_cost is not None:
        if final_amount * price < minimum_cost:
            final_amount = minimum_cost / price
    if predefined_amount is not None:
        final_amount = max(final_amount, predefined_amount)
    # because it's possible that calculated value might get truncated down in "createOrder" (i.e. 0.129 -> 0.12), we should ensure that final amount * price would bypass minimum cost requirements, by adding the "minimum precision"
    amount_precision = exchange.safe_number(market['precision'], 'amount')
    is_tick_size_precision = exchange.precisionMode == 4
    if amount_precision is None:
        amount_precision = 1e-15  # todo: revise this for better way in future
    else:
        # todo: remove after TICK_SIZE unification
        if not is_tick_size_precision:
            amount_precision = 1 / math.pow(10, amount_precision)  # this converts DECIMAL_PRECISION into TICK_SIZE
    final_amount = final_amount + amount_precision
    final_amount = final_amount * 1.1  # add around 10% to ensure "cost" is enough
    final_amount = float(exchange.decimal_to_precision(final_amount, 2, market['precision']['amount'], exchange.precisionMode))  # 2 stands for ROUND_UP constant, 0 stands for TRUNCATE
    return final_amount


def tco_try_cancel_order(exchange, symbol, order, skipped_properties):
    order_fetched = test_shared_methods.fetch_order(exchange, symbol, order['id'], skipped_properties)
    needs_cancel = exchange.in_array(order_fetched['status'], ['open', 'pending', None])
    # if it was not reported as closed/filled, then try to cancel it
    if needs_cancel:
        tco_debug(exchange, symbol, 'trying to cancel the remaining amount of partially filled order...')
        try:
            tco_cancel_order(exchange, symbol, order['id'])
        except Exception as e:
            # order might have been closed/filled already, before 'cancelOrder' call reaches server, so it is tolerable, we don't throw exception
            tco_debug(exchange, symbol, ' a moment ago order was reported as pending, but could not be cancelled at this moment. Exception message: ' + str(e))
    else:
        tco_debug(exchange, symbol, 'order is already closed/filled, no need to cancel it')
    return True
