from datetime import datetime
from zoneinfo import ZoneInfo

from app.sim.liquidity import bid_mark_ok
from app.sim.pricing import option_fill, perp_fill
from app.strategy.clock import (
    can_open_new,
    expiry_blocked_by_one_per_day,
    used_expiry_ymds_for_day,
    window_key,
)
from app.strategy.exits import check_expiry_close, check_exits
from app.strategy.signal import decide

_SH = ZoneInfo("Asia/Shanghai")


def test_signal_buy_call_short_perp() -> None:
    s = decide(20.0, 15.0)
    assert s is not None
    assert s.option_side == "call"
    assert s.perp_side == "short"


def test_signal_buy_put_long_perp() -> None:
    s = decide(10.0, 16.0)
    assert s is not None
    assert s.option_side == "put"
    assert s.perp_side == "long"


def test_signal_equal() -> None:
    assert decide(10.0, 10.0) is None


def test_decide_fixed_long_put() -> None:
    from app.strategy.signal import decide_fixed

    s = decide_fixed(20.0, 15.0, perp_side="long")
    assert s is not None
    assert s.option_side == "put"
    assert s.perp_side == "long"
    assert s.bias == "fixed_long_put"


def test_decide_fixed_short_call() -> None:
    from app.strategy.signal import decide_fixed

    s = decide_fixed(20.0, 15.0, perp_side="short")
    assert s is not None
    assert s.option_side == "call"
    assert s.perp_side == "short"
    assert s.bias == "fixed_short_call"


def test_signal_strike_below_spot_call_short() -> None:
    # 现价 1859、ATM 1850：即使 Put 卖一更高，也走 Call+空
    s = decide(10.0, 20.0, strike=1850, mark_px=1859)
    assert s is not None
    assert s.option_side == "call"
    assert s.perp_side == "short"
    assert s.bias == "strike_below_spot"


def test_signal_strike_above_spot_put_long() -> None:
    # 现价 1859、ATM 1875：即使 Call 卖一更高，也走 Put+多
    s = decide(20.0, 10.0, strike=1875, mark_px=1859)
    assert s is not None
    assert s.option_side == "put"
    assert s.perp_side == "long"
    assert s.bias == "strike_above_spot"


def test_signal_strike_flat_falls_back_to_ask() -> None:
    s = decide(20.0, 15.0, strike=1860, mark_px=1860)
    assert s is not None
    assert s.bias == "call_ask_gt_put"
    assert s.option_side == "call"


def test_exit_fixed_and_premium_multiple() -> None:
    fixed = check_exits(
        net_pnl=15.0,
        exit_mode="fixed_usdt",
        net_profit_target=15,
        premium_exit_multiple=1,
        initial_premium=40,
    )
    assert fixed.reason == "fixed_usdt"
    assert fixed.target == 15

    assert (
        check_exits(
            net_pnl=14.9,
            exit_mode="fixed_usdt",
            net_profit_target=15,
            premium_exit_multiple=1,
            initial_premium=40,
        ).should_close
        is False
    )

    prem = check_exits(
        net_pnl=40.0,
        exit_mode="premium_multiple",
        net_profit_target=15,
        premium_exit_multiple=1,
        initial_premium=40,
    )
    assert prem.reason == "premium_multiple"
    assert prem.target == 40

    half = check_exits(
        net_pnl=20.0,
        exit_mode="premium_multiple",
        net_profit_target=15,
        premium_exit_multiple=0.5,
        initial_premium=40,
    )
    assert half.should_close is True
    assert half.target == 20


def test_perp_pricing() -> None:
    r = perp_fill(side="long", action="open", bid=100, ask=101, qty_eth=1, fee_rate=0.001)
    assert abs(r.fill_px - 101 * 1.001) < 1e-9


def test_option_open_close_pricing() -> None:
    o = option_fill(action="open", bid=10, ask=12, qty_eth=2, fee_rate=0.001)
    assert o.fill_px > 12
    c = option_fill(action="close", bid=10, ask=12, qty_eth=2, fee_rate=0.001)
    assert c.fill_px < 10


def test_window_weekend_skip() -> None:
    # 2026-07-24 周五可开；25/26 周六日不可开
    fri = datetime(2026, 7, 24, 17, 0, tzinfo=_SH)
    sat = datetime(2026, 7, 25, 12, 0, tzinfo=_SH)
    sun = datetime(2026, 7, 26, 10, 0, tzinfo=_SH)
    mon = datetime(2026, 7, 27, 9, 0, tzinfo=_SH)
    assert can_open_new(fri, skip_weekends=True) is True
    assert can_open_new(sat, skip_weekends=True) is False
    assert can_open_new(sun, skip_weekends=True) is False
    assert can_open_new(mon, skip_weekends=True) is True
    assert can_open_new(sat, skip_weekends=False) is True
    assert window_key(fri) == "20260724"


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

    db = Database(tmp_path / "one_exp.db")
    day = datetime(2026, 8, 1, 12, 0, tzinfo=_SH)
    with db._lock:
        db._conn.execute(
            """INSERT INTO groups(group_id, status, expiry_ymd, open_at_ms)
               VALUES ('G-20260801-01','closed','260802',1)"""
        )
        db._conn.execute(
            """INSERT INTO groups(group_id, status, expiry_ymd, open_at_ms)
               VALUES ('G-20260731-01','closed','260801',1)"""
        )
        db._conn.commit()
    used = used_expiry_ymds_for_day(db, day)
    assert used == {"260802"}
    assert expiry_blocked_by_one_per_day("260802", used, enabled=True) is True
    assert expiry_blocked_by_one_per_day("260803", used, enabled=True) is False
    assert expiry_blocked_by_one_per_day("260802", used, enabled=False) is False
    db.close()


def test_bid_mark_deviation_30pct() -> None:
    # |7-10|/10 = 30% → 允许（≤30%）
    ok, _ = bid_mark_ok(bid=7.0, mark=10.0, max_dev_pct=30)
    assert ok is True
    ok2, _ = bid_mark_ok(bid=6.9, mark=10.0, max_dev_pct=30)
    assert ok2 is False
    ok3, why = bid_mark_ok(bid=None, mark=10.0, max_dev_pct=30)
    assert ok3 is False
    assert "买一" in why


def test_expiry_close() -> None:
    assert check_expiry_close(expiry_ms=None).should_close is False
    d = check_expiry_close(expiry_ms=1_000, now_ms=999)
    assert d.should_close is False
    d2 = check_expiry_close(expiry_ms=1_000, now_ms=1_000)
    assert d2.should_close is True
    assert d2.reason == "expiry"
    d3 = check_expiry_close(expiry_ms=1_000, now_ms=1_001)
    assert d3.should_close is True


def test_deep_otm_and_expiry_settle() -> None:
    from app.sim.pricing import is_deep_otm, option_expiry_settle, option_intrinsic

    assert is_deep_otm(option_side="call", strike=1860, spot=1840) is True
    assert is_deep_otm(option_side="call", strike=1860, spot=1882) is False
    assert is_deep_otm(option_side="put", strike=1860, spot=1882) is True
    assert option_intrinsic(option_side="call", strike=1860, spot=1840) == 0.0
    settled = option_expiry_settle(intrinsic=0.0, qty_eth=2.0, fee_rate=0.0005)
    assert settled.fill_px == 0.0
    assert settled.notional == 0.0


def test_pair_from_held_option_inst() -> None:
    from app.exchange.option_ids import flip_option_side, pair_from_option_inst

    put = "ETH-USD_UM-260727-1880-P"
    pair = pair_from_option_inst(put)
    assert pair is not None
    assert pair.strike == 1880
    assert pair.expiry_ymd == "260727"
    assert pair.put_inst_id == put
    assert pair.call_inst_id == "ETH-USD_UM-260727-1880-C"
    assert flip_option_side(put) == pair.call_inst_id

    bn = "ETH-260727-1890-C"
    bp = pair_from_option_inst(bn)
    assert bp is not None
    assert bp.strike == 1890
    assert bp.call_inst_id == bn
    assert bp.put_inst_id == "ETH-260727-1890-P"


def test_option_intrinsic_and_close_bid_floor() -> None:
    from app.sim.pricing import (
        option_expiry_settle,
        option_intrinsic,
        resolve_option_close_bid,
    )

    assert option_intrinsic(option_side="call", strike=1860, spot=1882) == 22.0
    assert option_intrinsic(option_side="put", strike=1860, spot=1882) == 0.0
    assert option_intrinsic(option_side="put", strike=1860, spot=1840) == 20.0

    # 到期：严格按内在价值，无滑点
    settled = option_expiry_settle(intrinsic=22.0, qty_eth=2.0, fee_rate=0.0005)
    assert settled.fill_px == 22.0
    assert settled.slip == 0.0
    assert settled.notional == 44.0
    assert abs(settled.fee - 44.0 * 0.0005) < 1e-12

    # 紧急：对齐 OKX 市价卖，只按买一（垃圾买一不抬到内在价值/标记）
    assert (
        resolve_option_close_bid(
            bid=0.2, mark=22.0, intrinsic=22.0, bypass_liquidity=True
        )
        == 0.2
    )
    # 常规：内在价值地板
    assert (
        resolve_option_close_bid(
            bid=0.2, mark=0.2, intrinsic=22.0, bypass_liquidity=False
        )
        == 22.0
    )
    # 买一高于内在价值，保留买一
    assert (
        resolve_option_close_bid(
            bid=25.0, mark=24.0, intrinsic=22.0, bypass_liquidity=True
        )
        == 25.0
    )
    # 紧急无买一 → 无法定价（不能用标记）
    assert (
        resolve_option_close_bid(
            bid=None, mark=3.0, intrinsic=22.0, bypass_liquidity=True
        )
        is None
    )
