"""以损定仓纯函数测试。"""

from __future__ import annotations

from app.strategy.risk_sizing import (
    BASE_EXIT_USDT,
    BASE_OPTION_ETH,
    BASE_PERP_ETH,
    compute_k,
    floor_k_1dp,
    normalize_risk_leverage_basis,
    resolve_sizing_option_ask,
    unit_cost,
)


def test_floor_k_1dp() -> None:
    assert floor_k_1dp(1.29) == 1.2
    assert floor_k_1dp(0.19) == 0.1
    assert floor_k_1dp(0.09) == 0.0
    assert floor_k_1dp(2.0) == 2.0


def test_compute_k_scales_1_2_15() -> None:
    # I=2000, A=20, fee=0.0005 → unit = 2*20 + 2000*0.0005*3 = 40 + 3 = 43
    # budget=43 → k=1.0
    r = compute_k(budget=43.0, index_px=2000.0, option_ask=20.0, fee_rate=0.0005)
    assert r.ok
    assert r.k == 1.0
    assert r.perp_qty_eth == BASE_PERP_ETH
    assert r.option_qty_eth == BASE_OPTION_ETH
    assert r.net_profit_target == BASE_EXIT_USDT
    assert r.max_loss is not None and r.max_loss <= 43.0 + 1e-6
    assert r.budget == 43.0


def test_compute_k_custom_units() -> None:
    # option_unit=4 → premium unit = 20*4=80; fee=3; cost=83; budget=83 → k=1
    r = compute_k(
        budget=83.0,
        index_px=2000.0,
        option_ask=20.0,
        fee_rate=0.0005,
        perp_unit=0.5,
        option_unit=4.0,
        exit_unit=30.0,
    )
    assert r.ok
    assert r.k == 1.0
    assert r.perp_qty_eth == 0.5
    assert r.option_qty_eth == 4.0
    assert r.net_profit_target == 30.0


def test_money_rounds_2dp() -> None:
    r = compute_k(budget=50.123456, index_px=1900.0, option_ask=18.5, fee_rate=0.0005)
    assert r.ok
    assert r.budget == round(50.123456, 2)
    assert r.max_loss is not None
    assert abs(r.max_loss * 100 - round(r.max_loss * 100)) < 1e-9


def test_compute_k_never_exceeds_budget() -> None:
    r = compute_k(budget=50.0, index_px=1900.0, option_ask=18.5, fee_rate=0.0005)
    assert r.ok
    assert r.k is not None
    assert abs(r.k * 10 - round(r.k * 10)) < 1e-9  # 一位小数
    assert r.max_loss is not None and r.max_loss <= 50.0 + 1e-6
    assert r.perp_qty_eth == round(1.0 * r.k, 4)
    assert r.option_qty_eth == round(2.0 * r.k, 4)
    assert r.net_profit_target == round(15.0 * r.k, 4)


def test_compute_k_too_small() -> None:
    # unit≈43, budget=2 → k_raw≪0.1
    r = compute_k(budget=2.0, index_px=2000.0, option_ask=20.0, fee_rate=0.0005)
    assert not r.ok
    assert "最小仓" in r.detail or "k=" in r.detail


def test_unit_cost() -> None:
    assert abs(unit_cost(index_px=2000, option_ask=20, fee_rate=0.0005) - 43.0) < 1e-9


def test_normalize_risk_leverage_basis() -> None:
    assert normalize_risk_leverage_basis("actual") == "actual"
    assert normalize_risk_leverage_basis("selection") == "selection"
    assert normalize_risk_leverage_basis("min_option_leverage") == "selection"
    assert normalize_risk_leverage_basis("weird", default="selection") == "selection"


def test_resolve_sizing_ask_selection_vs_actual() -> None:
    # 指数 2000、选约杠杆 100 → 隐含卖一 20；实际卖一更便宜 10
    sel_ask, basis = resolve_sizing_option_ask(
        index_px=2000.0,
        option_ask=10.0,
        leverage_basis="selection",
        min_option_leverage=100.0,
    )
    assert basis == "selection"
    assert abs(sel_ask - 20.0) < 1e-9

    act_ask, basis2 = resolve_sizing_option_ask(
        index_px=2000.0,
        option_ask=10.0,
        leverage_basis="actual",
        min_option_leverage=100.0,
    )
    assert basis2 == "actual"
    assert abs(act_ask - 10.0) < 1e-9


def test_selection_basis_yields_smaller_k_when_ask_cheap() -> None:
    # 预算 43：选约隐含 ask=20 → k=1；若用实际 ask=10 → 单位成本更小 → k 更大
    r_sel = compute_k(budget=43.0, index_px=2000.0, option_ask=20.0, fee_rate=0.0005)
    r_act = compute_k(budget=43.0, index_px=2000.0, option_ask=10.0, fee_rate=0.0005)
    assert r_sel.ok and r_act.ok
    assert r_sel.k == 1.0
    assert r_act.k is not None and r_act.k > r_sel.k


def test_compute_risk_sizing_respects_basis(tmp_path, monkeypatch) -> None:
    monkeypatch.setenv("MODE", "SIM")
    from app.models.db import Database
    from app.strategy.risk_sizing import compute_risk_sizing

    db = Database(tmp_path / "risk_basis.db")
    db.set_setting("sizing_mode", "risk_based")
    db.set_setting("risk_loss_mode", "absolute")
    db.set_setting("risk_loss_usdt", "43")
    db.set_setting("fee_rate", "0.0005")
    db.set_setting("min_option_leverage", "100")
    db.set_setting("risk_perp_unit", "1")
    db.set_setting("risk_option_unit", "2")
    db.set_setting("risk_exit_unit", "15")

    db.set_setting("risk_leverage_basis", "selection")
    r1 = compute_risk_sizing(index_px=2000.0, option_ask=10.0, db=db)
    assert r1.ok
    assert r1.leverage_basis == "selection"
    assert r1.k == 1.0
    assert r1.actual_option_ask == 10.0
    assert r1.option_ask == 20.0

    db.set_setting("risk_leverage_basis", "actual")
    r2 = compute_risk_sizing(index_px=2000.0, option_ask=10.0, db=db)
    assert r2.ok
    assert r2.leverage_basis == "actual"
    assert r2.k is not None and r2.k > 1.0
    assert r2.option_ask == 10.0
    db.close()
