"""
Test de validation Couche 7 — Détection de contexte de marché
=============================================================
Valide la classification PHOENIX / BULL_RUN / PUMP_TERMINAL / BEAR_RANGE
sur :
  1) Des fixtures synthétiques alignées avec les cas du 12/05/2026
     (SAGA → PHOENIX, SAPIEN → PUMP_TERMINAL, FF → BULL_RUN)
  2) Des données réelles Binance (si symbole listé)

Cf. PREDICTIVE_SYSTEM_V2.md — section "Cas tests de validation Couche 7"
"""

from __future__ import annotations

import json
import logging
import sys
import time
from typing import List

import numpy as np

from market_context import (
    detect_market_context,
    compute_metrics_from_klines,
    classify_regime,
    fetch_daily_klines,
)

logging.basicConfig(level=logging.INFO, format='%(levelname)s | %(name)s | %(message)s')
log = logging.getLogger('TestCouche7')


# ──────────────────────────────────────────────────────────────────────────────
# Fixtures synthétiques
# ──────────────────────────────────────────────────────────────────────────────

def _make_klines(closes: List[float], volume_pattern: List[float] | None = None,
                 upper_wick_days: List[int] | None = None) -> List[List]:
    """Construit des klines 1d synthétiques.

    - closes : prix de clôture (la dernière valeur = prix actuel)
    - volume_pattern : volume par jour (longueur = closes), sinon constant 1000
    - upper_wick_days : indices (négatifs depuis la fin) où injecter une longue mèche haute
                        pour simuler une capitulation
    """
    n = len(closes)
    if volume_pattern is None:
        volume_pattern = [1000.0] * n
    upper_wick_days = upper_wick_days or []
    upper_wick_set = {n + i if i < 0 else i for i in upper_wick_days}

    klines = []
    base_ts = int(time.time() * 1000) - n * 86_400_000
    for i, close in enumerate(closes):
        prev = closes[i - 1] if i > 0 else close * 0.99
        o = prev
        # Range "normal" 2%
        rng = max(close, prev) * 0.02
        h = max(o, close) + rng * 0.5
        l = min(o, close) - rng * 0.5
        if i in upper_wick_set:
            # Capitulation : grosse mèche haute + range × 3
            h = max(o, close) * 1.15  # +15% wick
            rng = h - l
        klines.append([
            base_ts + i * 86_400_000,
            f"{o:.8f}", f"{h:.8f}", f"{l:.8f}", f"{close:.8f}",
            f"{volume_pattern[i]:.4f}",
            base_ts + i * 86_400_000 + 86_399_999,
            f"{volume_pattern[i] * close:.4f}",
            100, "0", "0", "0",
        ])
    return klines


def fixture_saga_phoenix() -> List[List]:
    """SAGA-like : ATL il y a ~19 jours, rebond +180% depuis, volume × 4.8."""
    n = 60
    closes = []
    # 0..40 : descente progressive vers ATL
    for i in range(41):
        closes.append(0.08 * (1 - 0.78 * i / 40))  # 0.08 → 0.0176
    # 41..59 : rebond fort vers 0.048 (~19 jours)
    atl = closes[-1]
    for i in range(1, 20):
        closes.append(atl * (1 + 1.73 * i / 19))   # ATL → ~0.048 (+173%)
    assert len(closes) == 60
    # Volume : faible pendant la baisse, ×5 pendant le rebond
    vols = [1000.0] * 41 + [5000.0] * 19
    return _make_klines(closes, vols)


def fixture_sapien_parabolic() -> List[List]:
    """SAPIEN-like : ATH récent, +120% en 7j, sans bougie de capitulation franche.

    → attendu : PARABOLIC_EXTENSION (extension verticale, pas encore de capitulation
    confirmée par l'algo de détection de mèches).
    """
    n = 180
    closes = list(np.linspace(0.05, 0.07, n - 14))
    last = closes[-1]
    pump = list(last * np.linspace(1.0, 2.2, 14))
    closes.extend(pump)
    return _make_klines(closes)


def fixture_pump_terminal_strict() -> List[List]:
    """Pump terminal franc : ATH récent + capitulation candle + trend extreme.

    Pour valider que le régime PUMP_TERMINAL se déclenche bien quand toutes
    les conditions sont réunies (avec mèche haute manuellement injectée).
    """
    n = 180
    closes = list(np.linspace(0.05, 0.06, n - 14))
    last = closes[-1]
    pump = list(last * np.linspace(1.0, 2.5, 14))
    closes.extend(pump)
    # Retour léger depuis le pic = pic intraday
    closes[-1] = max(closes) * 0.92
    # Capitulation visible sur les 2 dernières bougies
    return _make_klines(closes, upper_wick_days=[-1, -2])


def fixture_ff_bull_run() -> List[List]:
    """FF-like BullRun :
    - trend +25% sur 30j
    - ATL > 45 jours (sinon Phoenix prendrait le pas)
    - tendance régulière, pas de capitulation
    """
    # 180 jours pour avoir ATL ancien et ATH éloigné
    n = 180
    # ATL il y a ~120 jours, puis remontée stable
    descent = list(np.linspace(0.07, 0.05, 60))
    plateau = list(np.linspace(0.05, 0.062, 90))
    recent = list(np.linspace(0.062, 0.077, 30))   # +24% sur 30j
    closes = descent + plateau + recent
    assert len(closes) == n
    return _make_klines(closes)


def fixture_bear_range() -> List[List]:
    """Pure bearish : -20% sur 30j avec un peu de bruit pour vol > seuil consolidation."""
    rng = np.random.default_rng(42)
    base = np.linspace(0.10, 0.08, 60)
    # Bruit ±2% pour produire une vol réaliste
    noise = rng.normal(0, 0.02, 60)
    closes = list(np.clip(base * (1 + noise), 0.001, None))
    return _make_klines(closes)


# ──────────────────────────────────────────────────────────────────────────────
# Test runner
# ──────────────────────────────────────────────────────────────────────────────

EXPECTATIONS = [
    ('SAGA (fixture, Phoenix)',         fixture_saga_phoenix,         'PHOENIX'),
    ('SAPIEN (fixture, Parabolic)',     fixture_sapien_parabolic,     'PARABOLIC_EXTENSION'),
    ('Pump terminal strict',            fixture_pump_terminal_strict, 'PUMP_TERMINAL'),
    ('FF (fixture, BullRun)',           fixture_ff_bull_run,          'BULL_RUN'),
    ('Bear-range control',              fixture_bear_range,           'BEAR_RANGE'),
]


def run_synthetic_tests() -> int:
    log.info("=" * 72)
    log.info("SYNTHETIC FIXTURES")
    log.info("=" * 72)
    failures = 0
    for label, fixture_fn, expected in EXPECTATIONS:
        klines = fixture_fn()
        result = detect_market_context(klines)
        if result is None:
            log.error(f"❌ {label}: detection returned None")
            failures += 1
            continue
        ok = (result.regime == expected)
        symbol = "✅" if ok else "❌"
        log.info(
            f"{symbol} {label:35s} → {result.regime:14s} "
            f"(conf {result.confidence:.2f})  expected={expected}"
        )
        log.info(
            f"     metrics: dist_ATL={result.metrics.distance_from_atl_60d_pct:+.1%}, "
            f"dist_ATH={result.metrics.distance_from_ath_180d_pct:.1%}, "
            f"trend30d={result.metrics.trend_30d_pct:+.1%}, "
            f"trend7d={result.metrics.trend_7d_pct:+.1%}, "
            f"vol_ratio={result.metrics.volume_ratio_7d_30d:.2f}, "
            f"cap_candles={result.metrics.capitulation_candles_3d}"
        )
        if result.notes:
            log.info(f"     notes: {result.notes[0]}")
        if not ok:
            failures += 1
    return failures


def run_live_tests() -> None:
    """Sanity check sur quelques symboles Binance réels (informatif uniquement).

    Pas d'assertion : les régimes peuvent évoluer entre la rédaction de la spec
    et l'exécution. Utile pour vérifier qu'aucun bug ne se produit sur de la
    donnée réelle et pour calibrer la pertinence.
    """
    log.info("")
    log.info("=" * 72)
    log.info("LIVE BINANCE DATA (informational)")
    log.info("=" * 72)
    candidates = [
        'SAGAUSDT', 'SAGAUSDC',
        'SAPIENUSDT',
        'FFUSDT',
        'BTCUSDT', 'ETHUSDT',
    ]
    for sym in candidates:
        klines = fetch_daily_klines(sym, limit=180)
        if not klines or len(klines) < 30:
            log.info(f"⏭️  {sym}: unlisted or insufficient data")
            continue
        result = detect_market_context(klines)
        if result is None:
            log.info(f"⚠️  {sym}: detection failed")
            continue
        log.info(
            f"🔎 {sym:10s} → {result.regime:14s} "
            f"(conf {result.confidence:.2f}, {len(klines)}d) | "
            f"price={result.metrics.current_price:.6g}, "
            f"dist_ATL={result.metrics.distance_from_atl_60d_pct:+.1%}, "
            f"dist_ATH={result.metrics.distance_from_ath_180d_pct:.1%}, "
            f"trend30d={result.metrics.trend_30d_pct:+.1%}, "
            f"trend7d={result.metrics.trend_7d_pct:+.1%}, "
            f"vol_ratio={result.metrics.volume_ratio_7d_30d:.2f}"
        )


if __name__ == '__main__':
    failures = run_synthetic_tests()
    try:
        run_live_tests()
    except Exception as e:
        log.warning(f"Live tests skipped (network/API error): {e}")
    if failures:
        log.error(f"\n❌ {failures} fixture(s) failed")
        sys.exit(1)
    log.info("\n✅ All synthetic fixtures passed")
    sys.exit(0)
