"""Reconcile degli ordini RESTING (2026-06-12): TP/DSL attesi dai libri vs ordini
in book + fill non bookati (caso MR02_BTC: TP resting fillato di notte e disaster-SL
sparito, scoperti solo al close sim ore dopo). Nessuna rete: client stubbato,
PAPER puntato su tmp_path."""
import json
import sys
from pathlib import Path

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

from src.live import books


class _Client:
    def __init__(self, open_orders=None, trades=None):
        self._oo = open_orders or []
        self._tr = trades or []

    def get_open_orders(self, currency="USDC", type="all"):
        # merge 'all'+'trigger_all' nel chiamante: qui si ritorna tutto due volte
        return self._oo

    def get_trade_history(self, limit=100, instrument_name=None):
        return [t for t in self._tr
                if not instrument_name or t["instrument"] == instrument_name]


def _mk_worker(root: Path, wid: str, st: dict):
    d = root / wid
    d.mkdir(parents=True)
    (d / "status.json").write_text(json.dumps(st))


def _setup(tmp_path, monkeypatch, statuses: dict):
    root = tmp_path / "portfolio_paper"
    for wid, st in statuses.items():
        _mk_worker(root, wid, st)
    monkeypatch.setattr(books, "PAPER", root)
    import importlib
    ra = importlib.import_module("scripts.analysis.reconcile_account")
    monkeypatch.setattr(ra, "PAPER", root)
    return ra


IN_POS = {"real_in_position": True, "real_tp_order_id": "TP-1",
          "real_dsl_order_id": "DSL-1"}


def test_expected_resting_reads_single_leg(tmp_path, monkeypatch):
    _setup(tmp_path, monkeypatch, {
        "MR01_bollinger_fade__ETH__1h": IN_POS,
        # pairs e worker flat: NESSUN resting atteso
        "PR01_pairs_reversion__ETH_BTC__1h": {"real_in_position": True,
                                              "real_amount_a": 0.1},
        "MR02_donchian_fade__BTC__1h": {"real_in_position": False,
                                        "real_tp_order_id": "TP-9"},
    })
    exp = books.expected_resting()
    assert {(e["order_id"], e["kind"]) for e in exp} == {("TP-1", "tp"), ("DSL-1", "dsl")}
    assert all(e["instrument"] == "ETH_USDC-PERPETUAL" for e in exp)


def test_resting_ok_when_on_book(tmp_path, monkeypatch):
    ra = _setup(tmp_path, monkeypatch, {"MR01_bollinger_fade__ETH__1h": IN_POS})
    cl = _Client(open_orders=[{"order_id": "TP-1", "label": "MR01_bollinger_fade__ETH__1h"},
                              {"order_id": "DSL-1", "label": "MR01_bollinger_fade__ETH__1h"}])
    rows = ra.compute_resting_drift(cl)
    assert [r["status"] for r in rows] == ["OK", "OK"]


def test_resting_filled_unbooked_vs_missing(tmp_path, monkeypatch):
    # TP sparito dal book MA con fill nel trade history -> FILLED_UNBOOKED (caso MR02);
    # DSL sparito senza fill (trigger genera order_id nuovo) -> MISSING
    ra = _setup(tmp_path, monkeypatch, {"MR01_bollinger_fade__ETH__1h": IN_POS})
    cl = _Client(open_orders=[],
                 trades=[{"instrument": "ETH_USDC-PERPETUAL", "order_id": "TP-1",
                          "amount": 0.103, "price": 1640.6}])
    by_kind = {r["kind"]: r for r in ra.compute_resting_drift(cl)}
    assert by_kind["tp"]["status"] == "FILLED_UNBOOKED"
    assert abs(by_kind["tp"]["filled"] - 0.103) < 1e-9
    assert by_kind["dsl"]["status"] == "MISSING"


def test_resting_stale_order_from_flat_worker(tmp_path, monkeypatch):
    # ordine in book con label di un NOSTRO worker flat -> STALE (fillerebbe a sorpresa);
    # ordini di altri bot (label sconosciuta) ignorati
    ra = _setup(tmp_path, monkeypatch, {
        "MR01_bollinger_fade__ETH__1h": {"real_in_position": False}})
    cl = _Client(open_orders=[
        {"order_id": "OLD-7", "label": "MR01_bollinger_fade__ETH__1h",
         "instrument": "ETH_USDC-PERPETUAL", "order_type": "limit"},
        {"order_id": "X-1", "label": "altro_bot", "instrument": "ETH_USDC-PERPETUAL"},
    ])
    rows = ra.compute_resting_drift(cl)
    assert len(rows) == 1
    assert rows[0]["status"] == "STALE" and rows[0]["order_id"] == "OLD-7"


def _age(root: Path, wid: str, minutes: float):
    """Invecchia lo status.json di un worker di `minutes` minuti (mtime)."""
    import os
    p = root / wid / "status.json"
    t = p.stat().st_mtime - minutes * 60
    os.utime(p, (t, t))


def test_stale_real_position_flagged(tmp_path, monkeypatch):
    # worker con posizione reale ma status.json vecchio = non gestito (caso
    # MR02_BTC 1h ritirato dallo swap a 15m mentre short reale)
    ra = _setup(tmp_path, monkeypatch, {
        "MR02_donchian_fade__BTC__1h": {"real_in_position": True, "real_amount": 0.0028,
                                        "real_side": "sell"}})
    _age(ra.PAPER, "MR02_donchian_fade__BTC__1h", 800)
    stale = ra.compute_stale_real_positions(max_age_min=15)
    assert len(stale) == 1
    assert stale[0]["worker"] == "MR02_donchian_fade__BTC__1h"
    assert stale[0]["real_amount"] == 0.0028


def test_fresh_real_position_not_flagged(tmp_path, monkeypatch):
    # worker vivo (status.json appena scritto) NON e' stantio anche se in posizione
    ra = _setup(tmp_path, monkeypatch, {
        "SH01_shape_ml__ETH__1h": {"real_in_position": True, "real_amount": 0.053,
                                   "real_side": "sell"}})
    assert ra.compute_stale_real_positions(max_age_min=15) == []


def test_flat_stale_worker_not_flagged(tmp_path, monkeypatch):
    # worker vecchio MA flat: niente posizione reale -> non e' un orfano
    ra = _setup(tmp_path, monkeypatch, {
        "MR01_bollinger_fade__BTC__1h": {"real_in_position": False, "real_amount": 0.0}})
    _age(ra.PAPER, "MR01_bollinger_fade__BTC__1h", 800)
    assert ra.compute_stale_real_positions(max_age_min=15) == []
