"""
Smoke tests for analysis/tech_snapshot.py end-to-end.

Uses FakeMarketDataProvider with synthetic OHLCV bars across M5/H1/H4.
Asserts TechSnapshot has all fields populated with sensible values
and that V15 bug parity is preserved (insufficient bars -> None).

Run:
    cd ~/apex_v16
    python -m tests.test_tech_snapshot
"""

from __future__ import annotations

import asyncio
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

import pandas as pd

from analysis.market_data import FakeMarketDataProvider
from analysis.tech_snapshot import TechSnapshot, build_tech_snapshot
from tests._fixtures.bars import (
    downtrend,
    hh_hl_h1,
    sideways,
    uptrend,
    with_pivot_low,
)


def _ok(label: str) -> None:
    print(f"  ok  {label}")


def make_provider(*, m5: pd.DataFrame, h1: pd.DataFrame,
                  h4: pd.DataFrame, symbol: str = "MES"
                  ) -> FakeMarketDataProvider:
    return FakeMarketDataProvider({
        (symbol, "5min"):  m5,
        (symbol, "1hour"): h1,
        (symbol, "4hour"): h4,
    })


# ============================================================
# 1. Insufficient bars -> None (V15 parity)
# ============================================================

async def test_returns_none_on_insufficient_bars():
    # Only 10 M5 bars (< _MIN_M5=30)
    p = make_provider(
        m5=uptrend(10), h1=hh_hl_h1(20), h4=hh_hl_h1(30),
    )
    snap = await build_tech_snapshot(
        symbol="MES", provider=p, tick_size=0.25, tick_value=1.25,
    )
    assert snap is None
    _ok("insufficient M5 bars -> None")


async def test_returns_none_on_missing_timeframe():
    p = FakeMarketDataProvider({
        ("MES", "5min"): uptrend(50),
        # Missing H1 and H4
    })
    snap = await build_tech_snapshot(
        symbol="MES", provider=p, tick_size=0.25, tick_value=1.25,
    )
    assert snap is None
    _ok("missing timeframe -> None")


# ============================================================
# 2. Happy path: all fields populated
# ============================================================

async def test_happy_path_full_snapshot():
    p = make_provider(
        m5=uptrend(80, body=0.3, step=0.1),     # mild — non-saturated RSI in M5
        h1=hh_hl_h1(20),
        h4=hh_hl_h1(30),
    )
    snap = await build_tech_snapshot(
        symbol="MES", provider=p,
        tick_size=0.25, tick_value=1.25,
        consecutive_sl_count=2,
    )
    assert isinstance(snap, TechSnapshot)
    assert snap.symbol == "MES"
    assert snap.price > 0
    # RSI multi-TF populated
    assert 0 <= snap.rsi <= 100
    assert 0 <= snap.rsi_h1 <= 100
    assert 0 <= snap.rsi_h4 <= 100
    # ATR / vol regime
    assert snap.atr_m5_points > 0
    assert snap.atr_ratio > 0
    assert snap.vol_regime in ("HIGH", "NORMAL")
    # Structure / regime
    assert snap.market_structure in (
        "BULLISH_EXPANSION", "BEARISH_EXPANSION", "RANGING"
    )
    assert snap.regime in ("TRENDING", "TRENDING_SOFT", "BREAKOUT", "RANGING")
    assert isinstance(snap.regime_near_trending, list)
    # Bias H4
    assert snap.bias in ("RIALZISTA", "RIBASSISTA", "NEUTRO")
    assert snap.allowed_direction in ("BUY", "SELL", "BOTH", "NONE")
    assert 0.3 <= snap.h1_compatibility <= 1.0
    # Tick mechanics propagated
    assert snap.tick_size == 0.25
    assert snap.tick_value == 1.25
    # Anti-revenge counter
    assert snap.consecutive_sl_count == 2
    # Swing data not requested -> empty
    assert snap.swing_data == {}
    _ok(f"happy: snap built bias={snap.bias} regime={snap.regime} "
        f"struct={snap.market_structure}")


# ============================================================
# 3. Frozen: cannot mutate
# ============================================================

async def test_snapshot_is_frozen():
    p = make_provider(
        m5=uptrend(80, body=0.3, step=0.1), h1=hh_hl_h1(20), h4=hh_hl_h1(30),
    )
    snap = await build_tech_snapshot(
        symbol="MES", provider=p, tick_size=0.25, tick_value=1.25,
    )
    try:
        snap.rsi = 99.0
    except Exception as e:
        # FrozenInstanceError or AttributeError
        assert "frozen" in str(e).lower() or "cannot assign" in str(e).lower()
        _ok("snapshot is frozen — direct mutation rejected")
        return
    raise AssertionError("snapshot should be frozen")


# ============================================================
# 4. Swing data populated when direction requested
# ============================================================

async def test_swing_data_when_direction_requested():
    p = make_provider(
        m5=with_pivot_low(n=80, pivot_idx=40, base_price=100.0, pivot_drop=1.0),
        h1=hh_hl_h1(20),
        h4=hh_hl_h1(30),
    )
    snap = await build_tech_snapshot(
        symbol="MES", provider=p,
        tick_size=0.25, tick_value=1.25,
        direction_for_swing="BUY",
    )
    assert snap is not None
    assert snap.swing_data != {}
    assert "swing_found" in snap.swing_data
    _ok(f"swing data populated when direction='BUY': "
        f"found={snap.swing_data.get('swing_found')}")


# ============================================================
# 5. Bias path coherence: bullish H4 -> bias != RIBASSISTA strict
# ============================================================

async def test_bullish_h4_yields_non_bearish_bias():
    """H4 in clean uptrend -> RSI override likely fires, but bias shouldn't be RIBASSISTA from algo path."""
    p = make_provider(
        m5=uptrend(80, body=0.3, step=0.1),
        h1=hh_hl_h1(20),
        h4=uptrend(40, body=1.0, step=1.0),   # strong H4 bullish (will hit RSI>80 override)
    )
    snap = await build_tech_snapshot(
        symbol="MES", provider=p, tick_size=0.25, tick_value=1.25,
    )
    assert snap is not None
    # V15 parity: clean H4 uptrend triggers RSI>80 override -> RIBASSISTA SELL
    # (mean-reversion expected). Documented; not a failure.
    assert snap.bias in ("RIBASSISTA", "RIALZISTA")
    _ok(f"bullish H4 -> bias={snap.bias} allowed={snap.allowed_direction}")


# ============================================================
# RUN
# ============================================================

async def main_async() -> int:
    print("test_tech_snapshot.py")
    await test_returns_none_on_insufficient_bars()
    await test_returns_none_on_missing_timeframe()
    await test_happy_path_full_snapshot()
    await test_snapshot_is_frozen()
    await test_swing_data_when_direction_requested()
    await test_bullish_h4_yields_non_bearish_bias()
    print("ALL 6 TESTS PASSED")
    return 0


def main() -> int:
    return asyncio.run(main_async())


if __name__ == "__main__":
    sys.exit(main())
