"""Tests para column_quality_score (nueva fórmula, report 2046).

Verifica las invariantes de la fórmula de calidad:
  - completeness (0.6) + validity (0.4) con renormalización por aplicabilidad.
  - Los OUTLIERS no bajan el score (van a observations, no a issues).
  - Columnas constantes e ids no bajan el score (observations).
  - Sin doble conteo de la falta de datos.
  - all-null -> score 0; función pura (no muta el input).
"""

import os
import sys

sys.path.insert(0, os.path.dirname(__file__))

from column_quality_score import column_quality_score


def _clean_numeric_col() -> dict:
    """ColumnProfile de una columna numérica nativa sana, sin problemas."""
    return {
        "name": "edad",
        "physical_type": "INTEGER",
        "inferred_type": "numeric",
        "semantic_type": "",
        "count": 1000,
        "n_rows": 1000,
        "null_count": 0,
        "null_pct": 0.0,
        "distinct_count": 80,
        "unique_pct": 0.08,
        "flags": [],
        "numeric": {"outlier_pct": 0.0},
        "categorical": None,
        "datetime": None,
    }


# --------------------------------------------------------------------------- #
# Golden
# --------------------------------------------------------------------------- #
def test_clean_column_high_score():
    out = column_quality_score(_clean_numeric_col())
    assert out["score"] == 100.0
    assert out["completeness"] == 1.0
    assert out["validity"] == 1.0
    assert out["applicable"] == ["completeness", "validity"]
    assert out["issues"] == []
    assert out["observations"] == []


def test_weights_60_40_native_type():
    """30% nulos en numérica nativa: score = 100*(0.6*0.7 + 0.4*1.0) = 82."""
    col = _clean_numeric_col()
    col["null_pct"] = 0.30
    col["null_count"] = 300
    out = column_quality_score(col)
    assert out["completeness"] == 0.7
    assert out["validity"] == 1.0
    assert out["score"] == 82.0
    assert any("nulos" in i for i in out["issues"])


# --------------------------------------------------------------------------- #
# Outliers FUERA del score
# --------------------------------------------------------------------------- #
def test_outliers_do_not_penalize_score():
    """Columna con outliers pero sin nulos -> score máximo; outliers en observations."""
    col = _clean_numeric_col()
    col["numeric"] = {"outlier_pct": 18.0, "skew": 0.2}  # 18% atípicos (escala 0-100)
    out = column_quality_score(col)
    assert out["score"] == 100.0  # los outliers NO bajan la calidad
    assert out["validity"] == 1.0
    # No aparecen como problema de calidad...
    assert not any("atípic" in i or "outlier" in i for i in out["issues"])
    # ...sino como observación analítica.
    assert any("atípic" in o for o in out["observations"])


def test_nulls_lower_score_more_than_outliers():
    """Vacíos sí penalizan; outliers no: comparar las dos columnas."""
    con_nulos = _clean_numeric_col()
    con_nulos["null_pct"] = 0.30
    con_outliers = _clean_numeric_col()
    con_outliers["numeric"] = {"outlier_pct": 30.0}
    assert column_quality_score(con_nulos)["score"] < \
        column_quality_score(con_outliers)["score"]


# --------------------------------------------------------------------------- #
# Validity: aplicabilidad y renormalización
# --------------------------------------------------------------------------- #
def test_validity_from_parse_rate_lowers_score():
    """Numérica como texto con 20% basura: validity=0.8 -> score=92."""
    col = {
        "name": "precio_txt", "inferred_type": "numeric", "semantic_type": "decimal",
        "null_pct": 0.0, "validity_rate": 0.80, "flags": [], "numeric": None,
    }
    out = column_quality_score(col)
    assert out["validity"] == 0.8
    assert out["score"] == 92.0  # 100*(0.6 + 0.4*0.8)
    assert any("no parsea" in i for i in out["issues"])


def test_validity_from_match_rate():
    """Texto con semantic_type y 5% no conforme: validity=0.95."""
    col = {
        "name": "email", "inferred_type": "text", "semantic_type": "email",
        "null_pct": 0.0, "match_rate": 0.95, "unique_pct": 0.5, "flags": [],
    }
    out = column_quality_score(col)
    assert out["validity"] == 0.95
    assert out["score"] == 98.0  # 100*(0.6 + 0.4*0.95)
    assert any("no casa" in i for i in out["issues"])


def test_free_text_renormalizes_to_completeness_only():
    """Texto libre sin semántica: validity no aplica -> score = 100*completeness."""
    col = {
        "name": "comentario", "inferred_type": "text", "semantic_type": "",
        "null_pct": 0.30, "unique_pct": 0.5, "flags": [], "numeric": None,
    }
    out = column_quality_score(col)
    assert out["validity"] is None
    assert out["applicable"] == ["completeness"]
    assert out["completeness"] == 0.7
    assert out["score"] == 70.0  # renormalizado a solo completeness


# --------------------------------------------------------------------------- #
# Casos límite (report §4.6)
# --------------------------------------------------------------------------- #
def test_all_null_column_scores_zero():
    col = _clean_numeric_col()
    col["null_pct"] = 1.0
    col["null_count"] = 1000
    out = column_quality_score(col)
    assert out["completeness"] == 0.0
    assert out["validity"] is None  # no medible sin valores no nulos
    assert out["score"] == 0.0


def test_constant_column_scores_full_and_is_observation():
    """Columna constante: dato válido y completo -> score 100; baja info = observación."""
    col = _clean_numeric_col()
    col["flags"] = ["constant"]
    col["distinct_count"] = 1
    col["unique_pct"] = 0.001
    out = column_quality_score(col)
    assert out["score"] == 100.0  # NO se castiga la baja informatividad
    assert not any("constante" in i for i in out["issues"])
    assert any("constante" in o for o in out["observations"])


def test_high_cardinality_id_scores_full_and_is_observation():
    """Id de alta cardinalidad: unicidad perfecta -> score 100; posible id = observación."""
    col = {
        "name": "uuid", "inferred_type": "text", "semantic_type": "",
        "null_pct": 0.0, "unique_pct": 0.99, "flags": ["possible_id"],
        "numeric": None,
    }
    out = column_quality_score(col)
    assert out["score"] == 100.0
    assert not any("identificador" in i for i in out["issues"])
    assert any("identificador" in o for o in out["observations"])


def test_mostly_null_no_double_counts_validity():
    """85% nulos: solo completeness penaliza; validity nativa sigue 1.0 (sin doble castigo)."""
    col = _clean_numeric_col()
    col["null_pct"] = 0.85
    col["flags"] = ["mostly_null"]
    out = column_quality_score(col)
    assert out["validity"] == 1.0  # ya no se multiplica por 0.5
    # score = 100*(0.6*0.15 + 0.4*1.0) = 49
    assert out["score"] == 49.0
    assert not any("mayoritariamente" in o for o in out["observations"])


# --------------------------------------------------------------------------- #
# Robustez
# --------------------------------------------------------------------------- #
def test_empty_dict_does_not_crash():
    out = column_quality_score({})
    assert isinstance(out["score"], float)
    assert out["completeness"] == 1.0
    assert 0.0 <= out["score"] <= 100.0
    assert isinstance(out["issues"], list)
    assert isinstance(out["observations"], list)


def test_none_values_treated_defensively():
    col = {
        "name": "x", "inferred_type": None, "semantic_type": None,
        "null_pct": None, "unique_pct": None, "flags": None, "numeric": None,
    }
    out = column_quality_score(col)
    assert out["completeness"] == 1.0
    assert isinstance(out["score"], float)


def test_does_not_mutate_input():
    col = _clean_numeric_col()
    col["flags"] = ["constant"]
    before = {k: (list(v) if isinstance(v, list) else v) for k, v in col.items()}
    column_quality_score(col)
    assert col["flags"] == before["flags"]
    assert col == before
