"""Guard FEED_BOOK_GAP (2026-06-12): il feed candele e il book d'esecuzione possono
divergere (MR02_BTC: fill reale a -443 bps dal sim; wick TP_PHANTOM il caso opposto).
Alert per episodio con isteresi a soglia/2, fail-open su errori di rete."""
import sys
from pathlib import Path

import pandas as pd

sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from src.portfolio import runner


class _Client:
    def __init__(self, marks):
        self._marks = marks
        self.calls = 0

    def get_ticker_batch(self, instruments):
        self.calls += 1
        return {"tickers": [{"instrument_name": i, "mark_price": self._marks[i]}
                            for i in instruments if i in self._marks]}


class _Boom:
    def get_ticker_batch(self, instruments):
        raise RuntimeError("rete giu'")


INSTR = {"BTC": "BTC_USDC-PERPETUAL"}


def _feed(close):
    return {"BTC": pd.DataFrame({"close": [100.0, close]})}


def _events(monkeypatch):
    sent = []
    monkeypatch.setattr("src.live.telegram_notifier.notify_event",
                        lambda ev, data: sent.append((ev, data)))
    return sent


def test_gap_alerts_once_per_episode(monkeypatch):
    sent = _events(monkeypatch)
    cl = _Client({"BTC_USDC-PERPETUAL": 60481.0})
    alerted = set()
    # feed a 63285 vs mark 60481 = il caso MR02 (~464 bps) -> alert, una volta sola
    for _ in range(3):
        runner._check_feed_book_gap(cl, _feed(63285.5), INSTR, 150.0, alerted)
    assert len(sent) == 1 and sent[0][0] == "FEED_BOOK_GAP"
    assert sent[0][1]["asset"] == "BTC" and sent[0][1]["gap_bps"] > 400


def test_recovery_with_hysteresis(monkeypatch):
    sent = _events(monkeypatch)
    cl = _Client({"BTC_USDC-PERPETUAL": 60000.0})
    alerted = {"BTC"}
    # gap fra soglia/2 e soglia: NESSUN recovery (isteresi)
    runner._check_feed_book_gap(cl, _feed(60000 * 1.0090), INSTR, 150.0, alerted)
    assert sent == [] and "BTC" in alerted
    # gap sotto soglia/2 -> RIENTRATO
    runner._check_feed_book_gap(cl, _feed(60010.0), INSTR, 150.0, alerted)
    assert len(sent) == 1 and sent[0][1].get("status") == "RIENTRATO"
    assert "BTC" not in alerted


def test_fail_open_and_no_instruments(monkeypatch):
    sent = _events(monkeypatch)
    runner._check_feed_book_gap(_Boom(), _feed(63285.5), INSTR, 150.0, set())
    cl = _Client({"BTC_USDC-PERPETUAL": 60481.0})
    runner._check_feed_book_gap(cl, {}, INSTR, 150.0, set())   # nessun feed
    assert sent == [] and cl.calls == 0
