from __future__ import annotations

from datetime import datetime
from zoneinfo import ZoneInfo

from app.market.instruments import (
    hours_until_expiry,
    list_eligible_expiry_ymds,
    next_session_expiry_ymd,
    option_leverage,
    parse_option_inst_id,
    pick_atm_strike,
    select_option_pair,
)

_SH = ZoneInfo("Asia/Shanghai")


def test_parse_option_inst_id() -> None:
    y, s, o = parse_option_inst_id("ETH-USD_UM-260725-3500-C")
    assert y == "260725"
    assert s == 3500.0
    assert o == "C"


def test_pick_atm_strike() -> None:
    assert pick_atm_strike([3400, 3500, 3600], 3510) == 3500


def test_pick_itm_or_atm_strike() -> None:
    from app.strategy.selection import is_itm_or_atm, pick_itm_or_atm_strike

    strikes = [3400, 3500, 3600]
    # Call：K≤S，现价 3510 → 3500（平值侧最近）
    assert pick_itm_or_atm_strike(strikes, 3510, option_side="call") == 3500
    # Put：K≥S，现价 3510 → 3600（实值最近；无 3510 档）
    assert pick_itm_or_atm_strike(strikes, 3510, option_side="put") == 3600
    # Put 现价正好 3500 → 平值 3500
    assert pick_itm_or_atm_strike(strikes, 3500, option_side="put") == 3500
    assert is_itm_or_atm(option_side="call", strike=3500, mark_px=3510)
    assert not is_itm_or_atm(option_side="call", strike=3600, mark_px=3510)
    assert is_itm_or_atm(option_side="put", strike=3600, mark_px=3510)
    assert not is_itm_or_atm(option_side="put", strike=3400, mark_px=3510)


def test_select_option_pair_itm_put() -> None:
    rows = [
        {"instId": "ETH-USD_UM-260725-3490-C", "state": "live"},
        {"instId": "ETH-USD_UM-260725-3490-P", "state": "live"},
        {"instId": "ETH-USD_UM-260725-3500-C", "state": "live"},
        {"instId": "ETH-USD_UM-260725-3500-P", "state": "live"},
        {"instId": "ETH-USD_UM-260725-3510-C", "state": "live"},
        {"instId": "ETH-USD_UM-260725-3510-P", "state": "live"},
    ]
    # 标的 3502：Put 实/平 → 3510（≥3502 最近）
    pair = select_option_pair(rows, mark_px=3502, expiry_ymd="260725", option_side="put")
    assert pair is not None
    assert pair.strike == 3510
    # Call 实/平 → 3500（≤3502 最近）
    pair_c = select_option_pair(
        rows, mark_px=3502, expiry_ymd="260725", option_side="call"
    )
    assert pair_c is not None
    assert pair_c.strike == 3500


def test_next_session_expiry_before_open() -> None:
    now = datetime(2026, 7, 24, 15, 0, tzinfo=_SH)
    assert next_session_expiry_ymd(now) == "260724"


def test_next_session_expiry_after_open() -> None:
    now = datetime(2026, 7, 24, 16, 0, tzinfo=_SH)
    assert next_session_expiry_ymd(now) == "260725"


def test_select_option_pair_atm() -> None:
    rows = [
        {"instId": "ETH-USD_UM-260725-3490-C", "state": "live"},
        {"instId": "ETH-USD_UM-260725-3490-P", "state": "live"},
        {"instId": "ETH-USD_UM-260725-3500-C", "state": "live"},
        {"instId": "ETH-USD_UM-260725-3500-P", "state": "live"},
        {"instId": "ETH-USD_UM-260726-3500-C", "state": "live"},
        {"instId": "ETH-USD_UM-260726-3500-P", "state": "live"},
    ]
    pair = select_option_pair(rows, mark_px=3502, expiry_ymd="260725")
    assert pair is not None
    assert pair.strike == 3500
    assert pair.call_inst_id.endswith("-3500-C")
    assert pair.put_inst_id.endswith("-3500-P")


def test_eligible_skips_short_ttm() -> None:
    # 2026-07-25 08:30 SH：当日 16:00 到期仅约 7.5h，应跳过 260725，选 260726
    now = datetime(2026, 7, 25, 8, 30, tzinfo=_SH)
    rows = [
        {"instId": "ETH-USD_UM-260725-1850-C", "state": "live"},
        {"instId": "ETH-USD_UM-260725-1850-P", "state": "live"},
        {"instId": "ETH-USD_UM-260726-1850-C", "state": "live"},
        {"instId": "ETH-USD_UM-260726-1850-P", "state": "live"},
    ]
    assert hours_until_expiry("260725", now) < 12
    assert hours_until_expiry("260726", now) >= 12
    eligible = list_eligible_expiry_ymds(rows, min_hours=12, now=now)
    assert eligible == ["260726"]
    pair = select_option_pair(rows, mark_px=1853, min_hours=12, now=now)
    assert pair is not None
    assert pair.expiry_ymd == "260726"


def test_option_leverage() -> None:
    assert abs((option_leverage(1850, 18.5) or 0) - 100) < 1e-9
    assert option_leverage(1850, 0) is None


def test_atm_allows_open_offset() -> None:
    from app.strategy.selection import atm_allows_open, atm_open_offset

    assert atm_open_offset(1850, 1852.5) == 2.5
    # 默认关闭：不限制
    assert atm_allows_open(1850, 1860, max_offset=3, enabled=False) is True
    assert atm_allows_open(1850, 1852.5, max_offset=3, enabled=True) is True
    assert atm_allows_open(1850, 1854, max_offset=3, enabled=True) is False
    assert atm_allows_open(1850, 1860, max_offset=3, enabled=True) is False
