"""
test_ml_cache_integrity.py — Test d'intégration : zéro NULL dans ml_predictions_cache
SaaS Turf Prédictions IA
Ticket: HRT-43 (suite au fix HRT-41 — métadonnées manquantes dans le cache ML)

Ces tests vérifient que la table ml_predictions_cache ne contient aucune ligne
avec des métadonnées NULL (hippodrome, race_label, heure) pour la date courante,
après le job ML de 19h30.

Usage:
    pytest tests/test_ml_cache_integrity.py -v -m integration
    pytest tests/test_ml_cache_integrity.py -v -m integration --date 2026-04-26

Variables d'environnement:
    TURF_DB_PATH  : chemin vers turf.db (défaut: /home/h3r7/turf_scraper/turf.db)
    TEST_DATE     : date cible au format YYYY-MM-DD (défaut: date du jour)
"""

import sqlite3
import os
import pytest
from datetime import date, datetime
from pathlib import Path


# ============================================================
# Configuration
# ============================================================

DEFAULT_DB_PATH = "/home/h3r7/turf_scraper/turf.db"
DB_PATH = os.environ.get("TURF_DB_PATH", DEFAULT_DB_PATH)


def _get_test_date() -> str:
    """Retourne la date cible pour les tests (env TEST_DATE ou date du jour)."""
    env_date = os.environ.get("TEST_DATE", "")
    if env_date:
        try:
            datetime.strptime(env_date, "%Y-%m-%d")
            return env_date
        except ValueError:
            raise ValueError(
                f"TEST_DATE invalide : '{env_date}'. Format attendu : YYYY-MM-DD"
            )
    return date.today().isoformat()


# ============================================================
# Fixture : connexion DB en lecture seule
# ============================================================


@pytest.fixture(scope="module")
def db_connection():
    """
    Connexion SQLite en mode lecture seule (uri=True + ?mode=ro).
    Garantit qu'aucune modification accidentelle de la DB de prod n'est possible.
    """
    db_path = Path(DB_PATH)
    if not db_path.exists():
        pytest.skip(
            f"Base de données introuvable : {DB_PATH}. "
            "Définir TURF_DB_PATH ou vérifier le chemin."
        )

    uri = f"file:{db_path.as_posix()}?mode=ro"
    conn = sqlite3.connect(uri, uri=True)
    conn.row_factory = sqlite3.Row
    yield conn
    conn.close()


@pytest.fixture(scope="module")
def target_date():
    """Date cible pour les tests (date du jour ou TEST_DATE)."""
    return _get_test_date()


# ============================================================
# Tests d'intégration
# ============================================================


@pytest.mark.integration
class TestMlCacheNullIntegrity:
    """
    Vérifie qu'après le job ML de 19h30, la table ml_predictions_cache
    ne contient aucune métadonnée NULL pour la date courante.

    Régression testée : HRT-41 (Fix #17 — métadonnées manquantes dans le cache ML)
    """

    def test_table_exists(self, db_connection):
        """Vérifie que la table ml_predictions_cache existe dans la DB."""
        cursor = db_connection.execute(
            "SELECT name FROM sqlite_master "
            "WHERE type='table' AND name='ml_predictions_cache'"
        )
        row = cursor.fetchone()
        assert row is not None, (
            "La table ml_predictions_cache est introuvable dans la base de données. "
            "Vérifier que le job ML a bien créé la table."
        )

    def test_rows_exist_for_today(self, db_connection, target_date):
        """
        Vérifie que des prédictions existent pour la date cible.

        Ce test passe en skip si aucune ligne n'existe (ex: avant le job 19h30).
        Il échoue uniquement si le job a manifestement tourné mais a laissé 0 lignes.
        """
        cursor = db_connection.execute(
            "SELECT COUNT(*) as cnt FROM ml_predictions_cache WHERE date = ?",
            (target_date,),
        )
        count = cursor.fetchone()["cnt"]

        if count == 0:
            pytest.skip(
                f"Aucune prédiction en cache pour le {target_date}. "
                "Ce test doit être exécuté après le job ML de 19h30."
            )

    def test_zero_null_hippodrome_today(self, db_connection, target_date):
        """
        CRITÈRE D'ACCEPTATION PRINCIPAL (HRT-43) :
        Vérifie que COUNT(*) WHERE date = today AND hippodrome IS NULL = 0.

        Régression directe du bug HRT-41 : le champ hippodrome était NULL
        pour toutes les prédictions du cache ML.
        """
        # Vérifier si des données existent avant de tester les NULLs
        cursor_total = db_connection.execute(
            "SELECT COUNT(*) as cnt FROM ml_predictions_cache WHERE date = ?",
            (target_date,),
        )
        total = cursor_total.fetchone()["cnt"]
        if total == 0:
            pytest.skip(
                f"Aucune prédiction en cache pour le {target_date}. "
                "Lancer ce test après le job ML de 19h30."
            )

        cursor = db_connection.execute(
            "SELECT COUNT(*) as cnt FROM ml_predictions_cache "
            "WHERE date = ? AND hippodrome IS NULL",
            (target_date,),
        )
        null_count = cursor.fetchone()["cnt"]

        assert null_count == 0, (
            f"RÉGRESSION HRT-41 DÉTECTÉE : {null_count} ligne(s) avec hippodrome IS NULL "
            f"dans ml_predictions_cache pour le {target_date}. "
            "Le patch de métadonnées n'a pas été appliqué correctement."
        )

    def test_zero_null_race_label_today(self, db_connection, target_date):
        """
        Vérifie que COUNT(*) WHERE date = today AND race_label IS NULL = 0.

        Complément du test hippodrome : vérifie que le libellé de course
        est bien renseigné pour toutes les prédictions.
        """
        cursor_total = db_connection.execute(
            "SELECT COUNT(*) as cnt FROM ml_predictions_cache WHERE date = ?",
            (target_date,),
        )
        total = cursor_total.fetchone()["cnt"]
        if total == 0:
            pytest.skip(
                f"Aucune prédiction en cache pour le {target_date}. "
                "Lancer ce test après le job ML de 19h30."
            )

        cursor = db_connection.execute(
            "SELECT COUNT(*) as cnt FROM ml_predictions_cache "
            "WHERE date = ? AND race_label IS NULL",
            (target_date,),
        )
        null_count = cursor.fetchone()["cnt"]

        assert null_count == 0, (
            f"ANOMALIE : {null_count} ligne(s) avec race_label IS NULL "
            f"dans ml_predictions_cache pour le {target_date}. "
            "Vérifier le pipeline de patch de métadonnées."
        )

    def test_zero_null_heure_today(self, db_connection, target_date):
        """
        Vérifie que COUNT(*) WHERE date = today AND heure IS NULL = 0.

        Vérifie que l'heure de course est bien renseignée pour toutes les prédictions.
        """
        cursor_total = db_connection.execute(
            "SELECT COUNT(*) as cnt FROM ml_predictions_cache WHERE date = ?",
            (target_date,),
        )
        total = cursor_total.fetchone()["cnt"]
        if total == 0:
            pytest.skip(
                f"Aucune prédiction en cache pour le {target_date}. "
                "Lancer ce test après le job ML de 19h30."
            )

        cursor = db_connection.execute(
            "SELECT COUNT(*) as cnt FROM ml_predictions_cache "
            "WHERE date = ? AND heure IS NULL",
            (target_date,),
        )
        null_count = cursor.fetchone()["cnt"]

        assert null_count == 0, (
            f"ANOMALIE : {null_count} ligne(s) avec heure IS NULL "
            f"dans ml_predictions_cache pour le {target_date}. "
            "Vérifier le pipeline de patch de métadonnées."
        )

    def test_full_metadata_coverage_today(self, db_connection, target_date):
        """
        Test de couverture globale : aucune des trois colonnes critiques
        (hippodrome, race_label, heure) n'est NULL pour une même ligne.

        Retourne les 5 premières lignes problématiques pour faciliter le débogage.
        """
        cursor_total = db_connection.execute(
            "SELECT COUNT(*) as cnt FROM ml_predictions_cache WHERE date = ?",
            (target_date,),
        )
        total = cursor_total.fetchone()["cnt"]
        if total == 0:
            pytest.skip(
                f"Aucune prédiction en cache pour le {target_date}. "
                "Lancer ce test après le job ML de 19h30."
            )

        cursor = db_connection.execute(
            "SELECT id, num_reunion, num_course, horse_name, hippodrome, race_label, heure "
            "FROM ml_predictions_cache "
            "WHERE date = ? "
            "AND (hippodrome IS NULL OR race_label IS NULL OR heure IS NULL) "
            "LIMIT 5",
            (target_date,),
        )
        bad_rows = cursor.fetchall()

        assert len(bad_rows) == 0, (
            f"ANOMALIE : {len(bad_rows)} ligne(s) avec au moins une métadonnée NULL "
            f"(hippodrome, race_label ou heure) pour le {target_date}.\n"
            "Exemples de lignes affectées :\n"
            + "\n".join(
                f"  - id={r['id']} R{r['num_reunion']}C{r['num_course']} "
                f"{r['horse_name']} | hippodrome={r['hippodrome']!r} "
                f"race_label={r['race_label']!r} heure={r['heure']!r}"
                for r in bad_rows
            )
        )

    def test_metadata_completeness_summary(self, db_connection, target_date):
        """
        Résumé diagnostique : affiche les statistiques de complétude des métadonnées
        pour la date cible. Toujours en mode informatif (pas de assertion stricte),
        utile pour le monitoring et les logs CI.
        """
        cursor = db_connection.execute(
            """
            SELECT
                COUNT(*) as total,
                SUM(CASE WHEN hippodrome IS NULL THEN 1 ELSE 0 END) as null_hippodrome,
                SUM(CASE WHEN race_label IS NULL THEN 1 ELSE 0 END) as null_race_label,
                SUM(CASE WHEN heure IS NULL THEN 1 ELSE 0 END) as null_heure,
                COUNT(DISTINCT hippodrome) as distinct_hippodromes,
                COUNT(DISTINCT race_label) as distinct_race_labels
            FROM ml_predictions_cache
            WHERE date = ?
            """,
            (target_date,),
        )
        row = cursor.fetchone()
        total = row["total"]

        if total == 0:
            pytest.skip(
                f"Aucune prédiction en cache pour le {target_date}. "
                "Lancer ce test après le job ML de 19h30."
            )

        # Afficher les statistiques (visibles avec pytest -v -s)
        print(f"\n=== Statistiques ml_predictions_cache pour le {target_date} ===")
        print(f"  Total lignes         : {total}")
        print(f"  NULL hippodrome      : {row['null_hippodrome']}")
        print(f"  NULL race_label      : {row['null_race_label']}")
        print(f"  NULL heure           : {row['null_heure']}")
        print(f"  Hippodromes distincts: {row['distinct_hippodromes']}")
        print(f"  Race labels distincts: {row['distinct_race_labels']}")

        # L'assertion ici reste stricte pour hippodrome (bug HRT-41 critique)
        assert row["null_hippodrome"] == 0, (
            f"RÉGRESSION HRT-41 : {row['null_hippodrome']}/{total} lignes "
            f"avec hippodrome IS NULL pour le {target_date}."
        )
