from copy import deepcopy
from pathlib import Path
from unittest.mock import MagicMock

import pandas as pd
import plotly.graph_objects as go
import pytest
from plotly.subplots import make_subplots

from freqtrade.commands import start_plot_dataframe, start_plot_profit
from freqtrade.configuration import TimeRange
from freqtrade.data import history
from freqtrade.data.btanalysis import load_backtest_data
from freqtrade.data.metrics import create_cum_profit
from freqtrade.exceptions import OperationalException
from freqtrade.plot.plotting import (add_areas, add_indicators, add_profit, create_plotconfig,
                                     generate_candlestick_graph, generate_plot_filename,
                                     generate_profit_graph, init_plotscript, load_and_plot_trades,
                                     plot_profit, plot_trades, store_plot_file)
from freqtrade.resolvers import StrategyResolver
from tests.conftest import get_args, log_has, log_has_re, patch_exchange


def fig_generating_mock(fig, *args, **kwargs):
    """ Return Fig - used to mock add_indicators and plot_trades"""
    return fig


def find_trace_in_fig_data(data, search_string: str):
    matches = (d for d in data if d.name == search_string)
    return next(matches)


def generate_empty_figure():
    return make_subplots(
        rows=3,
        cols=1,
        shared_xaxes=True,
        row_width=[1, 1, 4],
        vertical_spacing=0.0001,
    )


def test_init_plotscript(default_conf, mocker, testdatadir):
    default_conf['timerange'] = "20180110-20180112"
    default_conf['trade_source'] = "file"
    default_conf['timeframe'] = "5m"
    default_conf['exportfilename'] = testdatadir / "backtest-result.json"
    supported_markets = ["TRX/BTC", "ADA/BTC"]
    ret = init_plotscript(default_conf, supported_markets)
    assert "ohlcv" in ret
    assert "trades" in ret
    assert "pairs" in ret
    assert 'timerange' in ret

    default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"]
    ret = init_plotscript(default_conf, supported_markets, 20)
    assert "ohlcv" in ret
    assert "TRX/BTC" in ret["ohlcv"]
    assert "ADA/BTC" in ret["ohlcv"]


def test_add_indicators(default_conf, testdatadir, caplog):
    pair = "UNITTEST/BTC"
    timerange = TimeRange()

    data = history.load_pair_history(pair=pair, timeframe='1m',
                                     datadir=testdatadir, timerange=timerange)
    indicators1 = {"ema10": {}}
    indicators2 = {"macd": {"color": "red"}}

    strategy = StrategyResolver.load_strategy(default_conf)

    # Generate entry/exit signals and indicators
    data = strategy.analyze_ticker(data, {'pair': pair})
    fig = generate_empty_figure()

    # Row 1
    fig1 = add_indicators(fig=deepcopy(fig), row=1, indicators=indicators1, data=data)
    figure = fig1.layout.figure
    ema10 = find_trace_in_fig_data(figure.data, "ema10")
    assert isinstance(ema10, go.Scatter)
    assert ema10.yaxis == "y"

    fig2 = add_indicators(fig=deepcopy(fig), row=3, indicators=indicators2, data=data)
    figure = fig2.layout.figure
    macd = find_trace_in_fig_data(figure.data, "macd")
    assert isinstance(macd, go.Scatter)
    assert macd.yaxis == "y3"
    assert macd.line.color == "red"

    # No indicator found
    fig3 = add_indicators(fig=deepcopy(fig), row=3, indicators={'no_indicator': {}}, data=data)
    assert fig == fig3
    assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog)


def test_add_areas(default_conf, testdatadir, caplog):
    pair = "UNITTEST/BTC"
    timerange = TimeRange(None, 'line', 0, -1000)

    data = history.load_pair_history(pair=pair, timeframe='1m',
                                     datadir=testdatadir, timerange=timerange)
    indicators = {"macd": {"color": "red",
                           "fill_color": "black",
                           "fill_to": "macdhist",
                           "fill_label": "MACD Fill"}}

    ind_no_label = {"macd": {"fill_color": "red",
                             "fill_to": "macdhist"}}

    ind_plain = {"macd": {"fill_to": "macdhist"}}
    strategy = StrategyResolver.load_strategy(default_conf)

    # Generate entry/exit signals and indicators
    data = strategy.analyze_ticker(data, {'pair': pair})
    fig = generate_empty_figure()

    # indicator mentioned in fill_to does not exist
    fig1 = add_areas(fig, 1, data, {'ema10': {'fill_to': 'no_fill_indicator'}})
    assert fig == fig1
    assert log_has_re(r'fill_to: "no_fill_indicator" ignored\..*', caplog)

    # indicator does not exist
    fig2 = add_areas(fig, 1, data, {'no_indicator': {'fill_to': 'ema10'}})
    assert fig == fig2
    assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog)

    # everythin given in plot config, row 3
    fig3 = add_areas(fig, 3, data, indicators)
    figure = fig3.layout.figure
    fill_macd = find_trace_in_fig_data(figure.data, "MACD Fill")
    assert isinstance(fill_macd, go.Scatter)
    assert fill_macd.yaxis == "y3"
    assert fill_macd.fillcolor == "black"

    # label missing, row 1
    fig4 = add_areas(fig, 1, data, ind_no_label)
    figure = fig4.layout.figure
    fill_macd = find_trace_in_fig_data(figure.data, "macd<>macdhist")
    assert isinstance(fill_macd, go.Scatter)
    assert fill_macd.yaxis == "y"
    assert fill_macd.fillcolor == "red"

    # fit_to only
    fig5 = add_areas(fig, 1, data, ind_plain)
    figure = fig5.layout.figure
    fill_macd = find_trace_in_fig_data(figure.data, "macd<>macdhist")
    assert isinstance(fill_macd, go.Scatter)
    assert fill_macd.yaxis == "y"


def test_plot_trades(testdatadir, caplog):
    fig1 = generate_empty_figure()
    # nothing happens when no trades are available
    fig = plot_trades(fig1, None)
    assert fig == fig1
    assert log_has("No trades found.", caplog)
    pair = "ADA/BTC"
    filename = testdatadir / "backtest_results/backtest-result.json"
    trades = load_backtest_data(filename)
    trades = trades.loc[trades['pair'] == pair]

    fig = plot_trades(fig, trades)
    figure = fig1.layout.figure

    # Check entry - color, should be in first graph, ...
    trade_entries = find_trace_in_fig_data(figure.data, 'Trade entry')
    assert isinstance(trade_entries, go.Scatter)
    assert trade_entries.yaxis == 'y'
    assert len(trades) == len(trade_entries.x)
    assert trade_entries.marker.color == 'cyan'
    assert trade_entries.marker.symbol == 'circle-open'
    assert trade_entries.text[0] == '3.99%, buy_tag, roi, 15 min'

    trade_exit = find_trace_in_fig_data(figure.data, 'Exit - Profit')
    assert isinstance(trade_exit, go.Scatter)
    assert trade_exit.yaxis == 'y'
    assert len(trades.loc[trades['profit_ratio'] > 0]) == len(trade_exit.x)
    assert trade_exit.marker.color == 'green'
    assert trade_exit.marker.symbol == 'square-open'
    assert trade_exit.text[0] == '3.99%, buy_tag, roi, 15 min'

    trade_sell_loss = find_trace_in_fig_data(figure.data, 'Exit - Loss')
    assert isinstance(trade_sell_loss, go.Scatter)
    assert trade_sell_loss.yaxis == 'y'
    assert len(trades.loc[trades['profit_ratio'] <= 0]) == len(trade_sell_loss.x)
    assert trade_sell_loss.marker.color == 'red'
    assert trade_sell_loss.marker.symbol == 'square-open'
    assert trade_sell_loss.text[5] == '-10.45%, stop_loss, 720 min'


def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, testdatadir, caplog):
    row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators',
                            MagicMock(side_effect=fig_generating_mock))
    trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades',
                               MagicMock(side_effect=fig_generating_mock))

    pair = "UNITTEST/BTC"
    timerange = TimeRange(None, 'line', 0, -1000)
    data = history.load_pair_history(pair=pair, timeframe='1m',
                                     datadir=testdatadir, timerange=timerange)
    data['enter_long'] = 0
    data['exit_long'] = 0
    data['enter_short'] = 0
    data['exit_short'] = 0

    indicators1 = []
    indicators2 = []
    fig = generate_candlestick_graph(pair=pair, data=data, trades=None,
                                     indicators1=indicators1, indicators2=indicators2)
    assert isinstance(fig, go.Figure)
    assert fig.layout.title.text == pair
    figure = fig.layout.figure

    assert len(figure.data) == 2
    # Candlesticks are plotted first
    candles = find_trace_in_fig_data(figure.data, "Price")
    assert isinstance(candles, go.Candlestick)

    volume = find_trace_in_fig_data(figure.data, "Volume")
    assert isinstance(volume, go.Bar)

    assert row_mock.call_count == 2
    assert trades_mock.call_count == 1

    assert log_has("No enter_long-signals found.", caplog)
    assert log_has("No exit_long-signals found.", caplog)
    assert log_has("No enter_short-signals found.", caplog)
    assert log_has("No exit_short-signals found.", caplog)


def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir):
    row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators',
                            MagicMock(side_effect=fig_generating_mock))
    trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades',
                               MagicMock(side_effect=fig_generating_mock))
    pair = 'UNITTEST/BTC'
    timerange = TimeRange(None, 'line', 0, -1000)
    data = history.load_pair_history(pair=pair, timeframe='1m',
                                     datadir=testdatadir, timerange=timerange)

    strategy = StrategyResolver.load_strategy(default_conf)

    # Generate buy/sell signals and indicators
    data = strategy.analyze_ticker(data, {'pair': pair})

    indicators1 = []
    indicators2 = []
    fig = generate_candlestick_graph(pair=pair, data=data, trades=None,
                                     indicators1=indicators1, indicators2=indicators2)
    assert isinstance(fig, go.Figure)
    assert fig.layout.title.text == pair
    figure = fig.layout.figure

    assert len(figure.data) == 8
    # Candlesticks are plotted first
    candles = find_trace_in_fig_data(figure.data, "Price")
    assert isinstance(candles, go.Candlestick)

    volume = find_trace_in_fig_data(figure.data, "Volume")
    assert isinstance(volume, go.Bar)

    enter_long = find_trace_in_fig_data(figure.data, "enter_long")
    assert isinstance(enter_long, go.Scatter)
    # All buy-signals should be plotted
    assert int(data['enter_long'].sum()) == len(enter_long.x)

    exit_long = find_trace_in_fig_data(figure.data, "exit_long")
    assert isinstance(exit_long, go.Scatter)
    # All buy-signals should be plotted
    assert int(data['exit_long'].sum()) == len(exit_long.x)

    assert find_trace_in_fig_data(figure.data, "Bollinger Band")

    assert row_mock.call_count == 2
    assert trades_mock.call_count == 1


def test_generate_Plot_filename():
    fn = generate_plot_filename("UNITTEST/BTC", "5m")
    assert fn == "freqtrade-plot-UNITTEST_BTC-5m.html"


def test_generate_plot_file(mocker, caplog):
    fig = generate_empty_figure()
    plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock())
    store_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html",
                    directory=Path("user_data/plot"))

    expected_fn = str(Path("user_data/plot/freqtrade-plot-UNITTEST_BTC-5m.html"))
    assert plot_mock.call_count == 1
    assert plot_mock.call_args[0][0] == fig
    assert (plot_mock.call_args_list[0][1]['filename']
            == expected_fn)
    assert log_has(f"Stored plot as {expected_fn}",
                   caplog)


def test_add_profit(testdatadir):
    filename = testdatadir / "backtest_results/backtest-result.json"
    bt_data = load_backtest_data(filename)
    timerange = TimeRange.parse_timerange("20180110-20180112")

    df = history.load_pair_history(pair="TRX/BTC", timeframe='5m',
                                   datadir=testdatadir, timerange=timerange)
    fig = generate_empty_figure()

    cum_profits = create_cum_profit(df.set_index('date'),
                                    bt_data[bt_data["pair"] == 'TRX/BTC'],
                                    "cum_profits", timeframe="5m")

    fig1 = add_profit(fig, row=2, data=cum_profits, column='cum_profits', name='Profits')
    figure = fig1.layout.figure
    profits = find_trace_in_fig_data(figure.data, "Profits")
    assert isinstance(profits, go.Scatter)
    assert profits.yaxis == "y2"


def test_generate_profit_graph(testdatadir):
    filename = testdatadir / "backtest_results/backtest-result.json"
    trades = load_backtest_data(filename)
    timerange = TimeRange.parse_timerange("20180110-20180112")
    pairs = ["TRX/BTC", "XLM/BTC"]
    trades = trades[trades['close_date'] < pd.Timestamp('2018-01-12', tz='UTC')]

    data = history.load_data(datadir=testdatadir,
                             pairs=pairs,
                             timeframe='5m',
                             timerange=timerange)

    trades = trades[trades['pair'].isin(pairs)]

    fig = generate_profit_graph(
        pairs,
        data,
        trades,
        timeframe="5m",
        stake_currency='BTC',
        starting_balance=0)
    assert isinstance(fig, go.Figure)

    assert fig.layout.title.text == "Freqtrade Profit plot"
    assert fig.layout.yaxis.title.text == "Price"
    assert fig.layout.yaxis2.title.text == "Profit BTC"
    assert fig.layout.yaxis3.title.text == "Profit BTC"

    figure = fig.layout.figure
    assert len(figure.data) == 8

    avgclose = find_trace_in_fig_data(figure.data, "Avg close price")
    assert isinstance(avgclose, go.Scatter)

    profit = find_trace_in_fig_data(figure.data, "Profit")
    assert isinstance(profit, go.Scatter)
    drawdown = find_trace_in_fig_data(figure.data, "Max drawdown 73.89%")
    assert isinstance(drawdown, go.Scatter)
    parallel = find_trace_in_fig_data(figure.data, "Parallel trades")
    assert isinstance(parallel, go.Scatter)

    underwater = find_trace_in_fig_data(figure.data, "Underwater Plot")
    assert isinstance(underwater, go.Scatter)

    underwater_relative = find_trace_in_fig_data(figure.data, "Underwater Plot (%)")
    assert isinstance(underwater_relative, go.Scatter)

    for pair in pairs:
        profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}")
        assert isinstance(profit_pair, go.Scatter)

    with pytest.raises(OperationalException, match=r"No trades found.*"):
        # Pair cannot be empty - so it's an empty dataframe.
        generate_profit_graph(pairs, data, trades.loc[trades['pair'].isnull()], timeframe="5m",
                              stake_currency='BTC', starting_balance=0)


def test_start_plot_dataframe(mocker):
    aup = mocker.patch("freqtrade.plot.plotting.load_and_plot_trades", MagicMock())
    args = [
        "plot-dataframe",
        "--config", "config_examples/config_bittrex.example.json",
        "--pairs", "ETH/BTC"
    ]
    start_plot_dataframe(get_args(args))

    assert aup.call_count == 1
    called_config = aup.call_args_list[0][0][0]
    assert "pairs" in called_config
    assert called_config['pairs'] == ["ETH/BTC"]


def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir):
    patch_exchange(mocker)

    default_conf['trade_source'] = 'file'
    default_conf['exportfilename'] = testdatadir / "backtest-result.json"
    default_conf['indicators1'] = ["sma5", "ema10"]
    default_conf['indicators2'] = ["macd"]
    default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"]

    candle_mock = MagicMock()
    store_mock = MagicMock()
    mocker.patch.multiple(
        "freqtrade.plot.plotting",
        generate_candlestick_graph=candle_mock,
        store_plot_file=store_mock
    )
    load_and_plot_trades(default_conf)

    # Both mocks should be called once per pair
    assert candle_mock.call_count == 2
    assert store_mock.call_count == 2

    assert candle_mock.call_args_list[0][1]['indicators1'] == ['sma5', 'ema10']
    assert candle_mock.call_args_list[0][1]['indicators2'] == ['macd']

    assert log_has("End of plotting process. 2 plots generated", caplog)


def test_start_plot_profit(mocker):
    aup = mocker.patch("freqtrade.plot.plotting.plot_profit", MagicMock())
    args = [
        "plot-profit",
        "--config", "config_examples/config_bittrex.example.json",
        "--pairs", "ETH/BTC"
    ]
    start_plot_profit(get_args(args))

    assert aup.call_count == 1
    called_config = aup.call_args_list[0][0][0]
    assert "pairs" in called_config
    assert called_config['pairs'] == ["ETH/BTC"]


def test_start_plot_profit_error(mocker):

    args = [
        'plot-profit',
        '--pairs', 'ETH/BTC'
    ]
    argsp = get_args(args)
    # Make sure we use no config. Details: #2241
    # not resetting config causes random failures if config.json exists
    argsp['config'] = []
    with pytest.raises(OperationalException):
        start_plot_profit(argsp)


def test_plot_profit(default_conf, mocker, testdatadir):
    patch_exchange(mocker)
    default_conf['trade_source'] = 'file'
    default_conf['exportfilename'] = testdatadir / 'backtest-result_test_nofile.json'
    default_conf['pairs'] = ['ETH/BTC', 'LTC/BTC']

    profit_mock = MagicMock()
    store_mock = MagicMock()
    mocker.patch.multiple(
        "freqtrade.plot.plotting",
        generate_profit_graph=profit_mock,
        store_plot_file=store_mock
    )
    with pytest.raises(OperationalException,
                       match=r"No trades found, cannot generate Profit-plot.*"):
        plot_profit(default_conf)

    default_conf['exportfilename'] = testdatadir / "backtest_results/backtest-result.json"

    plot_profit(default_conf)

    # Plot-profit generates one combined plot
    assert profit_mock.call_count == 1
    assert store_mock.call_count == 1

    assert profit_mock.call_args_list[0][0][0] == default_conf['pairs']
    assert store_mock.call_args_list[0][1]['auto_open'] is False

    del default_conf['timeframe']
    with pytest.raises(OperationalException, match=r"Timeframe must be set.*--timeframe.*"):
        plot_profit(default_conf)


@pytest.mark.parametrize("ind1,ind2,plot_conf,exp", [
    # No indicators, use plot_conf
    ([], [], {},
     {'main_plot': {'sma': {}, 'ema3': {}, 'ema5': {}},
      'subplots': {'Other': {'macd': {}, 'macdsignal': {}}}}),
    # use indicators
    (['sma', 'ema3'], ['macd'], {},
     {'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'Other': {'macd': {}}}}),
    # only main_plot - adds empty subplots
    ([], [], {'main_plot': {'sma': {}}},
     {'main_plot': {'sma': {}}, 'subplots': {}}),
    # Main and subplots
    ([], [], {'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
     {'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}),
    # no main_plot, adds empty main_plot
    ([], [], {'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
     {'main_plot': {}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}),
    # indicator 1 / 2 should have prevelance
    (['sma', 'ema3'], ['macd'],
     {'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
     {'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'Other': {'macd': {}}}}
     ),
    # indicator 1 - overrides plot_config main_plot
    (['sma', 'ema3'], [],
     {'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
     {'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}
     ),
    # indicator 2 - overrides plot_config subplots
    ([], ['macd', 'macd_signal'],
     {'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}},
     {'main_plot': {'sma': {}}, 'subplots': {'Other': {'macd': {}, 'macd_signal': {}}}}
     ),
])
def test_create_plotconfig(ind1, ind2, plot_conf, exp):

    res = create_plotconfig(ind1, ind2, plot_conf)
    assert 'main_plot' in res
    assert 'subplots' in res
    assert isinstance(res['main_plot'], dict)
    assert isinstance(res['subplots'], dict)

    assert res == exp
