"""P0 实盘 SoT：closing 状态机、紧急期期、recover Put、到期无 intrinsic。"""

from __future__ import annotations

from types import SimpleNamespace

from app.sim.matcher import BLOCKING_STATUSES


def test_closing_in_blocking_statuses() -> None:
    assert "closing" in BLOCKING_STATUSES


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

    db = Database(tmp_path / "c.db")
    m = Matcher(db)
    with db._lock:
        db._conn.execute(
            """UPDATE positions SET group_id=?, option_inst_id=?, option2_inst_id=?,
                status='closing', hedge_mode='option_option' WHERE id=1""",
            ("G1", "C", "P"),
        )
        db._conn.commit()
    assert m.has_open_position() is True
    db.close()


def test_recover_opening_refuses_orphan_put(monkeypatch) -> None:
    monkeypatch.setenv("MODE", "LIVE")
    from app.config import get_settings

    get_settings.cache_clear()
    import app.live.reconcile as rec
    from app.live.reconcile import recover_stuck_opening

    class _Ex:
        def current_position(self):
            return {
                "status": "opening",
                "group_id": "G-oo",
                "option_inst_id": "ETH-CALL",
                "perp_side": "oo_put:ETH-PUT",
                "option_qty_eth": 1,
                "option_qty_contracts": 100,
            }

        @property
        def db(self):
            return SimpleNamespace()

        def _client(self):
            return object()

    def fake_opt(_c, inst):
        if "PUT" in inst:
            return 5.0
        return 0.0

    monkeypatch.setattr(rec, "_executor_client_and_exchange", lambda _e: (object(), "okx"))
    monkeypatch.setattr(rec, "exchange_option_abs_size", fake_opt)
    monkeypatch.setattr(rec, "exchange_perp_abs_size", lambda *_a, **_k: 0.0)
    monkeypatch.setattr(
        rec, "resolve_perp_inst_id", lambda *_a, **_k: "ETH-USDT-SWAP"
    )
    r = recover_stuck_opening(_Ex())
    assert r is not None
    assert r.ok is False
    assert "Put" in (r.detail or "")
    get_settings.cache_clear()


def test_expiry_fill_zero_not_intrinsic(monkeypatch, tmp_path) -> None:
    monkeypatch.setenv("MODE", "LIVE")
    from app.config import get_settings

    get_settings.cache_clear()
    from app.live.executor import OkxLiveExecutor
    from app.models.db import Database

    db = Database(tmp_path / "e.db")
    ex = OkxLiveExecutor(db)
    monkeypatch.setattr(ex, "_guard_live", lambda: None)
    with db._lock:
        db._conn.execute(
            """UPDATE positions SET
                group_id=?, perp_side=?, perp_qty_eth=?, perp_entry_px=?,
                option_inst_id=?, option_side=?, option_qty_eth=?, option_qty_contracts=?,
                option_entry_px=?, status='open' WHERE id=1""",
            ("G-e", "short", 4.0, 2000.0, "ETH-OPT", "call", 1.0, 100.0, 20.0),
        )
        db._conn.execute(
            """INSERT INTO groups(group_id, status, option_inst_id, perp_inst_id, strike, open_at_ms)
               VALUES (?,?,?,?,?,?)""",
            ("G-e", "open", "ETH-OPT", "ETH-USDT-SWAP", 1900.0, 1),
        )
        db._conn.commit()

    class _C:
        def get_ct_val(self, *_a, **_k):
            return 0.01

        def get_perp_pos_sz(self, *_a, **_k):
            return 400.0

        def place_market(self, *, inst_id, side, sz, **_k):
            return SimpleNamespace(avg_px=2010.0, fee=0.1, sz=float(sz))

    monkeypatch.setattr(ex, "_client", lambda: _C())
    monkeypatch.setattr(
        "app.live.executor.exchange_option_abs_size", lambda *_a, **_k: 2.0
    )
    monkeypatch.setattr(ex, "_group_strike", lambda *_a, **_k: 1900.0)
    monkeypatch.setattr(ex, "_close_spot_px", lambda *_a, **_k: 1950.0)
    monkeypatch.setattr(
        "app.live.executor.get_session",
        lambda: SimpleNamespace(snapshot=lambda: {}),
    )
    monkeypatch.setattr(
        "app.live.executor.resolve_perp_inst_id",
        lambda *_a, **_k: "ETH-USDT-SWAP",
    )
    monkeypatch.setattr(
        "app.live.live_pnl.reconcile_closed_group_pnl",
        lambda **_k: 0.0,
    )

    r = ex.close_group(reason="expiry", bypass_liquidity=True)
    assert r.ok, r.detail
    row = db.fetchone(
        "SELECT fill_px, notional FROM fills WHERE group_id=? AND leg='option' AND action='close'",
        ("G-e",),
    )
    assert row is not None
    assert float(row["fill_px"]) == 0.0
    assert float(row["notional"] or 0) == 0.0
    db.close()
    get_settings.cache_clear()


def test_binance_fetch_balances_maps_usdt(monkeypatch) -> None:
    from app.live.binance_trade import BinanceTradeClient

    c = BinanceTradeClient.__new__(BinanceTradeClient)

    def _signed(client, method, path, params=None):
        if "fapi" in str(getattr(client, "base_url", "")) or path.startswith("/fapi"):
            return [{"asset": "USDT", "availableBalance": "100.5"}]
        if "marginAccount" in path:
            return {"asset": [{"asset": "USDT", "available": "80"}]}
        return []

    c._signed = _signed  # type: ignore
    c._fapi = SimpleNamespace(base_url="https://fapi")
    c._eapi = SimpleNamespace(base_url="https://eapi")

    # simpler: patch by path
    def signed2(_client, method, path, params=None):
        if path == "/fapi/v2/balance":
            return [{"asset": "USDT", "availableBalance": "100.5"}]
        if path == "/eapi/v1/marginAccount":
            return {"asset": [{"asset": "USDT", "available": "80"}]}
        return []

    c._signed = signed2  # type: ignore
    bal = BinanceTradeClient.fetch_balances(c)
    assert bal["trading_usdt"] == 100.5
    assert bal["trading_usdc"] == 100.5  # mapped from USDT when no USDC
