import random
from datetime import UTC, datetime, timedelta
from types import SimpleNamespace

import pytest

from freqtrade.enums import ExitType
from freqtrade.exceptions import OperationalException
from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence.trade_model import Order
from freqtrade.plugins.protectionmanager import ProtectionManager
from tests.conftest import get_patched_freqtradebot, log_has_re


AVAILABLE_PROTECTIONS = ["CooldownPeriod", "LowProfitPairs", "MaxDrawdown", "StoplossGuard"]


def generate_mock_trade(
    pair: str,
    fee: float,
    is_open: bool,
    exit_reason: str = ExitType.EXIT_SIGNAL,
    min_ago_open: int | None = None,
    min_ago_close: int | None = None,
    profit_rate: float = 0.9,
    is_short: bool = False,
):
    open_rate = random.random()

    trade = Trade(
        pair=pair,
        stake_amount=0.01,
        fee_open=fee,
        fee_close=fee,
        open_date=datetime.now(UTC) - timedelta(minutes=min_ago_open or 200),
        close_date=datetime.now(UTC) - timedelta(minutes=min_ago_close or 30),
        open_rate=open_rate,
        is_open=is_open,
        amount=0.01 / open_rate,
        exchange="binance",
        is_short=is_short,
        leverage=1,
    )

    trade.orders.append(
        Order(
            ft_order_side=trade.entry_side,
            order_id=f"{pair}-{trade.entry_side}-{trade.open_date}",
            ft_is_open=False,
            ft_pair=pair,
            ft_amount=trade.amount,
            ft_price=trade.open_rate,
            amount=trade.amount,
            filled=trade.amount,
            remaining=0,
            price=open_rate,
            average=open_rate,
            status="closed",
            order_type="market",
            side=trade.entry_side,
        )
    )
    if not is_open:
        close_price = open_rate * (2 - profit_rate if is_short else profit_rate)
        trade.orders.append(
            Order(
                ft_order_side=trade.exit_side,
                order_id=f"{pair}-{trade.exit_side}-{trade.close_date}",
                ft_is_open=False,
                ft_pair=pair,
                ft_amount=trade.amount,
                ft_price=trade.open_rate,
                amount=trade.amount,
                filled=trade.amount,
                remaining=0,
                price=close_price,
                average=close_price,
                status="closed",
                order_type="market",
                side=trade.exit_side,
            )
        )

    trade.recalc_open_trade_value()
    if not is_open:
        trade.close(close_price)
        trade.exit_reason = exit_reason

    Trade.session.add(trade)
    Trade.commit()
    return trade


def test_protectionmanager(mocker, default_conf):
    default_conf["_strategy_protections"] = [
        {"method": protection} for protection in AVAILABLE_PROTECTIONS
    ]
    freqtrade = get_patched_freqtradebot(mocker, default_conf)

    for handler in freqtrade.protections._protection_handlers:
        assert handler.name in AVAILABLE_PROTECTIONS
        if not handler.has_global_stop:
            assert handler.global_stop(datetime.now(UTC), "*", 1000.0) is None
        if not handler.has_local_stop:
            assert handler.stop_per_pair("XRP/BTC", datetime.now(UTC), "*", 1000.0) is None


@pytest.mark.parametrize(
    "protconf,expected",
    [
        ([], None),
        ([{"method": "StoplossGuard", "lookback_period": 2000, "stop_duration_candles": 10}], None),
        ([{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 10}], None),
        (
            [
                {
                    "method": "StoplossGuard",
                    "lookback_period_candles": 20,
                    "lookback_period": 2000,
                    "stop_duration": 10,
                }
            ],
            r"Protections must specify either `lookback_period`.*",
        ),
        (
            [
                {
                    "method": "StoplossGuard",
                    "lookback_period": 20,
                    "stop_duration": 10,
                    "stop_duration_candles": 10,
                }
            ],
            r"Protections must specify either `stop_duration`.*",
        ),
        (
            [
                {
                    "method": "StoplossGuard",
                    "lookback_period": 20,
                    "stop_duration": 10,
                    "unlock_at": "20:02",
                }
            ],
            r"Protections must specify either `unlock_at`, `stop_duration` or.*",
        ),
        (
            [{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "20:02"}],
            None,
        ),
        (
            [{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "55:102"}],
            "Invalid date format for unlock_at: 55:102.",
        ),
    ],
)
def test_validate_protections(protconf, expected):
    if expected:
        with pytest.raises(OperationalException, match=expected):
            ProtectionManager.validate_protections(protconf)
    else:
        ProtectionManager.validate_protections(protconf)


@pytest.mark.parametrize(
    "timeframe,expected_lookback,expected_stop,protconf",
    [
        (
            "1m",
            20,
            10,
            [{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 10}],
        ),
        (
            "5m",
            100,
            15,
            [{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 15}],
        ),
        (
            "1h",
            1200,
            40,
            [{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 40}],
        ),
        (
            "1d",
            1440,
            5,
            [{"method": "StoplossGuard", "lookback_period_candles": 1, "stop_duration": 5}],
        ),
        (
            "1m",
            20,
            5,
            [{"method": "StoplossGuard", "lookback_period": 20, "stop_duration_candles": 5}],
        ),
        (
            "5m",
            15,
            25,
            [{"method": "StoplossGuard", "lookback_period": 15, "stop_duration_candles": 5}],
        ),
        (
            "1h",
            50,
            600,
            [{"method": "StoplossGuard", "lookback_period": 50, "stop_duration_candles": 10}],
        ),
        (
            "1h",
            60,
            540,
            [{"method": "StoplossGuard", "lookback_period_candles": 1, "stop_duration_candles": 9}],
        ),
        (
            "1m",
            20,
            "01:00",
            [{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "01:00"}],
        ),
        (
            "5m",
            100,
            "02:00",
            [{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "02:00"}],
        ),
        (
            "1h",
            1200,
            "03:00",
            [{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "03:00"}],
        ),
        (
            "1d",
            1440,
            "04:00",
            [{"method": "StoplossGuard", "lookback_period_candles": 1, "unlock_at": "04:00"}],
        ),
    ],
)
def test_protections_init(default_conf, timeframe, expected_lookback, expected_stop, protconf):
    """
    Test the initialization of protections with different configurations, including unlock_at.
    """
    default_conf["timeframe"] = timeframe
    man = ProtectionManager(default_conf, protconf)
    assert len(man._protection_handlers) == len(protconf)
    assert man._protection_handlers[0]._lookback_period == expected_lookback
    if isinstance(expected_stop, int):
        assert man._protection_handlers[0]._stop_duration == expected_stop
    else:
        assert man._protection_handlers[0]._unlock_at == expected_stop


@pytest.mark.parametrize("is_short", [False, True])
@pytest.mark.usefixtures("init_persistence")
def test_stoploss_guard(mocker, default_conf, fee, caplog, is_short):
    # Active for both sides (long and short)
    default_conf["_strategy_protections"] = [
        {"method": "StoplossGuard", "lookback_period": 60, "stop_duration": 40, "trade_limit": 3}
    ]
    freqtrade = get_patched_freqtradebot(mocker, default_conf)
    message = r"Trading stopped due to .*"
    assert not freqtrade.protections.global_stop()
    assert not log_has_re(message, caplog)
    caplog.clear()

    generate_mock_trade(
        "XRP/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=200,
        min_ago_close=30,
        is_short=is_short,
    )

    assert not freqtrade.protections.global_stop()
    assert not log_has_re(message, caplog)
    caplog.clear()
    # This trade does not count, as it's closed too long ago
    generate_mock_trade(
        "BCH/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=250,
        min_ago_close=100,
        is_short=is_short,
    )

    generate_mock_trade(
        "ETH/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=240,
        min_ago_close=30,
        is_short=is_short,
    )
    # 3 Trades closed - but the 2nd has been closed too long ago.
    assert not freqtrade.protections.global_stop()
    assert not log_has_re(message, caplog)
    caplog.clear()

    generate_mock_trade(
        "LTC/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=180,
        min_ago_close=30,
        is_short=is_short,
    )

    assert freqtrade.protections.global_stop()
    assert log_has_re(message, caplog)
    assert PairLocks.is_global_lock()

    # Test 5m after lock-period - this should try and relock the pair, but end-time
    # should be the previous end-time
    end_time = PairLocks.get_pair_longest_lock("*").lock_end_time + timedelta(minutes=5)
    freqtrade.protections.global_stop(end_time)
    assert not PairLocks.is_global_lock(end_time)


@pytest.mark.parametrize("only_per_pair", [False, True])
@pytest.mark.parametrize("only_per_side", [False, True])
@pytest.mark.usefixtures("init_persistence")
def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair, only_per_side):
    default_conf["_strategy_protections"] = [
        {
            "method": "StoplossGuard",
            "lookback_period": 60,
            "trade_limit": 2,
            "stop_duration": 60,
            "only_per_pair": only_per_pair,
            "only_per_side": only_per_side,
        }
    ]
    check_side = "long" if only_per_side else "*"
    is_short = False
    freqtrade = get_patched_freqtradebot(mocker, default_conf)
    message = r"Trading stopped due to .*"
    pair = "XRP/BTC"
    assert not freqtrade.protections.stop_per_pair(pair)
    assert not freqtrade.protections.global_stop()
    assert not log_has_re(message, caplog)
    caplog.clear()

    generate_mock_trade(
        pair,
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=200,
        min_ago_close=30,
        profit_rate=0.9,
        is_short=is_short,
    )

    assert not freqtrade.protections.stop_per_pair(pair)
    assert not freqtrade.protections.global_stop()
    assert not log_has_re(message, caplog)
    caplog.clear()
    # This trade does not count, as it's closed too long ago
    generate_mock_trade(
        pair,
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=250,
        min_ago_close=100,
        profit_rate=0.9,
        is_short=is_short,
    )
    # Trade does not count for per pair stop as it's the wrong pair.
    generate_mock_trade(
        "ETH/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=240,
        min_ago_close=30,
        profit_rate=0.9,
        is_short=is_short,
    )
    # 3 Trades closed - but the 2nd has been closed too long ago.
    assert not freqtrade.protections.stop_per_pair(pair)
    assert freqtrade.protections.global_stop() != only_per_pair
    if not only_per_pair:
        assert log_has_re(message, caplog)
    else:
        assert not log_has_re(message, caplog)

    caplog.clear()

    # Trade does not count potentially, as it's in the wrong direction
    generate_mock_trade(
        pair,
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=150,
        min_ago_close=25,
        profit_rate=0.9,
        is_short=not is_short,
    )
    freqtrade.protections.stop_per_pair(pair)
    assert freqtrade.protections.global_stop() != only_per_pair
    assert PairLocks.is_pair_locked(pair, side=check_side) != (only_per_side and only_per_pair)
    assert PairLocks.is_global_lock(side=check_side) != only_per_pair
    if only_per_side:
        assert not PairLocks.is_pair_locked(pair, side="*")
        assert not PairLocks.is_global_lock(side="*")

    caplog.clear()

    # 2nd Trade that counts with correct pair
    generate_mock_trade(
        pair,
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=180,
        min_ago_close=31,
        profit_rate=0.9,
        is_short=is_short,
    )

    freqtrade.protections.stop_per_pair(pair)
    assert freqtrade.protections.global_stop() != only_per_pair
    assert PairLocks.is_pair_locked(pair, side=check_side)
    assert PairLocks.is_global_lock(side=check_side) != only_per_pair
    if only_per_side:
        assert not PairLocks.is_pair_locked(pair, side="*")
        assert not PairLocks.is_global_lock(side="*")


@pytest.mark.usefixtures("init_persistence")
def test_CooldownPeriod(mocker, default_conf, fee, caplog):
    default_conf["_strategy_protections"] = [
        {
            "method": "CooldownPeriod",
            "stop_duration": 60,
        }
    ]
    freqtrade = get_patched_freqtradebot(mocker, default_conf)
    message = r"Trading stopped due to .*"
    assert not freqtrade.protections.global_stop()
    assert not freqtrade.protections.stop_per_pair("XRP/BTC")

    assert not log_has_re(message, caplog)
    caplog.clear()

    generate_mock_trade(
        "XRP/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=200,
        min_ago_close=30,
    )

    assert not freqtrade.protections.global_stop()
    assert freqtrade.protections.stop_per_pair("XRP/BTC")
    assert PairLocks.is_pair_locked("XRP/BTC")
    assert not PairLocks.is_global_lock()

    generate_mock_trade(
        "ETH/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.ROI.value,
        min_ago_open=205,
        min_ago_close=35,
    )

    assert not freqtrade.protections.global_stop()
    assert not PairLocks.is_pair_locked("ETH/BTC")
    assert freqtrade.protections.stop_per_pair("ETH/BTC")
    assert PairLocks.is_pair_locked("ETH/BTC")
    assert not PairLocks.is_global_lock()


@pytest.mark.usefixtures("init_persistence")
def test_CooldownPeriod_unlock_at(mocker, default_conf, fee, caplog, time_machine):
    default_conf["_strategy_protections"] = [
        {
            "method": "CooldownPeriod",
            "unlock_at": "05:00",
        }
    ]
    freqtrade = get_patched_freqtradebot(mocker, default_conf)
    message = r"Trading stopped due to .*"
    assert not freqtrade.protections.global_stop()
    assert not freqtrade.protections.stop_per_pair("XRP/BTC")

    assert not log_has_re(message, caplog)
    caplog.clear()

    start_dt = datetime(2024, 5, 2, 0, 30, 0, tzinfo=UTC)
    time_machine.move_to(start_dt, tick=False)

    generate_mock_trade(
        "XRP/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=20,
        min_ago_close=10,
    )

    assert not freqtrade.protections.global_stop()
    assert freqtrade.protections.stop_per_pair("XRP/BTC")
    assert PairLocks.is_pair_locked("XRP/BTC")
    assert not PairLocks.is_global_lock()

    # Move time to "4:30"
    time_machine.move_to(start_dt + timedelta(hours=4), tick=False)
    assert PairLocks.is_pair_locked("XRP/BTC")
    assert not PairLocks.is_global_lock()

    # Move time to "past 5:00"
    time_machine.move_to(start_dt + timedelta(hours=5), tick=False)
    assert not PairLocks.is_pair_locked("XRP/BTC")
    assert not PairLocks.is_global_lock()

    # Force rollover to the next day.
    start_dt = datetime(2024, 5, 2, 22, 00, 0, tzinfo=UTC)
    time_machine.move_to(start_dt, tick=False)
    generate_mock_trade(
        "ETH/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.ROI.value,
        min_ago_open=20,
        min_ago_close=10,
    )

    assert not freqtrade.protections.global_stop()
    assert not PairLocks.is_pair_locked("ETH/BTC")
    assert freqtrade.protections.stop_per_pair("ETH/BTC")
    assert PairLocks.is_pair_locked("ETH/BTC")
    assert not PairLocks.is_global_lock()
    # Move to 23:00
    time_machine.move_to(start_dt + timedelta(hours=1), tick=False)
    assert PairLocks.is_pair_locked("ETH/BTC")
    assert not PairLocks.is_global_lock()

    # Move to 04:59 (should still be locked)
    time_machine.move_to(start_dt + timedelta(hours=6, minutes=59), tick=False)
    assert PairLocks.is_pair_locked("ETH/BTC")
    assert not PairLocks.is_global_lock()

    # Move to 05:01 (should still be locked - it unlocks once the 05:00 candle stops at 05:05)
    time_machine.move_to(start_dt + timedelta(hours=7, minutes=1), tick=False)

    assert PairLocks.is_pair_locked("ETH/BTC")
    assert not PairLocks.is_global_lock()

    # Move to 05:01 (unlocked).
    time_machine.move_to(start_dt + timedelta(hours=7, minutes=5), tick=False)

    assert not PairLocks.is_pair_locked("ETH/BTC")
    assert not PairLocks.is_global_lock()


@pytest.mark.parametrize("only_per_side", [False, True])
@pytest.mark.usefixtures("init_persistence")
def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side):
    default_conf["_strategy_protections"] = [
        {
            "method": "LowProfitPairs",
            "lookback_period": 400,
            "stop_duration": 60,
            "trade_limit": 2,
            "required_profit": 0.0,
            "only_per_side": only_per_side,
        }
    ]
    freqtrade = get_patched_freqtradebot(mocker, default_conf)
    message = r"Trading stopped due to .*"
    assert not freqtrade.protections.global_stop()
    assert not freqtrade.protections.stop_per_pair("XRP/BTC")

    assert not log_has_re(message, caplog)
    caplog.clear()

    generate_mock_trade(
        "XRP/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=800,
        min_ago_close=450,
        profit_rate=0.9,
    )

    Trade.commit()
    # Not locked with 1 trade
    assert not freqtrade.protections.global_stop()
    assert not freqtrade.protections.stop_per_pair("XRP/BTC")
    assert not PairLocks.is_pair_locked("XRP/BTC")
    assert not PairLocks.is_global_lock()

    generate_mock_trade(
        "XRP/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=200,
        min_ago_close=120,
        profit_rate=0.9,
    )

    Trade.commit()
    # Not locked with 1 trade (first trade is outside of lookback_period)
    assert not freqtrade.protections.global_stop()
    assert not freqtrade.protections.stop_per_pair("XRP/BTC")
    assert not PairLocks.is_pair_locked("XRP/BTC")
    assert not PairLocks.is_global_lock()

    # Add positive trade
    generate_mock_trade(
        "XRP/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.ROI.value,
        min_ago_open=20,
        min_ago_close=10,
        profit_rate=1.15,
        is_short=True,
    )
    Trade.commit()
    assert freqtrade.protections.stop_per_pair("XRP/BTC") != only_per_side
    assert not PairLocks.is_pair_locked("XRP/BTC", side="*")
    assert PairLocks.is_pair_locked("XRP/BTC", side="long") == only_per_side

    generate_mock_trade(
        "XRP/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=110,
        min_ago_close=21,
        profit_rate=0.8,
    )
    Trade.commit()

    # Locks due to 2nd trade
    assert freqtrade.protections.global_stop() != only_per_side
    assert freqtrade.protections.stop_per_pair("XRP/BTC") != only_per_side
    assert PairLocks.is_pair_locked("XRP/BTC", side="long")
    assert PairLocks.is_pair_locked("XRP/BTC", side="*") != only_per_side
    assert not PairLocks.is_global_lock()
    Trade.commit()


@pytest.mark.usefixtures("init_persistence")
def test_MaxDrawdown_ratio_mode(mocker, default_conf, fee, caplog):
    default_conf["_strategy_protections"] = [
        {
            "method": "MaxDrawdown",
            "lookback_period": 1000,
            "stop_duration": 60,
            "trade_limit": 3,
            "max_allowed_drawdown": 0.15,
        }
    ]
    freqtrade = get_patched_freqtradebot(mocker, default_conf)
    message = r"Trading stopped due to Max.*"
    starting_balance = 0.05

    assert not freqtrade.protections.global_stop(starting_balance=starting_balance)
    assert not freqtrade.protections.stop_per_pair("XRP/BTC", starting_balance=starting_balance)
    caplog.clear()

    generate_mock_trade(
        "XRP/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=1000,
        min_ago_close=900,
        profit_rate=1.1,
    )
    generate_mock_trade(
        "ETH/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=1000,
        min_ago_close=900,
        profit_rate=1.1,
    )
    generate_mock_trade(
        "NEO/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=1000,
        min_ago_close=900,
        profit_rate=1.1,
    )
    Trade.commit()
    # No losing trade yet ... so max_drawdown will raise exception
    assert not freqtrade.protections.global_stop(starting_balance=starting_balance)
    assert not freqtrade.protections.stop_per_pair("XRP/BTC", starting_balance=starting_balance)

    generate_mock_trade(
        "XRP/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=500,
        min_ago_close=400,
        profit_rate=0.9,
    )
    # Not locked with one trade
    assert not freqtrade.protections.global_stop(starting_balance=starting_balance)
    assert not freqtrade.protections.stop_per_pair("XRP/BTC", starting_balance=starting_balance)
    assert not PairLocks.is_pair_locked("XRP/BTC")
    assert not PairLocks.is_global_lock()

    generate_mock_trade(
        "XRP/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=1200,
        min_ago_close=1100,
        profit_rate=0.5,
    )
    Trade.commit()

    # Not locked with 1 trade (2nd trade is outside of lookback_period)
    assert not freqtrade.protections.global_stop(starting_balance=starting_balance)
    assert not freqtrade.protections.stop_per_pair("XRP/BTC", starting_balance=starting_balance)
    assert not PairLocks.is_pair_locked("XRP/BTC")
    assert not PairLocks.is_global_lock()
    assert not log_has_re(message, caplog)

    # Winning trade ... (should not lock, does not change drawdown!)
    generate_mock_trade(
        "XRP/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.ROI.value,
        min_ago_open=320,
        min_ago_close=410,
        profit_rate=1.5,
    )
    Trade.commit()
    assert not freqtrade.protections.global_stop(starting_balance=starting_balance)
    assert not PairLocks.is_global_lock()

    caplog.clear()

    # Add additional negative trade, causing a loss of > 15%
    generate_mock_trade(
        "XRP/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.ROI.value,
        min_ago_open=20,
        min_ago_close=10,
        profit_rate=0.2,
    )
    Trade.commit()
    assert not freqtrade.protections.stop_per_pair("XRP/BTC", starting_balance=starting_balance)
    # local lock not supported
    assert not PairLocks.is_pair_locked("XRP/BTC")
    assert freqtrade.protections.global_stop(starting_balance=starting_balance)
    assert PairLocks.is_global_lock()
    assert log_has_re(message, caplog)


@pytest.mark.usefixtures("init_persistence")
def test_MaxDrawdown_equity_mode(mocker, default_conf, fee):
    default_conf["_strategy_protections"] = [
        {
            "method": "MaxDrawdown",
            "lookback_period": 1000,
            "stop_duration": 60,
            "trade_limit": 1,
            "max_allowed_drawdown": 0.01,
            "calculation_mode": "equity",
        }
    ]
    freqtrade = get_patched_freqtradebot(mocker, default_conf)
    starting_balance = 0.01

    assert not freqtrade.protections.global_stop(starting_balance=starting_balance)

    generate_mock_trade(
        "XRP/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=30,
        min_ago_close=10,
        profit_rate=0.5,
    )
    Trade.commit()

    assert freqtrade.protections.global_stop(starting_balance=starting_balance)
    assert PairLocks.is_global_lock()


@pytest.mark.parametrize(
    "calculation_mode,expected_locked",
    [("ratios", True), ("equity", False)],
)
@pytest.mark.usefixtures("init_persistence")
def test_MaxDrawdown_mode_comparison(mocker, default_conf, fee, calculation_mode, expected_locked):
    default_conf["_strategy_protections"] = [
        {
            "method": "MaxDrawdown",
            "lookback_period": 1000,
            "stop_duration": 60,
            "trade_limit": 3,
            "max_allowed_drawdown": 0.15,
            "calculation_mode": calculation_mode,
        }
    ]
    freqtrade = get_patched_freqtradebot(mocker, default_conf)
    starting_balance = 1000.0

    # Same trade sequence for both modes: ratios mode lock expected,equity mode no lock expected.
    generate_mock_trade(
        "XRP/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.ROI.value,
        min_ago_open=120,
        min_ago_close=50,
        profit_rate=1.2,
    )
    generate_mock_trade(
        "ETH/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=80,
        min_ago_close=20,
        profit_rate=0.9,
    )
    generate_mock_trade(
        "NEO/BTC",
        fee.return_value,
        False,
        exit_reason=ExitType.STOP_LOSS.value,
        min_ago_open=40,
        min_ago_close=10,
        profit_rate=0.9,
    )
    Trade.commit()

    lock = freqtrade.protections.global_stop(starting_balance=starting_balance)
    assert bool(lock) is expected_locked
    assert PairLocks.is_global_lock(side="long") is expected_locked


@pytest.mark.parametrize("calculation_mode", ["ratios", "equity"])
@pytest.mark.usefixtures("init_persistence")
def test_MaxDrawdown_threshold_boundary(mocker, default_conf, calculation_mode):
    threshold = 0.15
    default_conf["_strategy_protections"] = [
        {
            "method": "MaxDrawdown",
            "lookback_period": 1000,
            "stop_duration": 60,
            "trade_limit": 1,
            "max_allowed_drawdown": threshold,
            "calculation_mode": calculation_mode,
        }
    ]
    freqtrade = get_patched_freqtradebot(mocker, default_conf)
    handler = next(p for p in freqtrade.protections._protection_handlers if p.name == "MaxDrawdown")
    md_globals = handler._max_drawdown.__globals__

    now = datetime.now(UTC)
    trades_in_window = [
        SimpleNamespace(
            close_date_utc=now - timedelta(minutes=10), close_profit_abs=-1.0, close_profit=-0.1
        )
    ]
    all_closed_trades = [
        SimpleNamespace(
            close_date_utc=now - timedelta(minutes=1500), close_profit_abs=5.0, close_profit=0.2
        ),
        trades_in_window[0],
    ]

    proxy_side_effect = [trades_in_window]
    if calculation_mode == "equity":
        proxy_side_effect.append(all_closed_trades)

    mocker.patch.object(
        md_globals["Trade"],
        "get_trades_proxy",
        side_effect=proxy_side_effect,
    )
    calc_mock = mocker.Mock(
        return_value=SimpleNamespace(relative_account_drawdown=threshold, drawdown_abs=threshold)
    )
    mocker.patch.dict(md_globals, {"calculate_max_drawdown": calc_mock})

    assert not handler.global_stop(datetime.now(UTC), "long", starting_balance=1000.0)
    assert not PairLocks.is_global_lock()


@pytest.mark.parametrize(
    "calculation_mode,expected_value_col,expected_proxy_calls",
    [("ratios", "close_profit", 1), ("equity", "profit_abs", 2)],
)
@pytest.mark.usefixtures("init_persistence")
def test_MaxDrawdown_calculation_mode_dispatch(
    mocker, default_conf, calculation_mode, expected_value_col, expected_proxy_calls
):
    default_conf["_strategy_protections"] = [
        {
            "method": "MaxDrawdown",
            "lookback_period": 1000,
            "stop_duration": 60,
            "trade_limit": 1,
            "max_allowed_drawdown": 0.15,
            "calculation_mode": calculation_mode,
        }
    ]
    freqtrade = get_patched_freqtradebot(mocker, default_conf)
    handler = next(p for p in freqtrade.protections._protection_handlers if p.name == "MaxDrawdown")
    md_globals = handler._max_drawdown.__globals__

    now = datetime.now(UTC)
    trades_in_window = [
        SimpleNamespace(
            close_date_utc=now - timedelta(minutes=10), close_profit_abs=-1.0, close_profit=-0.1
        )
    ]
    all_closed_trades = [
        SimpleNamespace(
            close_date_utc=now - timedelta(minutes=1500), close_profit_abs=5.0, close_profit=0.2
        ),
        trades_in_window[0],
    ]

    proxy_side_effect = [trades_in_window]
    if calculation_mode == "equity":
        proxy_side_effect.append(all_closed_trades)

    proxy_mock = mocker.patch.object(
        md_globals["Trade"],
        "get_trades_proxy",
        side_effect=proxy_side_effect,
    )
    calc_mock = mocker.Mock(
        return_value=SimpleNamespace(relative_account_drawdown=0.0, drawdown_abs=0.0)
    )
    mocker.patch.dict(md_globals, {"calculate_max_drawdown": calc_mock})

    assert not handler.global_stop(datetime.now(UTC), "long", starting_balance=1000.0)

    assert proxy_mock.call_count == expected_proxy_calls
    kwargs = calc_mock.call_args.kwargs
    assert kwargs["value_col"] == expected_value_col

    if calculation_mode == "equity":
        assert kwargs["starting_balance"] == 1005.0
        assert kwargs["relative"] is True
    else:
        assert "starting_balance" not in kwargs
        assert "relative" not in kwargs


@pytest.mark.parametrize(
    "protectionconf,desc_expected,exception_expected",
    [
        (
            {
                "method": "StoplossGuard",
                "lookback_period": 60,
                "trade_limit": 2,
                "stop_duration": 60,
            },
            "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, "
            "2 stoplosses with profit < 0.00% within 60 minutes.'}]",
            None,
        ),
        (
            {"method": "CooldownPeriod", "stop_duration": 60},
            "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period for 60 minutes.'}]",
            None,
        ),
        (
            {"method": "LowProfitPairs", "lookback_period": 60, "stop_duration": 60},
            "[{'LowProfitPairs': 'LowProfitPairs - Low Profit Protection, locks pairs with "
            "profit < 0.0 within 60 minutes.'}]",
            None,
        ),
        (
            {"method": "MaxDrawdown", "lookback_period": 60, "stop_duration": 60},
            "[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading "
            "if drawdown is > 0.0 within 60 minutes.'}]",
            None,
        ),
        (
            {
                "method": "StoplossGuard",
                "lookback_period_candles": 12,
                "trade_limit": 2,
                "required_profit": -0.05,
                "stop_duration": 60,
            },
            "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, "
            "2 stoplosses with profit < -5.00% within 12 candles.'}]",
            None,
        ),
        (
            {"method": "CooldownPeriod", "stop_duration_candles": 5},
            "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period for 5 candles.'}]",
            None,
        ),
        (
            {"method": "LowProfitPairs", "lookback_period_candles": 11, "stop_duration": 60},
            "[{'LowProfitPairs': 'LowProfitPairs - Low Profit Protection, locks pairs with "
            "profit < 0.0 within 11 candles.'}]",
            None,
        ),
        (
            {"method": "MaxDrawdown", "lookback_period_candles": 20, "stop_duration": 60},
            "[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading "
            "if drawdown is > 0.0 within 20 candles.'}]",
            None,
        ),
        (
            {
                "method": "CooldownPeriod",
                "unlock_at": "01:00",
            },
            "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period until 01:00.'}]",
            None,
        ),
        (
            {
                "method": "StoplossGuard",
                "lookback_period_candles": 12,
                "trade_limit": 2,
                "required_profit": -0.05,
                "unlock_at": "01:00",
            },
            "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, "
            "2 stoplosses with profit < -5.00% within 12 candles.'}]",
            None,
        ),
        (
            {"method": "LowProfitPairs", "lookback_period_candles": 11, "unlock_at": "03:00"},
            "[{'LowProfitPairs': 'LowProfitPairs - Low Profit Protection, locks pairs with "
            "profit < 0.0 within 11 candles.'}]",
            None,
        ),
        (
            {"method": "MaxDrawdown", "lookback_period_candles": 20, "unlock_at": "04:00"},
            "[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading "
            "if drawdown is > 0.0 within 20 candles.'}]",
            None,
        ),
    ],
)
def test_protection_manager_desc(
    mocker, default_conf, protectionconf, desc_expected, exception_expected
):
    default_conf["_strategy_protections"] = [protectionconf]
    freqtrade = get_patched_freqtradebot(mocker, default_conf)

    short_desc = str(freqtrade.protections.short_desc())
    assert short_desc == desc_expected
