"""期期对冲：选约 / 定仓纯函数测试。"""

from __future__ import annotations

from app.exchange.candles import AmplitudeHL
from app.strategy.oo_selection import (
    pick_otm_call_strike,
    pick_otm_put_strike,
    select_oo_pair,
)
from app.strategy.risk_sizing import compute_oo_sizing


def test_otm_strikes_near_amplitude() -> None:
    strikes = [1800.0, 1850.0, 1900.0, 1950.0, 2000.0, 2050.0, 2100.0]
    assert pick_otm_call_strike(strikes, spot=1950, high=2040) == 2050.0
    assert pick_otm_put_strike(strikes, spot=1950, low=1860) == 1850.0


def test_otm_strikes_reject_beyond_1pct() -> None:
    # 高点 2000，最近虚值 Call 仅 2100（偏离 5%）→ 拒绝
    strikes = [1900.0, 1950.0, 2100.0]
    assert pick_otm_call_strike(strikes, spot=1950, high=2000, max_dev_pct=1.0) is None
    # 低点 1900，最近虚值 Put 仅 1800（偏离 ~5.3%）→ 拒绝
    assert (
        pick_otm_put_strike(
            [1800.0, 1950.0, 2000.0], spot=1950, low=1900, max_dev_pct=1.0
        )
        is None
    )
    # 高点 2095，Call 2100 偏离约 0.24% → 通过
    assert (
        pick_otm_call_strike(strikes, spot=1950, high=2095, max_dev_pct=1.0) == 2100.0
    )


def test_select_oo_pair_same_expiry(tmp_path=None) -> None:
    contracts = []
    for k in (1900, 2000, 2100):
        for side, letter in (("call", "C"), ("put", "P")):
            contracts.append(
                {
                    "expiry_ymd": "260810",
                    "expiry_ms": 1_786_320_000_000,
                    "strike": float(k),
                    "side": letter,
                    "inst_id": f"ETH-{k}-{letter}",
                }
            )
    picked = select_oo_pair(
        contracts,
        spot=2000.0,
        high=2105.0,
        low=1890.0,
        min_hours=1.0,
        max_dev_pct=1.0,
    )
    assert picked is not None
    ymd, _ems, ck, pk, call_i, put_i = picked
    assert ymd == "260810"
    assert ck == 2100.0
    assert pk == 1900.0
    assert "C" in call_i and "P" in put_i


def test_compute_oo_sizing_1_1_and_reward() -> None:
    r = compute_oo_sizing(
        budget=100.0,
        call_ask=5.0,
        put_ask=10.0,  # Put 更贵 → 数量更少
        fee_rate=0.0,
        index_px=2000.0,
        cushion=1.0,  # 测纯平分，不预留
        reward_ratio=2.0,
    )
    assert r.ok
    # 各 50U：Call 50/5=10，Put 50/10=5
    assert r.call_qty_eth == 10.0
    assert r.put_qty_eth == 5.0
    assert abs(float(r.net_profit_target or 0) - 200.0) < 1e-9
    assert abs(float(r.call_premium or 0) - 50.0) < 1e-6
    assert abs(float(r.put_premium or 0) - 50.0) < 1e-6


def test_compute_oo_sizing_equal_asks() -> None:
    r = compute_oo_sizing(
        budget=100.0,
        call_ask=5.0,
        put_ask=5.0,
        fee_rate=0.0,
        index_px=2000.0,
        cushion=0.92,
        reward_ratio=2.0,
    )
    assert r.ok
    # 各腿 46U / 5 = 9.2
    assert r.call_qty_eth == 9.2
    assert r.put_qty_eth == 9.2
    assert abs(float(r.net_profit_target or 0) - 200.0) < 1e-9
    assert float(r.call_premium or 0) + float(r.put_premium or 0) <= 92.0 + 1e-6


def test_amplitude_range_pct() -> None:
    a = AmplitudeHL(high=2030, low=1970, mid=2000, hours=12, bar_count=12)
    assert abs(a.range_pct - 3.0) < 1e-9


def test_preview_oo_exit_budget_times_ratio(tmp_path, monkeypatch) -> None:
    monkeypatch.setenv("MODE", "SIM")
    from app.models.db import Database
    from app.sim.ledger import Ledger
    from app.strategy.risk_sizing import _preview_oo_sizing

    db = Database(tmp_path / "oo_prev.db")
    db.set_setting("hedge_mode", "option_option")
    db.set_setting("sizing_mode", "risk_based")
    db.set_setting("risk_loss_mode", "percent")
    db.set_setting("risk_loss_pct", "1")
    db.set_setting("risk_capital_source", "manual")
    db.set_setting("risk_manual_capital_usdt", "10000")
    db.set_setting("oo_reward_ratio", "2")
    monkeypatch.setattr("app.strategy.risk_sizing.get_db", lambda: db)
    # 无 session 盘口时仍应给出 预算×比
    out = _preview_oo_sizing(db, Ledger(db), {"hedge_mode": "option_option"})
    assert out.get("ok") is True
    assert abs(float(out.get("budget") or 0) - 100.0) < 1e-6
    assert abs(float(out.get("net_profit_target") or 0) - 200.0) < 1e-6
    db.close()


def test_amplitude_max_gate() -> None:
    from app.strategy.oo_selection import build_oo_pick_core

    contracts = []
    for k in (1900, 1975, 2000, 2025, 2100):
        for side, letter in (("call", "C"), ("put", "P")):
            contracts.append(
                {
                    "expiry_ymd": "260810",
                    "expiry_ms": 1_786_320_000_000,
                    "strike": float(k),
                    "side": letter,
                    "inst_id": f"ETH-{k}-{letter}",
                }
            )
    amp = AmplitudeHL(high=2030, low=1970, mid=2000, hours=12, bar_count=12)
    # 3% > 上限 1.5% → 拒
    assert (
        build_oo_pick_core(
            contracts=contracts,
            spot=2000,
            call_ask=5,
            put_ask=5,
            min_hours=1,
            min_leverage=1,
            amplitude_hours=12,
            amplitude_pct=1.5,
            amplitude=amp,
        )
        is None
    )
    # 3% ≤ 上限 3.5%，且 2025/1975 贴高低 ≤1% → 通过
    ok = build_oo_pick_core(
        contracts=contracts,
        spot=2000,
        call_ask=5,
        put_ask=5,
        min_hours=1,
        min_leverage=1,
        amplitude_hours=12,
        amplitude_pct=3.5,
        amplitude=amp,
        max_dev_pct=1.0,
    )
    assert ok is not None
    assert ok.call.strike == 2025.0
    assert ok.put.strike == 1975.0
    # 过滤关闭：振幅 3% 超过 1.5% 上限也可过
    ok_off = build_oo_pick_core(
        contracts=contracts,
        spot=2000,
        call_ask=5,
        put_ask=5,
        min_hours=1,
        min_leverage=1,
        amplitude_hours=12,
        amplitude_pct=1.5,
        amplitude=amp,
        max_dev_pct=1.0,
        amplitude_filter_enabled=False,
    )
    assert ok_off is not None
