import json
from datetime import UTC, datetime

import pytest

from freqtrade.persistence.trade_model import LocalTrade, Trade
from tests.conftest import create_mock_trades_usdt


@pytest.mark.usefixtures("init_persistence")
def test_trade_fromjson():
    """Test the Trade.from_json() method."""
    trade_string = """{
        "trade_id": 25,
        "pair": "ETH/USDT",
        "base_currency": "ETH",
        "quote_currency": "USDT",
        "is_open": false,
        "exchange": "binance",
        "amount": 407.0,
        "amount_requested": 102.92547026,
        "stake_amount": 102.7494348,
        "strategy": "SampleStrategy55",
        "buy_tag": "Strategy2",
        "enter_tag": "Strategy2",
        "timeframe": 5,
        "fee_open": 0.001,
        "fee_open_cost": 0.1027494,
        "fee_open_currency": "ETH",
        "fee_close": 0.001,
        "fee_close_cost": 0.1054944,
        "fee_close_currency": "USDT",
        "open_date": "2022-10-18 09:12:42",
        "open_timestamp": 1666084362912,
        "open_rate": 0.2518998249562391,
        "open_rate_requested": 0.2516,
        "open_trade_value": 102.62575199,
        "close_date": "2022-10-18 09:45:22",
        "close_timestamp": 1666086322208,
        "realized_profit": 2.76315361,
        "close_rate": 0.2592,
        "close_rate_requested": 0.2592,
        "close_profit": 0.026865,
        "close_profit_pct": 2.69,
        "close_profit_abs": 2.76315361,
        "trade_duration_s": 1959,
        "trade_duration": 32,
        "profit_ratio": 0.02686,
        "profit_pct": 2.69,
        "profit_abs": 2.76315361,
        "sell_reason": "no longer good",
        "exit_reason": "no longer good",
        "exit_order_status": "closed",
        "stop_loss_abs": 0.1981,
        "stop_loss_ratio": -0.216,
        "stop_loss_pct": -21.6,
        "stoploss_last_update": "2022-10-18 09:13:42",
        "stoploss_last_update_timestamp": 1666077222000,
        "initial_stop_loss_abs": 0.1981,
        "initial_stop_loss_ratio": -0.216,
        "initial_stop_loss_pct": -21.6,
        "min_rate": 0.2495,
        "max_rate": 0.2592,
        "leverage": 1.0,
        "interest_rate": 0.0,
        "liquidation_price": null,
        "is_short": false,
        "trading_mode": "spot",
        "funding_fees": 0.0,
        "amount_precision": 1.0,
        "price_precision": 3.0,
        "precision_mode": 2,
        "contract_size": 1.0,
        "open_order_id": null,
        "orders": [
            {
                "amount": 102.0,
                "safe_price": 0.2526,
                "ft_order_side": "buy",
                "order_filled_timestamp": 1666084370887,
                "ft_is_entry": true,
                "pair": "ETH/USDT",
                "order_id": "78404228",
                "status": "closed",
                "average": 0.2526,
                "cost": 25.7652,
                "filled": 102.0,
                "is_open": false,
                "order_date": "2022-10-18 09:12:42",
                "order_timestamp": 1666084362684,
                "order_filled_date": "2022-10-18 09:12:50",
                "order_type": "limit",
                "price": 0.2526,
                "remaining": 0.0
            },
            {
                "amount": 102.0,
                "safe_price": 0.2517,
                "ft_order_side": "buy",
                "order_filled_timestamp": 1666084379056,
                "ft_is_entry": true,
                "pair": "ETH/USDT",
                "order_id": "78405139",
                "status": "closed",
                "average": 0.2517,
                "cost": 25.6734,
                "filled": 102.0,
                "is_open": false,
                "order_date": "2022-10-18 09:12:57",
                "order_timestamp": 1666084377681,
                "order_filled_date": "2022-10-18 09:12:59",
                "order_type": "limit",
                "price": 0.2517,
                "remaining": 0.0
            },
            {
                "amount": 102.0,
                "safe_price": 0.2517,
                "ft_order_side": "buy",
                "order_filled_timestamp": 1666084389644,
                "ft_is_entry": true,
                "pair": "ETH/USDT",
                "order_id": "78405265",
                "status": "closed",
                "average": 0.2517,
                "cost": 25.6734,
                "filled": 102.0,
                "is_open": false,
                "order_date": "2022-10-18 09:13:03",
                "order_timestamp": 1666084383295,
                "order_filled_date": "2022-10-18 09:13:09",
                "order_type": "limit",
                "price": 0.2517,
                "remaining": 0.0
            },
            {
                "amount": 102.0,
                "safe_price": 0.2516,
                "ft_order_side": "buy",
                "order_filled_timestamp": 1666084723521,
                "ft_is_entry": true,
                "pair": "ETH/USDT",
                "order_id": "78405395",
                "status": "closed",
                "average": 0.2516,
                "cost": 25.6632,
                "filled": 102.0,
                "is_open": false,
                "order_date": "2022-10-18 09:13:13",
                "order_timestamp": 1666084393920,
                "order_filled_date": "2022-10-18 09:18:43",
                "order_type": "limit",
                "price": 0.2516,
                "remaining": 0.0
            },
            {
                "amount": 407.0,
                "safe_price": 0.2592,
                "ft_order_side": "sell",
                "order_filled_timestamp": 1666086322198,
                "ft_is_entry": false,
                "pair": "ETH/USDT",
                "order_id": "78432649",
                "status": "closed",
                "average": 0.2592,
                "cost": 105.4944,
                "filled": 407.0,
                "is_open": false,
                "order_date": "2022-10-18 09:45:21",
                "order_timestamp": 1666086321435,
                "order_filled_date": "2022-10-18 09:45:22",
                "order_type": "market",
                "price": 0.2592,
                "remaining": 0.0,
                "funding_fee": -0.055
            }
        ]
    }"""
    trade = Trade.from_json(trade_string)
    Trade.session.add(trade)
    Trade.commit()

    assert trade.id == 25
    assert trade.pair == "ETH/USDT"
    assert trade.open_date_utc == datetime(2022, 10, 18, 9, 12, 42, tzinfo=UTC)
    assert isinstance(trade.open_date, datetime)
    assert trade.exit_reason == "no longer good"
    assert trade.realized_profit == 2.76315361
    assert trade.precision_mode == 2
    assert trade.amount_precision == 1.0
    assert trade.contract_size == 1.0

    assert len(trade.orders) == 5
    last_o = trade.orders[-1]
    assert last_o.order_filled_utc == datetime(2022, 10, 18, 9, 45, 22, tzinfo=UTC)
    assert isinstance(last_o.order_date, datetime)
    assert last_o.funding_fee == -0.055


@pytest.mark.usefixtures("init_persistence")
def test_trade_serialize_load_back(fee):
    create_mock_trades_usdt(fee, None)

    t = Trade.get_trades([Trade.id == 1]).first()
    assert t.id == 1
    t.funding_fees = 0.025
    t.orders[0].funding_fee = 0.0125
    assert len(t.orders) == 2
    Trade.commit()

    tjson = t.to_json(False)
    assert isinstance(tjson, dict)
    trade_string = json.dumps(tjson)
    trade = Trade.from_json(trade_string)

    assert trade.id == t.id
    assert trade.funding_fees == t.funding_fees
    assert len(trade.orders) == len(t.orders)
    assert trade.orders[0].funding_fee == t.orders[0].funding_fee
    excluded = [
        "trade_id",
        "quote_currency",
        "open_timestamp",
        "close_timestamp",
        "realized_profit_ratio",
        "close_profit_pct",
        "trade_duration_s",
        "trade_duration",
        "profit_ratio",
        "profit_pct",
        "profit_abs",
        "stop_loss_abs",
        "initial_stop_loss_abs",
        "open_fill_date",
        "open_fill_timestamp",
        "orders",
    ]
    failed = []
    # Ensure all attributes written can be read.
    for obj, value in tjson.items():
        if obj in excluded:
            continue
        tattr = getattr(trade, obj, None)
        if isinstance(tattr, datetime):
            tattr = tattr.strftime("%Y-%m-%d %H:%M:%S")
        if tattr != value:
            failed.append((obj, tattr, value))

    assert tjson.get("trade_id") == trade.id
    assert tjson.get("quote_currency") == trade.stake_currency
    assert tjson.get("stop_loss_abs") == trade.stop_loss
    assert tjson.get("initial_stop_loss_abs") == trade.initial_stop_loss

    excluded_o = [
        "order_filled_timestamp",
        "ft_is_entry",
        "pair",
        "is_open",
        "order_timestamp",
    ]
    order_obj = trade.orders[0]
    for obj, value in tjson["orders"][0].items():
        if obj in excluded_o:
            continue
        tattr = getattr(order_obj, obj, None)
        if isinstance(tattr, datetime):
            tattr = tattr.strftime("%Y-%m-%d %H:%M:%S")
        if tattr != value:
            failed.append((obj, tattr, value))

    assert tjson["orders"][0]["pair"] == order_obj.ft_pair
    assert not failed

    trade2 = LocalTrade.from_json(trade_string)
    assert len(trade2.orders) == len(t.orders)

    trade3 = LocalTrade.from_json(trade_string)
    assert len(trade3.orders) == len(t.orders)


@pytest.mark.usefixtures("init_persistence")
def test_trade_fromjson_backtesting():
    """
    trade.from_json should be able to load a trade output via backtesting.
    """
    trade_string = """
        {
             "pair":"XRP/USDT:USDT",
             "stake_amount":3015.294001,
             "max_stake_amount":60305.88002,
             "amount":94612.3,
             "open_date":"2024-03-02 10:40:00+00:00",
             "close_date":"2024-03-02 11:10:00+00:00",
             "open_rate":0.6374,
             "close_rate":0.6399,
             "fee_open":0.0005,
             "fee_close":0.0005,
             "trade_duration":30,
             "profit_ratio":-0.09853216535933962,
             "profit_abs":-296.95489539,
             "exit_reason":"trailing_stop_loss",
             "initial_stop_loss_abs":0.6689,
             "initial_stop_loss_ratio":-0.99,
             "stop_loss_abs":0.6399,
             "stop_loss_ratio":-0.3368287257705749,
             "min_rate":0.6294,
             "max_rate":0.6421,
             "is_open":false,
             "enter_tag":"[0.6373, 0.5993, 0.64]",
             "leverage":20,
             "is_short":true,
             "open_timestamp":1709376000000,
             "close_timestamp":1709377800000,
             "orders":[
                {
                   "amount":94612.3,
                   "safe_price":0.6374,
                   "ft_order_side":"sell",
                   "order_filled_timestamp":1709376000000,
                   "ft_is_entry":true,
                   "ft_order_tag":"[0.6373, 0.5993, 0.64]",
                   "cost":60336.032960009994
                },
                {
                   "amount":94612.3,
                   "safe_price":0.6399,
                   "ft_order_side":"buy",
                   "order_filled_timestamp":1709377800000,
                   "ft_is_entry":false,
                   "ft_order_tag":"trailing_stop_loss",
                   "cost":60572.681975385
                }
             ]
          }
        """

    trade = Trade.from_json(trade_string)
    Trade.session.add(trade)
    Trade.commit()

    # Trade-id not given - use first available
    assert trade.id == 1