#  Drakkar-Software OctoBot-Trading
#  Copyright (c) Drakkar-Software, All rights reserved.
#
#  This library is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 3.0 of the License, or (at your option) any later version.
#
#  This library is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#  Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public
#  License along with this library.
import pytest
import mock

import trading_backend.exchanges
import octobot_commons.constants as commons_constants
import octobot_commons.configuration as commons_configuration
import octobot_trading.constants as constants
import octobot_trading.exchanges as exchanges
import octobot_trading.exchanges.util.exchange_util as exchange_util

from tests import event_loop
from tests.exchanges import MockedRestExchange, MockedAutoFillRestExchange
import octobot_tentacles_manager.api as api


@pytest.fixture
def tentacles_setup_config():
    setup_config = mock.Mock()
    setup_config.is_tentacle_activated = mock.Mock(return_value=True)
    return setup_config


@pytest.fixture()
def exchange_config():
    return {
        commons_constants.CONFIG_EXCHANGE_KEY: commons_configuration.encrypt("01234").decode(),
        commons_constants.CONFIG_EXCHANGE_SECRET: commons_configuration.encrypt("012345").decode()
    }


@pytest.fixture()
def supported_exchanges():
    return {
        "plop.exchange": exchanges.ExchangeDetails("id_plop", "name_plop", "url_plop", "api_plop", "logo_plop", True),
        "blip": exchanges.ExchangeDetails("id_blip", "name_blip", "url_blip", "api_blip", "logo_blip", True)
    }


@pytest.mark.asyncio
async def test_is_compatible_account_with_checked_exchange(exchange_config, tentacles_setup_config):
    with mock.patch.object(trading_backend.exchanges.Huobi, "is_valid_account",
                           mock.AsyncMock(return_value=(True, None))) as is_valid_account_mock:
        compatible, auth, error = await exchanges.is_compatible_account("huobi", exchange_config,
                                                                        tentacles_setup_config, False)
        assert compatible is True
        assert auth is True
        assert error is None
        is_valid_account_mock.assert_called_once()
    with mock.patch.object(trading_backend.exchanges.Huobi, "is_valid_account",
                           mock.AsyncMock(return_value=(False, "plop"))) as is_valid_account_mock:
        compatible, auth, error = await exchanges.is_compatible_account("huobi", exchange_config,
                                                                        tentacles_setup_config, False)
        # still True as on spot trading
        assert compatible is True
        assert auth is True
        assert error is None
        is_valid_account_mock.assert_called_once()
    exchange_config[commons_constants.CONFIG_EXCHANGE_TYPE] = commons_constants.CONFIG_EXCHANGE_FUTURE
    with mock.patch.object(trading_backend.exchanges.Huobi, "is_valid_account",
                           mock.AsyncMock(return_value=(False, "plop"))) as is_valid_account_mock:
        compatible, auth, error = await exchanges.is_compatible_account("huobi", exchange_config,
                                                                        tentacles_setup_config, False)
        assert compatible is False
        assert auth is True
        assert "plop" in error and len(error) > len("plop")
        is_valid_account_mock.assert_called_once()


def test_get_partners_explanation_message():
    assert ".cloud/en/guides" in exchanges.get_partners_explanation_message()


def test_log_time_sync_error():
    logger = mock.Mock()
    exchanges.log_time_sync_error(logger, "exchange_name", "error", "hello call")
    args = logger.error.call_args[0][0]
    assert "exchange_name".capitalize() in args
    assert "error" in args
    assert "hello call" in args
    assert ".cloud/en/guides" in args


@pytest.mark.asyncio
async def test_is_compatible_account_with_unchecked_exchange(exchange_config, tentacles_setup_config):
    compatible, auth, error = await exchanges.is_compatible_account("hitbtc", exchange_config, tentacles_setup_config,
                                                                    False)
    assert compatible is False
    assert auth is False
    assert isinstance(error, str)
    exchange_config[commons_constants.CONFIG_EXCHANGE_TYPE] = commons_constants.CONFIG_EXCHANGE_FUTURE
    with mock.patch.object(trading_backend.exchanges.Exchange, "is_valid_account",
                           mock.AsyncMock(return_value=(True, "plop"))) as is_valid_account_mock:
        compatible, auth, error = await exchanges.is_compatible_account("hitbtc", exchange_config,
                                                                        tentacles_setup_config, False)
        assert compatible is True
        assert auth is True
        assert "plop" in error and len(error) > len("plop")
        is_valid_account_mock.assert_called_once()
    with mock.patch.object(trading_backend.exchanges.Exchange, "is_valid_account",
                           mock.AsyncMock(side_effect=trading_backend.ExchangeAuthError)) as is_valid_account_mock:
        compatible, auth, error = await exchanges.is_compatible_account("hitbtc", exchange_config,
                                                                        tentacles_setup_config, False)
        assert compatible is False
        assert auth is False
        assert "authentication" in error and len(error) > len("authentication")
        is_valid_account_mock.assert_called_once()


def test_get_auto_filled_exchange_names(tentacles_setup_config, supported_exchanges):
    with mock.patch.object(api, "get_tentacle_config", mock.Mock()) as get_tentacle_config_mock:
        # no auto filled exchanges
        assert exchanges.get_auto_filled_exchange_names(tentacles_setup_config) == []
        get_tentacle_config_mock.assert_called_once_with(tentacles_setup_config, MockedAutoFillRestExchange)
        get_tentacle_config_mock.reset_mock()

        with MockedAutoFillRestExchange.patched_supported_exchanges(supported_exchanges):
            auto_filled_exchanges = exchanges.get_auto_filled_exchange_names(tentacles_setup_config)
            assert auto_filled_exchanges == list(supported_exchanges)
            assert "blip" in auto_filled_exchanges
            get_tentacle_config_mock.assert_called_once_with(tentacles_setup_config, MockedAutoFillRestExchange)


def test_get_exchange_class_from_name(tentacles_setup_config, supported_exchanges):
    # not found exchange
    assert exchanges.get_exchange_class_from_name(
        exchanges.RestExchange, "plop", tentacles_setup_config, True,
        strict_name_matching=False
    ) == exchanges.DefaultRestExchange
    with mock.patch.object(api, "get_tentacle_config", mock.Mock()) as get_tentacle_config_mock:
        assert exchanges.get_exchange_class_from_name(
            exchanges.RestExchange, "plop", tentacles_setup_config, False,
            strict_name_matching=False
        ) == MockedRestExchange
        get_tentacle_config_mock.assert_not_called()
        assert exchanges.get_exchange_class_from_name(
            exchanges.RestExchange, "plop", tentacles_setup_config, True,
            strict_name_matching=True
        ) is None
        get_tentacle_config_mock.assert_called_once()
        get_tentacle_config_mock.reset_mock()

    # regular exchange
    assert exchanges.get_exchange_class_from_name(
        exchanges.RestExchange, MockedRestExchange.get_name(), tentacles_setup_config, True,
        strict_name_matching=False
    ) == exchanges.DefaultRestExchange
    assert exchanges.get_exchange_class_from_name(
        exchanges.RestExchange, MockedRestExchange.get_name(), tentacles_setup_config, False,
        strict_name_matching=False
    ) == MockedRestExchange
    assert exchanges.get_exchange_class_from_name(
        exchanges.RestExchange, MockedRestExchange.get_name(), tentacles_setup_config, True,
        strict_name_matching=True
    ) == MockedRestExchange

    with mock.patch.object(api, "get_tentacle_config", mock.Mock()) as get_tentacle_config_mock:
        # auto-filled exchange
        with MockedAutoFillRestExchange.patched_supported_exchanges(supported_exchanges):
            assert exchanges.get_exchange_class_from_name(
                exchanges.RestExchange, MockedRestExchange.get_name(), tentacles_setup_config, True,
                strict_name_matching=False
            ) == exchanges.DefaultRestExchange
            assert exchanges.get_exchange_class_from_name(
                exchanges.RestExchange, MockedRestExchange.get_name(), tentacles_setup_config, False,
                strict_name_matching=False
            ) == MockedRestExchange
            assert exchanges.get_exchange_class_from_name(
                exchanges.RestExchange, MockedRestExchange.get_name(), tentacles_setup_config, True,
                strict_name_matching=True
            ) == MockedRestExchange

            get_tentacle_config_mock.assert_not_called()

            assert exchanges.get_exchange_class_from_name(
                exchanges.RestExchange, "blip", tentacles_setup_config, True,
                strict_name_matching=False
            ) == exchanges.DefaultRestExchange
            assert exchanges.get_exchange_class_from_name(
                exchanges.RestExchange, "blip", tentacles_setup_config, False,
                strict_name_matching=False
            ) == MockedRestExchange
            get_tentacle_config_mock.assert_not_called()
            assert exchanges.get_exchange_class_from_name(
                exchanges.RestExchange, "blip", tentacles_setup_config, True,
                strict_name_matching=True
            ) == MockedAutoFillRestExchange
            get_tentacle_config_mock.assert_called_once()


@pytest.mark.asyncio
async def test_get_exchange_details(tentacles_setup_config, supported_exchanges):
    with mock.patch.object(api, "get_tentacle_config", mock.Mock()) as get_tentacle_config_mock:
        # not found exchange
        with pytest.raises(KeyError):
            await exchanges.get_exchange_details(
                "blip", False, tentacles_setup_config, None
            )
        get_tentacle_config_mock.assert_not_called()
        with pytest.raises(KeyError):
            await exchanges.get_exchange_details(
                "blip", True, tentacles_setup_config, None
            )
        get_tentacle_config_mock.assert_called_once()
        get_tentacle_config_mock.reset_mock()

        # regular exchange
        details = await exchanges.get_exchange_details(
            "binance", False, tentacles_setup_config, None
        )
        assert details.id == "binance"
        assert details.name == "Binance"
        assert details.url == "https://www.binance.com"
        assert len(details.api) > 1
        assert "https://user-images.githubusercontent.com/" in details.logo_url
        assert details.has_websocket is False   # default value
        get_tentacle_config_mock.assert_not_called()

        # auto-filled exchange
        with MockedAutoFillRestExchange.patched_supported_exchanges(supported_exchanges):
            with pytest.raises(KeyError):
                await exchanges.get_exchange_details(
                    "blip", False, tentacles_setup_config, None
                )
            get_tentacle_config_mock.assert_not_called()
            details = await exchanges.get_exchange_details(
                "blip", True, tentacles_setup_config, None
            )
            assert details == supported_exchanges["blip"]
            get_tentacle_config_mock.assert_called_once()


def test_is_api_permission_error():
    with mock.patch.object(exchange_util, "is_error_on_this_type") as is_error_on_this_type_mock:
        err = Exception("plop")
        exchanges.is_api_permission_error(err)
        is_error_on_this_type_mock.assert_called_once_with(
            err, constants.EXCHANGE_PERMISSION_ERRORS
        )


def test_is_exchange_rules_compliancy_error():
    with mock.patch.object(exchange_util, "is_error_on_this_type") as is_error_on_this_type_mock:
        err = Exception("plop")
        exchanges.is_exchange_rules_compliancy_error(err)
        is_error_on_this_type_mock.assert_called_once_with(
            err, constants.EXCHANGE_COMPLIANCY_ERRORS
        )


def test_is_error_on_this_type():
    assert exchange_util.is_error_on_this_type(Exception("plop"), constants.EXCHANGE_PERMISSION_ERRORS) is False
    assert exchange_util.is_error_on_this_type(Exception("api key"), constants.EXCHANGE_PERMISSION_ERRORS) is True
    assert exchange_util.is_error_on_this_type(Exception("api"), constants.EXCHANGE_PERMISSION_ERRORS) is False
    assert exchange_util.is_error_on_this_type(Exception("api"), constants.EXCHANGE_COMPLIANCY_ERRORS) is False
