"""Circuit-breaker venue-lock (2026-06-12): dopo lock_trip reject 'locked'
consecutivi le APERTURE sono sospese senza toccare l'API (i worker seguono il
path REAL_OPEN_FAIL/sim_fallback); le chiusure si tentano sempre; il probe a
fine cooldown riarma o resetta. Nessuna rete: CerberoClient stubbato."""
import time

from src.live.execution import ExecutionClient


class _Venue:
    """Stub CerberoClient: locked finche' .locked=True, poi ordini ok."""
    def __init__(self):
        self.locked = True
        self.place_calls = 0

    def place_order(self, instrument, side, amount, order_type="market",
                    price=None, label=None, reduce_only=False):
        self.place_calls += 1
        if self.locked:
            return {"state": "error", "error": "locked_by_admin"}
        return {"order": {"order_id": "o1", "order_state": "filled",
                          "filled_amount": amount, "average_price": 100.0},
                "trades": [{"amount": amount, "price": 100.0, "fee": 0.0}]}

    def get_ticker(self, instrument):
        return {"mark_price": 100.0}

    def get_trade_history(self, limit=50, instrument_name=None):
        return []


def _ec(monkeypatch, sent):
    monkeypatch.setattr("src.live.telegram_notifier.notify_event",
                        lambda ev, data: sent.append((ev, data)))
    venue = _Venue()
    ec = ExecutionClient(client=venue)
    return ec, venue


INST = "ETH_USDC-PERPETUAL"


def test_breaker_trips_and_blocks_opens(monkeypatch):
    sent = []
    ec, venue = _ec(monkeypatch, sent)
    for _ in range(ec.lock_trip):
        f = ec.open(INST, "buy", 50.0)
        assert not f.verified
    assert venue.place_calls == ec.lock_trip and ec.lock_blocked()
    assert [e for e, _ in sent] == ["VENUE_LOCK"]          # un alert al trip
    # aperture sospese: NESSUNA chiamata API, nota dedicata
    f = ec.open(INST, "buy", 50.0)
    assert venue.place_calls == ec.lock_trip
    assert "venue_lock_breaker" in f.notes
    assert sent == sent[:1]                                # niente spam


def test_closes_still_attempted_and_refresh_cooldown(monkeypatch):
    sent = []
    ec, venue = _ec(monkeypatch, sent)
    for _ in range(ec.lock_trip):
        ec.open(INST, "buy", 50.0)
    ec._net_close_allowance = lambda *a, **k: 0.0          # niente fallback netting
    before = ec._lock_until
    time.sleep(0.01)
    f = ec.close_amount(INST, "buy", 0.5, label="w1")      # la chiusura PASSA dall'API
    assert venue.place_calls == ec.lock_trip + 1
    assert not f.verified
    assert ec._lock_until > before                          # locked dal close -> cooldown esteso


def test_probe_rearms_or_resets(monkeypatch):
    sent = []
    ec, venue = _ec(monkeypatch, sent)
    for _ in range(ec.lock_trip):
        ec.open(INST, "buy", 50.0)
    # cooldown scaduto + venue ancora locked -> probe fallisce e riscatta
    ec._lock_until = time.monotonic() - 1
    ec.open(INST, "buy", 50.0)
    assert venue.place_calls == ec.lock_trip + 1 and ec.lock_blocked()
    # venue sbloccato -> il probe passa, breaker resettato, alert di rientro
    ec._lock_until = time.monotonic() - 1
    venue.locked = False
    f = ec.open(INST, "buy", 50.0)
    assert f.verified and not ec.lock_blocked() and ec._lock_streak == 0
    assert sent[-1][0] == "VENUE_LOCK" and sent[-1][1].get("status") == "RIENTRATO"
