"""`room_info` darf das entry_room-Gate NICHT veraendert haben.

ANLASS (2026-08-10): der Raum zum Gegenlevel lag bisher nur als Fliesstext in
`reasons` und war nicht auswertbar. Fuer die Anzeige in der M15-Karte wurde er
herausgezogen — als EINE Implementierung fuer Gate und Anzeige, nicht als zweite
Kopie (die Divergenz-Falle, die im Projekt dreimal zugeschlagen hat).

⚠⚠ MIGRATIONS-REGEL: eine Refaktorierung ist nur dann eine, wenn das Verhalten
bitgenau gleich bleibt. Die ALTE Fassung liegt dafuer woertlich eingefroren hier
im Test (aus `git show`) und laeuft gegen die neue.

⚠ Der gefaehrlichste Fall ist die RUNDUNG: `dist_atr` wird auf 2 Stellen
gerundet ausgewiesen. Wuerde `frei` auf dem gerundeten Wert entschieden, kippte
ein Grenzfall (0,5951 → 0,60 → faelschlich frei). Genau dafuer gibt es
`test_grenzfall_rundung` — ohne den waere der Fehler unsichtbar, weil er nur
etwa jeden hundertsten Fall trifft.
"""
from __future__ import annotations

import random

import pytest

from core.wave_rec import WaveRecommender


class _Stub:
    """Minimal-Recommender: nur die drei Felder, die beide Methoden lesen."""

    def __init__(self, x, ph, pl, atr):
        self._entry_room_atr = x
        self._pb_levels = {"ph": ph, "pl": pl}
        self._pb_feats = {"atr": atr}

    room_info = WaveRecommender.room_info
    _room_gate = WaveRecommender._room_gate


# ── die ALTE Fassung, woertlich eingefroren (Stand vor dem Umbau) ─────────
def _alt_room_gate(s, rec, cur):
    x = s._entry_room_atr
    sig = rec.get("signal")
    if x <= 0 or sig == "WARTEN":
        return rec
    lv = s._pb_levels or {}
    pf = s._pb_feats or {}
    atr5 = pf.get("atr")
    if not atr5:
        return rec
    d = 1 if sig == "LONG" else -1
    if d > 0:
        cands = [p for p in (lv.get("ph") or []) if p > cur]
        lvl = min(cands) if cands else None
    else:
        cands = [p for p in (lv.get("pl") or []) if p < cur]
        lvl = max(cands) if cands else None
    if lvl is None:
        return rec
    dist = (lvl - cur) * d / atr5
    if dist >= x:
        return rec
    return {"signal": "WARTEN", "conf_pct": 0, "score": 0.0, "setup": "WAVE",
            "regime": None, "rsi": None, "block": "entry_room",
            "reasons": [f"kein Raum: {'Widerstand' if d > 0 else 'Support'} "
                        f"{lvl:.2f} nur {dist:.2f}×ATR entfernt (Gate {x:.1f}) — "
                        f"Ertrag gedeckelt, Kosten fressen den Edge"]}


def _rec(sig):
    return {"signal": sig, "conf_pct": 70, "score": 1.0, "setup": "WAVE",
            "regime": None, "rsi": None, "reasons": ["Test"]}


def test_bitgleich_ueber_zufallsfaelle():
    """2.000 Zufallslagen — alte und neue Fassung muessen IDENTISCH entscheiden.

    ⚠ Die Level werden eng um den Kurs gestreut, damit die Gate-Grenze wirklich
    oft getroffen wird. Liegen alle Level weit weg, blockt nie etwas und der
    Test belegt nichts (dieselbe Falle wie bei `test_squeeze_scan`, wo erst die
    Vola-Wechsel die Bedingungen durchlaufen liessen)."""
    r = random.Random(4711)
    getroffen = 0
    for _ in range(2000):
        cur = 79.0 + r.uniform(-2, 2)
        atr = r.choice([0.06, 0.12, 0.2, 0.35])
        x = r.choice([0.0, 0.3, 0.6, 1.0])
        ph = [round(cur + r.uniform(-1.0, 1.0), 3) for _ in range(r.randint(0, 5))]
        pl = [round(cur + r.uniform(-1.0, 1.0), 3) for _ in range(r.randint(0, 5))]
        s = _Stub(x, ph, pl, atr)
        for sig in ("LONG", "SHORT", "WARTEN"):
            neu = s._room_gate(_rec(sig), cur)
            alt = _alt_room_gate(s, _rec(sig), cur)
            assert neu == alt, f"Abweichung bei {sig} cur={cur} atr={atr} x={x}"
            if neu.get("block") == "entry_room":
                getroffen += 1
    # Ein Test, in dem das Gate nie feuert, belegt nichts.
    assert getroffen > 300, f"Gate feuerte nur {getroffen}x — Testdaten zu lasch"


def test_grenzfall_rundung():
    """dist = 0,5951 liegt UNTER dem Gate 0,6 — gerundet waere es 0,60.

    Wuerde `frei` auf dem gerundeten Wert entschieden, liesse das Gate diesen
    Fall faelschlich durch. Das ist der einzige Weg, wie der Umbau das Verhalten
    haette veraendern koennen."""
    cur, atr, x = 79.0, 0.1, 0.6
    lvl = cur + 0.05951                      # dist = 0,5951
    s = _Stub(x, [round(lvl, 5)], [], atr)
    info = s.room_info(cur)
    assert info["long"]["dist_atr"] == 0.6, "Anzeige rundet wie erwartet"
    assert info["long"]["frei"] is False, "…aber die ENTSCHEIDUNG nutzt den Rohwert"
    assert s._room_gate(_rec("LONG"), cur)["block"] == "entry_room"


# ── die neue Anzeige-Faehigkeit ──────────────────────────────────────────
def test_beide_richtungen_werden_geliefert():
    """Die Karte gibt keine Richtung vor → sie braucht den Raum fuer beide."""
    cur = 79.0
    s = _Stub(0.6, [79.30], [78.20], 0.2)
    info = s.room_info(cur)
    assert info["long"]["level"] == 79.30
    assert info["long"]["dist_atr"] == 1.5 and info["long"]["frei"] is True
    assert info["short"]["level"] == 78.20
    assert info["short"]["dist_atr"] == 4.0 and info["short"]["frei"] is True
    assert info["gate"] == 0.6 and info["atr_m5"] == 0.2


def test_der_reale_fall_vom_10_08():
    """Trade #49671601: BUY, Widerstand praktisch AUF dem Kurs.

    ctx_dist_res_atr war −0,07 bei ATR_M5 0,12 (dem Floor) — der Widerstand lag
    rund 0,008 $ voraus, der Stop 0,560 $ zurueck. Genau diese Lage muss die
    Karte kuenftig ANZEIGEN."""
    cur, atr = 79.065, 0.12
    s = _Stub(0.6, [79.073], [78.99], atr)
    info = s.room_info(cur)
    assert info["long"]["frei"] is False           # kein Raum nach oben
    assert info["long"]["dist_atr"] < 0.1
    assert s._room_gate(_rec("LONG"), cur)["block"] == "entry_room"


def test_freie_bahn_ohne_level():
    """Kein Level voraus = bestes gemessenes Segment → frei, nicht geblockt."""
    s = _Stub(0.6, [], [], 0.2)
    info = s.room_info(79.0)
    assert info["long"] == {"level": None, "dist_atr": None, "frei": True}
    assert s._room_gate(_rec("LONG"), 79.0).get("block") is None


def test_ohne_atr_kein_absturz():
    """Kaltstart: keine pb_feats → fail-open, kein Gate, keine Exception."""
    s = _Stub(0.6, [79.3], [78.2], None)
    info = s.room_info(79.0)
    assert info["atr_m5"] is None and info["long"] is None
    assert s._room_gate(_rec("LONG"), 79.0).get("block") is None


if __name__ == "__main__":
    raise SystemExit(pytest.main([__file__, "-v"]))
