# Copyright 2022 Neal Chambers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Union

import pytest
from ccxt import binance, kraken
from prezzemolo.vertex import Vertex
from rp2.rp2_decimal import ZERO, RP2Decimal

from dali.abstract_pair_converter_plugin import AssetPairAndTimestamp, MappedGraph
from dali.cache import CACHE_DIR, load_from_cache
from dali.configuration import Keyword
from dali.historical_bar import HistoricalBar
from dali.plugin.pair_converter.ccxt import PairConverterPlugin
from dali.plugin.pair_converter.csv.kraken import Kraken as KrakenCsvPricing

# Default exchange
TEST_EXCHANGE: str = "Kraken"
ALT_EXCHANGE: str = "Binance.com"
LOCKED_EXCHANGE: str = "Kraken"
FIAT_EXHANGE: str = "fiat"
TEST_MARKETS: Dict[str, List[str]] = {
    "BTCUSDT": [ALT_EXCHANGE],
    "BTCGBP": [ALT_EXCHANGE],
    "BETHETH": [ALT_EXCHANGE],
    "ETHUSDT": [ALT_EXCHANGE],
    "USDTUSD": [TEST_EXCHANGE],
    "USDJPY": [FIAT_EXHANGE],
}
LOCKED_MARKETS: Dict[str, List[str]] = {
    "BTCUSDT": [TEST_EXCHANGE],
    "USDTUSD": [TEST_EXCHANGE],
}

# BTCUSDT conversion
BAR_DURATION: str = "1m"
BAR_TIMESTAMP: datetime = datetime.fromtimestamp(1504541580, timezone.utc)
BAR_LOW: RP2Decimal = RP2Decimal("4230.0")
BAR_HIGH: RP2Decimal = RP2Decimal("4240.6")
BAR_OPEN: RP2Decimal = RP2Decimal("4235.4")
BAR_CLOSE: RP2Decimal = RP2Decimal("4230.7")
BAR_VOLUME: RP2Decimal = RP2Decimal("37.72941911")

# USDTUSD conversion
USDTUSD_DURATION: str = "1m"
USDTUSD_TIMESTAMP: datetime = datetime.fromtimestamp(1504541580, timezone.utc)
USDTUSD_LOW: RP2Decimal = RP2Decimal("0.9987")
USDTUSD_HIGH: RP2Decimal = RP2Decimal("0.9988")
USDTUSD_OPEN: RP2Decimal = RP2Decimal("0.9987")
USDTUSD_CLOSE: RP2Decimal = RP2Decimal("0.9988")
USDTUSD_VOLUME: RP2Decimal = RP2Decimal("113.786789")

# Missing Fiat Test
JPY_USD_RATE: RP2Decimal = RP2Decimal("115")

# No Fiat pair conversion
BETHETH_TIMESTAMP: datetime = datetime.fromtimestamp(1504541590, timezone.utc)
BETHETH_LOW: RP2Decimal = RP2Decimal("0.9722")
BETHETH_HIGH: RP2Decimal = RP2Decimal("0.9739")
BETHETH_OPEN: RP2Decimal = RP2Decimal("0.9736")
BETHETH_CLOSE: RP2Decimal = RP2Decimal("0.9735")
BETHETH_VOLUME: RP2Decimal = RP2Decimal("309")

ETHUSDT_LOW: RP2Decimal = RP2Decimal("1753.75")
ETHUSDT_HIGH: RP2Decimal = RP2Decimal("1764.70")
ETHUSDT_OPEN: RP2Decimal = RP2Decimal("1755.81")
ETHUSDT_CLOSE: RP2Decimal = RP2Decimal("1763.03")
ETHUSDT_VOLUME: RP2Decimal = RP2Decimal("434")

# Non-USD Fiat pair conversion
BTCGBP_TIMESTAMP: datetime = datetime.fromtimestamp(1504541600, timezone.utc)
BTCGBP_LOW: RP2Decimal = RP2Decimal("3379.06")
BTCGBP_HIGH: RP2Decimal = RP2Decimal("3387.53")
BTCGBP_OPEN: RP2Decimal = RP2Decimal("3383.29")
BTCGBP_CLOSE: RP2Decimal = RP2Decimal("3379.66")
BTCGBP_VOLUME: RP2Decimal = RP2Decimal("37.72941911")

# Fiat to Fiat Test
EUR_USD_RATE: RP2Decimal = RP2Decimal("1.0847")
EUR_USD_TIMESTAMP: datetime = datetime.fromtimestamp(1585958400, timezone.utc)

# Kraken CSV read Test
KRAKEN_TIMESTAMP: datetime = datetime.fromtimestamp(1601855760, timezone.utc)
KRAKEN_LOW: RP2Decimal = RP2Decimal("1.5557")
KRAKEN_HIGH: RP2Decimal = RP2Decimal("1.5556")
KRAKEN_OPEN: RP2Decimal = RP2Decimal("1.5555")
KRAKEN_CLOSE: RP2Decimal = RP2Decimal("1.5558")
KRAKEN_VOLUME: RP2Decimal = RP2Decimal("15.15")

_MS_IN_SECOND: int = 1000


class TestCcxtPlugin:
    @pytest.fixture
    def test_graph(self) -> MappedGraph[str]:
        beth: Vertex[str] = Vertex[str](name="BETH")
        btc: Vertex[str] = Vertex[str](name="BTC")
        eth: Vertex[str] = Vertex[str](name="ETH")
        gbp: Vertex[str] = Vertex[str](name="GBP")
        jpy: Vertex[str] = Vertex[str](name="JPY")
        usdt: Vertex[str] = Vertex[str](name="USDT")
        usd: Vertex[str] = Vertex[str](name="USD")

        beth.add_neighbor(eth, 1.0)
        btc.add_neighbor(usdt, 1.0)
        btc.add_neighbor(gbp, 1.0)
        eth.add_neighbor(usdt, 1.0)
        usdt.add_neighbor(usd, 1.0)
        usd.add_neighbor(jpy, 2.0)

        return MappedGraph[str]([beth, btc, eth, gbp, jpy, usdt, usd])

    def __btcusdt_mock(self, plugin: PairConverterPlugin, mocker: Any, test_graph: MappedGraph[str]) -> None:
        exchange = kraken(
            {
                "apiKey": "key",
                "secret": "secret",
            }
        )
        alt_exchange = binance(
            {
                "apiKey": "key",
                "secret": "secret",
            }
        )

        kraken_csv = KrakenCsvPricing(google_api_key="whatever")

        mocker.patch.object(plugin, "_PairConverterPlugin__exchange_markets", {TEST_EXCHANGE: TEST_MARKETS})
        mocker.patch.object(kraken_csv, "find_historical_bar", [])
        mocker.patch.object(plugin, "_PairConverterPlugin__exchange_csv_reader", {"kraken": kraken_csv})
        mocker.patch.object(alt_exchange, "fetchOHLCV").return_value = [
            [
                BAR_TIMESTAMP,  # UTC timestamp in milliseconds, integer
                BAR_OPEN,  # (O)pen price, float
                BAR_HIGH,  # (H)ighest price, float
                BAR_LOW,  # (L)owest price, float
                BAR_CLOSE,  # (C)losing price, float
                BAR_VOLUME,  # (V)olume (in terms of the base currency), float
            ],
        ]

        mocker.patch.object(exchange, "fetchOHLCV").return_value = [
            [
                USDTUSD_TIMESTAMP,  # UTC timestamp in milliseconds, integer
                USDTUSD_OPEN,  # (O)pen price, float
                USDTUSD_HIGH,  # (H)ighest price, float
                USDTUSD_LOW,  # (L)owest price, float
                USDTUSD_CLOSE,  # (C)losing price, float
                USDTUSD_VOLUME,  # (V)olume (in terms of the base currency), float
            ],
        ]
        mocker.patch.object(plugin, "_PairConverterPlugin__exchanges", {TEST_EXCHANGE: exchange, ALT_EXCHANGE: alt_exchange})
        mocker.patch.object(plugin, "_PairConverterPlugin__exchange_graphs", {TEST_EXCHANGE: test_graph})

    def test_unknown_exchange(self, mocker: Any, test_graph: MappedGraph[str]) -> None:
        plugin: PairConverterPlugin = PairConverterPlugin(Keyword.HISTORICAL_PRICE_HIGH.value)
        self.__btcusdt_mock(plugin, mocker, test_graph)

        data = plugin.get_historic_bar_from_native_source(BAR_TIMESTAMP, "BTC", "USD", "Bogus Exchange")
        assert data

    def test_historical_prices(self, mocker: Any, test_graph: MappedGraph[str]) -> None:
        plugin: PairConverterPlugin = PairConverterPlugin(Keyword.HISTORICAL_PRICE_HIGH.value)
        cache_path = os.path.join(CACHE_DIR, plugin.cache_key())
        if os.path.exists(cache_path):
            os.remove(cache_path)

        # Reinstantiate plugin now that cache is gone
        plugin = PairConverterPlugin(Keyword.HISTORICAL_PRICE_HIGH.value)
        self.__btcusdt_mock(plugin, mocker, test_graph)

        data = plugin.get_historic_bar_from_native_source(BAR_TIMESTAMP, "BTC", "USD", TEST_EXCHANGE)

        assert data
        assert data.timestamp == BAR_TIMESTAMP
        assert data.low == BAR_LOW * USDTUSD_LOW
        assert data.high == BAR_HIGH * USDTUSD_HIGH
        assert data.open == BAR_OPEN * USDTUSD_OPEN
        assert data.close == BAR_CLOSE * USDTUSD_CLOSE
        assert data.volume == BAR_VOLUME + USDTUSD_VOLUME

        # Read price again, but populate plugin cache this time
        value = plugin.get_conversion_rate(BAR_TIMESTAMP, "BTC", "USD", TEST_EXCHANGE)
        assert value
        assert value == BAR_HIGH * USDTUSD_HIGH

        # Save plugin cache
        plugin.save_historical_price_cache()

        # Load plugin cache and verify
        cache = load_from_cache(plugin.cache_key())
        key = AssetPairAndTimestamp(BAR_TIMESTAMP, "BTC", "USD", TEST_EXCHANGE)

        # 3 cached prices - BTC/USDT, USDT/USD, BTC/USD
        assert len(cache) == 3, str(cache)
        assert key in cache
        data = cache[key]

        assert data
        assert data.timestamp == BAR_TIMESTAMP
        assert data.timestamp == BAR_TIMESTAMP
        assert data.low == BAR_LOW * USDTUSD_LOW
        assert data.high == BAR_HIGH * USDTUSD_HIGH
        assert data.open == BAR_OPEN * USDTUSD_OPEN
        assert data.close == BAR_CLOSE * USDTUSD_CLOSE
        assert data.volume == BAR_VOLUME + USDTUSD_VOLUME

    def test_missing_historical_prices(self, mocker: Any) -> None:
        plugin = PairConverterPlugin(Keyword.HISTORICAL_PRICE_HIGH.value)
        timestamp = datetime(2020, 6, 1, 0, 0)

        mocker.patch.object(plugin, "get_historic_bar_from_native_source").return_value = None

        data = plugin.get_historic_bar_from_native_source(timestamp, "BOGUSCOIN", "JPY", TEST_EXCHANGE)
        assert data is None

    def test_missing_fiat_pair(self, mocker: Any, test_graph: MappedGraph[str]) -> None:
        plugin: PairConverterPlugin = PairConverterPlugin(Keyword.HISTORICAL_PRICE_HIGH.value)
        self.__btcusdt_mock(plugin, mocker, test_graph)

        mocker.patch.object(plugin, "_get_fiat_exchange_rate").return_value = HistoricalBar(
            duration=timedelta(seconds=86400),
            timestamp=BAR_TIMESTAMP,
            open=RP2Decimal(str(JPY_USD_RATE)),
            high=RP2Decimal(str(JPY_USD_RATE)),
            low=RP2Decimal(str(JPY_USD_RATE)),
            close=RP2Decimal(str(JPY_USD_RATE)),
            volume=ZERO,
        )

        data = plugin.get_historic_bar_from_native_source(BAR_TIMESTAMP, "BTC", "JPY", TEST_EXCHANGE)

        assert data
        assert data.timestamp == BAR_TIMESTAMP
        assert data.low == BAR_LOW * USDTUSD_LOW * JPY_USD_RATE
        assert data.high == BAR_HIGH * USDTUSD_HIGH * JPY_USD_RATE
        assert data.open == BAR_OPEN * USDTUSD_OPEN * JPY_USD_RATE
        assert data.close == BAR_CLOSE * USDTUSD_CLOSE * JPY_USD_RATE
        assert data.volume == BAR_VOLUME + USDTUSD_VOLUME

    # Some crypto assets have no fiat or stable coin pair; they are only paired with BTC or ETH (e.g. EZ or BETH)
    # To get an accurate fiat price, we must get the price in the base asset (e.g. BETH -> ETH) then convert that to fiat (e.g. ETH -> USD)
    def test_no_fiat_pair(self, mocker: Any, test_graph: MappedGraph[str]) -> None:
        plugin: PairConverterPlugin = PairConverterPlugin(Keyword.HISTORICAL_PRICE_HIGH.value)

        exchange = kraken(
            {
                "apiKey": "key",
                "secret": "secret",
            }
        )
        alt_exchange = binance(
            {
                "apiKey": "key",
                "secret": "secret",
            }
        )
        mocker.patch.object(plugin, "_PairConverterPlugin__exchange_markets", {TEST_EXCHANGE: TEST_MARKETS})

        def no_fiat_fetch_ohlcv(symbol: str, timeframe: str, timestamp: int, candles: int) -> List[List[Union[float, int]]]:
            # pylint: disable=unused-argument
            result: List[List[Union[float, int]]] = []
            if symbol == "BETH/ETH":
                result = [
                    [
                        BETHETH_TIMESTAMP.timestamp() * _MS_IN_SECOND,  # UTC timestamp in milliseconds, integer
                        float(BETHETH_OPEN),  # (O)pen price, float
                        float(BETHETH_HIGH),  # (H)ighest price, float
                        float(BETHETH_LOW),  # (L)owest price, float
                        float(BETHETH_CLOSE),  # (C)losing price, float
                        float(BETHETH_VOLUME),  # (V)olume (in terms of the base currency), float
                    ],
                ]
            elif symbol == "ETH/USDT":
                result = [
                    [
                        BETHETH_TIMESTAMP.timestamp() * _MS_IN_SECOND,  # UTC timestamp in milliseconds, integer
                        float(ETHUSDT_OPEN),  # (O)pen price, float
                        float(ETHUSDT_HIGH),  # (H)ighest price, float
                        float(ETHUSDT_LOW),  # (L)owest price, float
                        float(ETHUSDT_CLOSE),  # (C)losing price, float
                        float(ETHUSDT_VOLUME),  # (V)olume (in terms of the base currency), float
                    ],
                ]

            return result

        mocker.patch.object(alt_exchange, "fetchOHLCV").side_effect = no_fiat_fetch_ohlcv
        mocker.patch.object(exchange, "fetchOHLCV").return_value = [
            [
                USDTUSD_TIMESTAMP,  # UTC timestamp in milliseconds, integer
                USDTUSD_OPEN,  # (O)pen price, float
                USDTUSD_HIGH,  # (H)ighest price, float
                USDTUSD_LOW,  # (L)owest price, float
                USDTUSD_CLOSE,  # (C)losing price, float
                USDTUSD_VOLUME,  # (V)olume (in terms of the base currency), float
            ],
        ]
        mocker.patch.object(plugin, "_PairConverterPlugin__exchanges", {TEST_EXCHANGE: exchange, ALT_EXCHANGE: alt_exchange})
        mocker.patch.object(plugin, "_PairConverterPlugin__exchange_graphs", {TEST_EXCHANGE: test_graph})

        data = plugin.get_historic_bar_from_native_source(BETHETH_TIMESTAMP, "BETH", "USD", TEST_EXCHANGE)

        assert data
        assert data.timestamp == BETHETH_TIMESTAMP
        assert data.low == BETHETH_LOW * ETHUSDT_LOW * USDTUSD_LOW
        assert data.high == BETHETH_HIGH * ETHUSDT_HIGH * USDTUSD_HIGH
        assert data.open == BETHETH_OPEN * ETHUSDT_OPEN * USDTUSD_OPEN
        assert data.close == BETHETH_CLOSE * ETHUSDT_CLOSE * USDTUSD_CLOSE
        assert data.volume == BETHETH_VOLUME + ETHUSDT_VOLUME + USDTUSD_VOLUME

    # Test to make sure the default stable coin is not used with a fiat market that does exist on the exchange
    def test_nonusd_fiat_pair(self, mocker: Any, test_graph: MappedGraph[str]) -> None:
        plugin: PairConverterPlugin = PairConverterPlugin(Keyword.HISTORICAL_PRICE_HIGH.value, default_exchange="Binance.com")
        alt_exchange = binance(
            {
                "apiKey": "key",
                "secret": "secret",
            }
        )
        exchange = kraken(
            {
                "apiKey": "key",
                "secret": "secret",
            }
        )
        mocker.patch.object(plugin, "_PairConverterPlugin__exchange_markets", {TEST_EXCHANGE: TEST_MARKETS})
        mocker.patch.object(exchange, "fetchOHLCV").return_value = [
            [
                BAR_TIMESTAMP,  # UTC timestamp in milliseconds, integer
                BAR_OPEN,  # (O)pen price, float
                BAR_HIGH,  # (H)ighest price, float
                BAR_LOW,  # (L)owest price, float
                BAR_CLOSE,  # (C)losing price, float
                BAR_VOLUME,  # (V)olume (in terms of the base currency), float
            ],
        ]
        mocker.patch.object(alt_exchange, "fetchOHLCV").return_value = [
            [
                BTCGBP_TIMESTAMP,  # UTC timestamp in milliseconds, integer
                BTCGBP_OPEN,  # (O)pen price, float
                BTCGBP_HIGH,  # (H)ighest price, float
                BTCGBP_LOW,  # (L)owest price, float
                BTCGBP_CLOSE,  # (C)losing price, float
                BTCGBP_VOLUME,  # (V)olume (in terms of the base currency), float
            ],
        ]
        mocker.patch.object(plugin, "_PairConverterPlugin__exchanges", {TEST_EXCHANGE: exchange, ALT_EXCHANGE: alt_exchange})
        mocker.patch.object(plugin, "_PairConverterPlugin__exchange_graphs", {TEST_EXCHANGE: test_graph})

        data = plugin.get_historic_bar_from_native_source(BTCGBP_TIMESTAMP, "BTC", "GBP", TEST_EXCHANGE)

        assert data
        assert data.timestamp == BTCGBP_TIMESTAMP
        assert data.low == BTCGBP_LOW
        assert data.high == BTCGBP_HIGH
        assert data.open == BTCGBP_OPEN
        assert data.close == BTCGBP_CLOSE
        assert data.volume == BTCGBP_VOLUME

    # Plugin should hand off the handling of a fiat to fiat pair to the fiat converter
    def test_fiat_pair(self, mocker: Any, test_graph: MappedGraph[str]) -> None:
        plugin: PairConverterPlugin = PairConverterPlugin(Keyword.HISTORICAL_PRICE_HIGH.value)
        exchange = binance(
            {
                "apiKey": "key",
                "secret": "secret",
            }
        )

        # Need to be mocked to prevent logger spam
        mocker.patch.object(plugin, "_PairConverterPlugin__exchange_markets", {TEST_EXCHANGE: ["WHATEVER"]})
        mocker.patch.object(plugin, "_PairConverterPlugin__exchange_graphs", {TEST_EXCHANGE: test_graph})
        mocker.patch.object(plugin, "_get_fiat_exchange_rate").return_value = HistoricalBar(
            duration=timedelta(seconds=86400),
            timestamp=EUR_USD_TIMESTAMP,
            open=RP2Decimal(str(EUR_USD_RATE)),
            high=RP2Decimal(str(EUR_USD_RATE)),
            low=RP2Decimal(str(EUR_USD_RATE)),
            close=RP2Decimal(str(EUR_USD_RATE)),
            volume=ZERO,
        )
        mocker.patch.object(plugin, "_PairConverterPlugin__exchanges", {TEST_EXCHANGE: exchange})

        data = plugin.get_historic_bar_from_native_source(EUR_USD_TIMESTAMP, "EUR", "USD", TEST_EXCHANGE)

        assert data
        assert data.timestamp == EUR_USD_TIMESTAMP
        assert data.low == EUR_USD_RATE
        assert data.high == EUR_USD_RATE
        assert data.open == EUR_USD_RATE
        assert data.close == EUR_USD_RATE
        assert data.volume == ZERO

    def test_kraken_csv(self, mocker: Any, test_graph: MappedGraph[str]) -> None:
        plugin: PairConverterPlugin = PairConverterPlugin(Keyword.HISTORICAL_PRICE_HIGH.value, google_api_key="whatever")

        cache_path = os.path.join(CACHE_DIR, "Test-" + plugin.cache_key())
        if os.path.exists(cache_path):
            os.remove(cache_path)

        kraken_csv = KrakenCsvPricing(google_api_key="whatever")
        mocker.patch.object(kraken_csv, "cache_key").return_value = "Test-" + kraken_csv.cache_key()
        mocker.patch.object(kraken_csv, "_Kraken__CACHE_DIRECTORY", "output/kraken_test")
        if not os.path.exists("output/kraken_test"):
            os.makedirs("output/kraken_test")
        with open("input/USD_OHLCVT_test.zip", "rb") as file:
            mocker.patch.object(kraken_csv, "_google_file_to_bytes").return_value = file.read()

        mocker.patch.object(plugin, "_PairConverterPlugin__exchange_csv_reader", {"Kraken": kraken_csv})
        exchange = kraken(
            {
                "apiKey": "key",
                "secret": "secret",
            }
        )
        alt_exchange = binance(
            {
                "apiKey": "key",
                "secret": "secret",
            }
        )
        modified_markets = TEST_MARKETS
        modified_markets["BTCUSDT"] = [ALT_EXCHANGE]
        mocker.patch.object(plugin, "_PairConverterPlugin__exchange_markets", {TEST_EXCHANGE: modified_markets})
        mocker.patch.object(alt_exchange, "fetchOHLCV").return_value = [
            [
                KRAKEN_TIMESTAMP,  # Match the timestamp to assure correct price look up
                BAR_OPEN,  # (O)pen price, float
                BAR_HIGH,  # (H)ighest price, float
                BAR_LOW,  # (L)owest price, float
                BAR_CLOSE,  # (C)losing price, float
                BAR_VOLUME,  # (V)olume (in terms of the base currency), float
            ],
        ]

        mocker.patch.object(exchange, "fetchOHLCV").return_value = [
            [
                KRAKEN_TIMESTAMP,  # UTC timestamp in milliseconds, integer
                USDTUSD_OPEN,  # (O)pen price, float
                USDTUSD_HIGH,  # (H)ighest price, float
                USDTUSD_LOW,  # (L)owest price, float
                USDTUSD_CLOSE,  # (C)losing price, float
                USDTUSD_VOLUME,  # (V)olume (in terms of the base currency), float
            ],
        ]
        mocker.patch.object(plugin, "_PairConverterPlugin__exchanges", {TEST_EXCHANGE: exchange, ALT_EXCHANGE: alt_exchange})
        mocker.patch.object(plugin, "_PairConverterPlugin__exchange_graphs", {TEST_EXCHANGE: test_graph})

        data = plugin.get_historic_bar_from_native_source(KRAKEN_TIMESTAMP, "BTC", "USD", TEST_EXCHANGE)

        assert data
        assert data.timestamp == KRAKEN_TIMESTAMP
        assert data.low == BAR_LOW * KRAKEN_LOW
        assert data.high == BAR_HIGH * KRAKEN_HIGH
        assert data.open == BAR_OPEN * KRAKEN_OPEN
        assert data.close == BAR_CLOSE * KRAKEN_CLOSE
        assert data.volume == BAR_VOLUME + KRAKEN_VOLUME

    def test_locked_exchange(self, mocker: Any, test_graph: MappedGraph[str]) -> None:
        plugin: PairConverterPlugin = PairConverterPlugin(Keyword.HISTORICAL_PRICE_HIGH.value, default_exchange=LOCKED_EXCHANGE, exchange_locked=True)
        # Name is changed to exchange_instance to avoid conflicts with the side effect function `add_exchange_side_effect`
        exchange_instance = kraken(
            {
                "apiKey": "key",
                "secret": "secret",
            }
        )
        alt_exchange = binance(
            {
                "apiKey": "key",
                "secret": "secret",
            }
        )
        kraken_csv = KrakenCsvPricing(google_api_key="whatever")

        mocker.patch.object(kraken_csv, "find_historical_bar").return_value = None
        mocker.patch.object(plugin, "_PairConverterPlugin__exchange_csv_reader", {LOCKED_EXCHANGE: kraken_csv})
        mocker.patch.object(exchange_instance, "fetchOHLCV").return_value = [
            [
                BAR_TIMESTAMP,  # UTC timestamp in milliseconds, integer
                BAR_OPEN,  # (O)pen price, float
                BAR_HIGH,  # (H)ighest price, float
                BAR_LOW,  # (L)owest price, float
                BAR_CLOSE,  # (C)losing price, float
                BAR_VOLUME,  # (V)olume (in terms of the base currency), float
            ],
        ]

        mocker.patch.object(alt_exchange, "fetchOHLCV").return_value = [
            [
                USDTUSD_TIMESTAMP,  # UTC timestamp in milliseconds, integer
                USDTUSD_OPEN,  # (O)pen price, float
                USDTUSD_HIGH,  # (H)ighest price, float
                USDTUSD_LOW,  # (L)owest price, float
                USDTUSD_CLOSE,  # (C)losing price, float
                USDTUSD_VOLUME,  # (V)olume (in terms of the base currency), float
            ],
        ]

        def add_exchange_side_effect(exchange: str) -> None:  # pylint: disable=unused-argument
            mocker.patch.object(plugin, "_PairConverterPlugin__exchanges", {LOCKED_EXCHANGE: exchange_instance})
            mocker.patch.object(plugin, "_PairConverterPlugin__exchange_graphs", {LOCKED_EXCHANGE: test_graph})
            mocker.patch.object(plugin, "_PairConverterPlugin__exchange_markets", {LOCKED_EXCHANGE: LOCKED_MARKETS})

        mocker.patch.object(plugin, "_add_exchange_to_memcache", autospec=True).side_effect = add_exchange_side_effect

        data = plugin.get_historic_bar_from_native_source(BAR_TIMESTAMP, "BTC", "USD", "not-kraken")

        assert data
        assert mocker.patch.object(plugin, "_add_exchange_to_memcache").called_once_with(LOCKED_EXCHANGE)
