"""Gate ④ der M15-Karte: das Ziel muss in der ERLAUBTEN Richtung liegen.

ANLASS (2026-08-10, User-Fund): die Karte zeigte `verboten=SHORT` (also LONG
erlaubt) UND gleichzeitig ein Ziel 0,15 $ UNTER dem Kurs. Die Arithmetik war
korrekt — aber fuer die verbotene Richtung. Ursache: das Ziel war schlicht das
ENTFERNTERE der beiden Level (`max` nach gap), ohne Richtungspruefung.

⚠ Die Tests pruefen BEIDE Richtungen. Ein Test, der nur zeigt „im Fehlerfall
kommt kein Ziel", belegt nicht, dass der korrekte Fall noch funktioniert —
genau daran ist am 07.08. die Neustart-Sperre fast gescheitert (Negativ-Test
gruen, Positiv-Test rot).
"""
from __future__ import annotations

import pytest

from core.engine import TradingEngine


class _Stub:
    """Minimal-Engine: nur die beiden Methoden, die `_m15_setup` wirklich ruft."""

    def __init__(self, res, sup):
        self._res, self._sup = res, sup

    def _draw_levels(self):
        return {"res": [self._res] if self._res else [],
                "sup": [self._sup] if self._sup else []}

    def _vola_regime(self, atr15):
        return None


def _setup(bid, res, sup, htf):
    """`_m15_setup` mit Stub-Engine aufrufen (die Methode ist eine reine Funktion)."""
    ws = {"tf_atr": {"M15": 0.25}, "htf_trend": htf, "h1_trend": htf,
          "pb_feats": {"atr": 0.2, "ema_diff": 0.05, "mom6": 0.1, "mom3": 0.1}}
    cone = {"levels": [{"band": 80, "minutes": m, "lo": bid - 1.0, "hi": bid + 1.0}
                       for m in (30, 60, 120)]}
    return TradingEngine._m15_setup(
        _Stub(res, sup), {"bid": bid, "ask": bid + 0.03}, ws, cone, None)


# ── Der reale Fehlerfall vom 10.08. ──────────────────────────────────────
def test_kein_ziel_gegen_das_veto():
    """LONG erlaubt, naechstes Level ist der Widerstand → Ausbruch, kein Ziel.

    Exakt die Live-Zahlen: bid 78,918 · res 79,033 (0,47xATR) · sup 78,768
    (0,61xATR). Vorher stand hier `ziel=78.768` — 0,15 $ UNTER dem Kurs, bei
    verbotenem SHORT."""
    s = _setup(78.918, 79.033, 78.768, htf=1)
    assert s["bias"]["verboten"] == "SHORT"       # LONG ist erlaubt
    assert s["ziel"] is None, "Ziel zeigte in die verbotene Richtung"
    assert "Ausbruch" in (s["ziel_grund"] or "")


def test_ziel_niemals_auf_der_verbotenen_seite():
    """Die eigentliche Invariante — ueber viele Konstellationen geprueft.

    Wird ein Ziel ausgewiesen, MUSS es in der erlaubten Richtung vom Kurs
    liegen. Das ist die Aussage, die der Fehler verletzt hat."""
    for htf, verboten in ((1, "SHORT"), (-1, "LONG")):
        for res_d in (0.05, 0.15, 0.30, 0.60):
            for sup_d in (0.05, 0.15, 0.30, 0.60):
                bid = 78.918
                s = _setup(bid, bid + res_d, bid - sup_d, htf=htf)
                assert s["bias"]["verboten"] == verboten
                z = s["ziel"]
                if z is None:
                    continue
                if verboten == "SHORT":            # LONG erlaubt → Ziel OBEN
                    assert z["level"] > bid, f"Ziel {z['level']} unter Kurs {bid}"
                    assert z["richtung"] == "LONG"
                else:                              # SHORT erlaubt → Ziel UNTEN
                    assert z["level"] < bid, f"Ziel {z['level']} ueber Kurs {bid}"
                    assert z["richtung"] == "SHORT"


# ── Der korrekte Fall MUSS erhalten bleiben ──────────────────────────────
def test_fade_am_widerstand_bleibt_wenn_short_erlaubt():
    """M30 ab → SHORT erlaubt, naechstes Level ist der Widerstand → Ziel Support.

    Das ist genau die Anzeige, die vorher zu sehen war — sie war nur unter dem
    FALSCHEN Veto zu sehen. Sie darf nicht mit weggefixt werden."""
    s = _setup(78.918, 79.033, 78.768, htf=-1)
    assert s["bias"]["verboten"] == "LONG"
    assert s["ziel"] is not None
    assert s["ziel"]["level"] == 78.768
    assert s["ziel"]["von"] == 79.033
    assert s["ziel"]["richtung"] == "SHORT"


def test_bounce_am_support_mit_long():
    """Kurs dicht ueber dem Support, LONG erlaubt → Einstieg Support, Ziel Widerstand."""
    s = _setup(78.80, 79.30, 78.77, htf=1)         # sup 0,03 nah, res 0,50 fern
    assert s["ziel"] is not None
    assert s["ziel"]["von"] == 78.77               # Einstieg = naechstes Level
    assert s["ziel"]["level"] == 79.30             # Ziel = oben
    assert s["ziel"]["richtung"] == "LONG"


def test_ohne_veto_kein_ziel():
    """M30 flach → keine erlaubte Richtung → ein einzelnes Ziel waere wieder
    eine Richtungsaussage."""
    s = _setup(78.918, 79.033, 78.768, htf=0)
    assert s["bias"]["verboten"] is None
    assert s["ziel"] is None
    assert "kein Veto" in (s["ziel_grund"] or "")


def test_kegel_pruefung_bleibt_wirksam():
    """Liegt das Ziel ausserhalb des 80-%-Bandes, muessen alle Horizonte
    `drin=False` melden — die Erreichbarkeits-Aussage selbst bleibt intakt."""
    ws = {"tf_atr": {"M15": 0.25}, "htf_trend": 1, "h1_trend": 1,
          "pb_feats": {"atr": 0.2, "ema_diff": 0.05, "mom6": 0.1, "mom3": 0.1}}
    cone = {"levels": [{"band": 80, "minutes": m, "lo": 78.79, "hi": 78.81}
                       for m in (30, 60, 120)]}
    s = TradingEngine._m15_setup(
        _Stub(79.30, 78.77), {"bid": 78.80, "ask": 78.83}, ws, cone, None)
    assert s["ziel"] is not None
    assert all(h["drin"] is False for h in s["ziel"]["horizonte"])


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