"""网格执行器与指标的正确性测试（合成数据，不依赖网络）。"""
import sys
from pathlib import Path

import pandas as pd
import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from bot.executors.base import ExecutionContext
from bot.executors.grid import GridConfig, GridExecutor, LevelState
from bot.models import CloseType, Side
from bot import indicators as ind


def mk_candle(ts, o, h, l, c, v=100.0):
    return pd.Series({"ts": ts, "open": o, "high": h, "low": l, "close": c, "volume": v})


def mk_grid(side=Side.BUY, center=100.0, step=2.0):
    cfg = GridConfig(center=center, step=step, total_margin=120.0, leverage=2.0)
    return GridExecutor("TEST/USDT:USDT", side, cfg)


class TestGridExecutor:
    def test_long_level_fill_and_tp(self):
        ex = mk_grid()
        ctx = ExecutionContext(fee_rate=0.00035, wallet=1000.0)
        # 第一层挂单价 98（center-step），K 线下影触及
        ex.on_candle(mk_candle(0, 100, 100.5, 97.5, 99.8), ctx)
        assert ex.levels[0].state is LevelState.OPEN
        assert ex.levels[1].state is LevelState.PENDING
        assert ex.position.amount > 0
        # 反弹触及 TP（98+2=100）
        ex.on_candle(mk_candle(300, 99.8, 100.5, 99.5, 100.2), ctx)
        assert ex.levels[0].state is LevelState.CLOSED
        assert ex.trades[0].close_type is CloseType.TAKE_PROFIT
        assert ex.trades[0].pnl_quote > 0     # 反弹一格，扣费后仍为正
        assert not ex.is_active or ex.position.amount == 0

    def test_boundary_breach_closes_all(self):
        ex = mk_grid()
        ctx = ExecutionContext(fee_rate=0.00035, wallet=1000.0)
        ex.on_candle(mk_candle(0, 100, 100.5, 97.5, 98.0), ctx)   # 层1 成交
        floor = ex.floor                                           # 100-3.25*2=93.5
        ex.on_candle(mk_candle(300, 98, 98.2, 92.0, 93.0), ctx)   # 收在 floor 下
        assert not ex.is_active
        assert ex.close_type is CloseType.BOUNDARY_BREACH
        assert ex.position.amount == 0
        # 层1 亏损平仓；层2（96）也可能已成交并同价平仓
        assert all(t.close_type is CloseType.BOUNDARY_BREACH for t in ex.trades)

    def test_no_fill_recenter_timeout(self):
        ex = mk_grid()
        ex.cfg.recenter_candles = 3
        ctx = ExecutionContext(fee_rate=0.00035, wallet=1000.0)
        for i in range(3):
            # 价格一直在中心上方，所有挂单（98/96/94/92）不触及
            ex.on_candle(mk_candle(i * 300, 101, 101.5, 100.5, 101.2), ctx)
        assert not ex.is_active
        assert not ex.trades

    def test_inventory_decreasing_weights(self):
        ex = mk_grid()
        amounts = [lv.amount for lv in ex.levels]
        # 权重 3:2:1:1 → 第1层数量最大，逐层递减（价格影响小，此处近似）
        assert amounts[0] > amounts[2]

    def test_short_grid_mirror(self):
        ex = mk_grid(side=Side.SELL)
        ctx = ExecutionContext(fee_rate=0.00035, wallet=1000.0)
        # 第一层挂卖价 102，上影触及
        ex.on_candle(mk_candle(0, 100, 102.5, 99.5, 101.0), ctx)
        assert ex.levels[0].state is LevelState.OPEN
        # 回落到 100 止盈
        ex.on_candle(mk_candle(300, 101, 101.2, 99.5, 100.0), ctx)
        assert ex.trades and ex.trades[0].pnl_quote > 0


class TestIndicators:
    def test_atr_positive_and_causal(self):
        df = pd.DataFrame({
            "high": [10, 11, 12, 11, 12, 13],
            "low": [9, 9.5, 10.5, 10, 10.5, 11],
            "close": [9.5, 10.5, 11, 10.5, 11.5, 12],
        })
        a = ind.atr(df, 3)
        assert (a.dropna() > 0).all()
        # 因果性：截断后前 N 项不变
        a2 = ind.atr(df.iloc[:5], 3)
        pd.testing.assert_series_equal(a.iloc[:5], a2)

    def test_donchian_excludes_current(self):
        df = pd.DataFrame({"high": [1, 5, 2, 3, 4], "low": [1, 1, 1, 1, 1]})
        dh = ind.donchian_high(df, 2)
        assert dh.iloc[2] == 5      # 前两根(1,5)的最大
        assert dh.iloc[3] == 5      # 前两根(5,2)
        assert dh.iloc[4] == 3      # 前两根(2,3)
