"""Orfani single-leg (2026-06-12): un close fallito/cappato registra la quota
residua in orphan_legs (parita' coi pairs) cosi' il reconciler la conta come
drift SPIEGATO — prima il REAL_CLOSE_PARTIAL di MR07 (0.102 ETH nel lock testnet)
lasciava drift non spiegato. Nessuna rete: executor finto."""
import json
from types import SimpleNamespace

from src.live.execution import Fill
from src.live.strategy_worker import StrategyWorker
from src.strategies.base import Signal
from src.live import books


class FailingCloseExec:
    """Apre ok, chiude con fill ZERO (venue locked / netting negato)."""
    verify_polls = 1
    verify_sleep = 0.0
    disaster_sl_pct = None

    def open(self, instrument, side, notional, label=None):
        amt = round(notional / 100.0, 6)
        return Fill(instrument, side, notional, amt, 100.0, 0.0, 0.05,
                    "oid-open", "filled", True, filled_amount=amt)

    def place_tp_limit(self, *a, **k):
        return Fill("x", "sell", 0.0, 0.0, None, 0.0, 0.0, None, None, False)

    def cancel_order(self, oid):
        return {"state": "cancelled"}

    def resting_fills(self, instrument, oid):
        return 0.0, None, 0.0

    def close_amount(self, instrument, side, amount, label=None):
        return Fill(instrument, "sell", 0.0, amount, None, 0.0, 0.0,
                    None, "error", False, notes="place_order error: locked_by_admin",
                    filled_amount=0.0)


def _worker(tmp_path):
    return StrategyWorker(
        strategy=SimpleNamespace(name="MR07_return_reversal", fee_rt=0.001),
        asset="ETH", tf="1h", capital=100.0, position_size=0.5, leverage=2.0,
        data_dir=tmp_path, executor=FailingCloseExec(),
        exec_instrument="ETH_USDC-PERPETUAL")


def test_failed_close_registers_orphan(tmp_path, monkeypatch):
    monkeypatch.setattr("src.live.strategy_worker.notify_event", lambda *a, **k: None)
    w = _worker(tmp_path)
    w._open_position(Signal(idx=0, direction=1, entry_price=100.0,
                            metadata={"max_bars": 12}), 100.0)
    amt = w.real_amount
    assert w.real_in_position and amt > 0
    w._close_position(105.0, "time_limit")
    assert not w.real_in_position
    assert len(w.orphan_legs) == 1
    o = w.orphan_legs[0]
    assert o["instrument"] == "ETH_USDC-PERPETUAL"
    assert o["entry_side"] == "buy" and abs(o["amount"] - amt) < 1e-9

    # persistito nello status (resume-safe) e visto da books.real_books
    st = json.loads((tmp_path / w.worker_id / "status.json").read_text())
    assert st["orphan_legs"] == w.orphan_legs
    monkeypatch.setattr(books, "PAPER", tmp_path)
    _, orphans = books.real_books()
    assert abs(orphans["ETH_USDC-PERPETUAL"] - amt) < 1e-9   # buy = firmato +


def test_clean_close_no_orphan(tmp_path, monkeypatch):
    monkeypatch.setattr("src.live.strategy_worker.notify_event", lambda *a, **k: None)
    w = _worker(tmp_path)
    w.executor.close_amount = lambda instrument, side, amount, label=None: Fill(
        instrument, "sell", 0.0, amount, 105.0, 0.0, 0.05, "oid-close",
        "filled", True, filled_amount=amount)
    w._open_position(Signal(idx=0, direction=1, entry_price=100.0,
                            metadata={"max_bars": 12}), 100.0)
    w._close_position(105.0, "time_limit")
    assert w.orphan_legs == []
