"""Tests para categorical_cardinality_block."""

import sys
import os
from math import log2

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

from categorical_cardinality_block import categorical_cardinality_block


# Output contract: every call returns exactly these 16 keys.
EXPECTED_KEYS = {
    "n_distinct",
    "n_rows",
    "pct_distinct",
    "entropy",
    "entropy_max",
    "entropy_norm",
    "mode",
    "mode_pct",
    "imbalance",
    "n_singletons",
    "n_singletons_partial",
    "len_min",
    "len_mean",
    "len_max",
    "id_like",
    "dominated",
}


def _sample_cat():
    """A realistic summarize_categorical output for one column."""
    return {
        "top": [
            {"value": "a", "count": 5, "pct": 0.5},
            {"value": "b", "count": 3, "pct": 0.3},
            {"value": "c", "count": 1, "pct": 0.1},
            {"value": "d", "count": 1, "pct": 0.1},
        ],
        "mode": "a",
        "mode_pct": 0.5,
        "n_distinct": 4,
        "entropy": 1.685,  # <= log2(4) = 2.0
        "imbalance": 5.0,
        "len_min": 1,
        "len_mean": 1.0,
        "len_max": 1,
    }


def test_normal_case():
    """Caso normal: pct_distinct, entropy_max=log2(n_distinct), entropy_norm in [0,1], n_singletons."""
    cat = _sample_cat()
    result = categorical_cardinality_block(cat, n_rows=10)

    assert set(result.keys()) == EXPECTED_KEYS

    # passthroughs
    assert result["n_distinct"] == 4
    assert result["n_rows"] == 10
    assert result["entropy"] == 1.685
    assert result["imbalance"] == 5.0
    assert result["mode"] == "a"
    assert result["mode_pct"] == 0.5  # passthrough, not normalized
    assert result["len_min"] == 1
    assert result["len_max"] == 1

    # pct_distinct = 4 / 10 * 100
    assert abs(result["pct_distinct"] - 40.0) < 1e-12

    # entropy_max = log2(4) = 2.0
    assert abs(result["entropy_max"] - log2(4)) < 1e-12
    assert abs(result["entropy_max"] - 2.0) < 1e-12

    # entropy_norm = 1.685 / 2.0 = 0.8425, within [0, 1]
    assert abs(result["entropy_norm"] - 1.685 / 2.0) < 1e-12
    assert 0.0 <= result["entropy_norm"] <= 1.0

    # singletons: c and d have count == 1
    assert result["n_singletons"] == 2
    # top covers all distinct values (4 == 4)
    assert result["n_singletons_partial"] is False

    # neither id-like (40%) nor dominated (mode_pct 0.5)
    assert result["id_like"] is False
    assert result["dominated"] is False


def test_empty_cat_does_not_raise():
    """Caso cat={}: no lanza, claves derivadas None y flags False."""
    result = categorical_cardinality_block({}, n_rows=100)

    assert set(result.keys()) == EXPECTED_KEYS
    for key in (
        "n_distinct",
        "pct_distinct",
        "entropy",
        "entropy_max",
        "entropy_norm",
        "mode",
        "mode_pct",
        "imbalance",
        "n_singletons",
        "len_min",
        "len_mean",
        "len_max",
    ):
        assert result[key] is None
    assert result["n_singletons_partial"] is False
    assert result["id_like"] is False
    assert result["dominated"] is False
    # n_rows is a passthrough of the argument, still coherent.
    assert result["n_rows"] == 100


def test_none_cat_does_not_raise():
    """Caso cat=None: tratado como {}, mismas garantias que el dict vacio."""
    result = categorical_cardinality_block(None, n_rows=None)
    assert set(result.keys()) == EXPECTED_KEYS
    assert result["n_distinct"] is None
    assert result["pct_distinct"] is None
    assert result["entropy_max"] is None
    assert result["entropy_norm"] is None
    assert result["id_like"] is False
    assert result["dominated"] is False


def test_n_rows_zero_no_zero_division():
    """Caso n_rows=0: pct_distinct None sin ZeroDivisionError."""
    cat = _sample_cat()
    result = categorical_cardinality_block(cat, n_rows=0)
    assert result["pct_distinct"] is None
    # n_distinct still passes through.
    assert result["n_distinct"] == 4
    assert result["id_like"] is False


def test_id_like_when_distinct_near_rows():
    """id_like True cuando n_distinct ~ n_rows (pct_distinct >= 99)."""
    cat = {"n_distinct": 99, "entropy": 6.6, "top": [], "mode": None}
    result = categorical_cardinality_block(cat, n_rows=100)
    assert abs(result["pct_distinct"] - 99.0) < 1e-12
    assert result["id_like"] is True

    # exact identity column: 100 / 100 = 100%
    cat_full = {"n_distinct": 100, "top": []}
    result_full = categorical_cardinality_block(cat_full, n_rows=100)
    assert result_full["id_like"] is True


def test_dominated_when_mode_pct_high():
    """dominated True cuando mode_pct alto (>= 90)."""
    cat = {
        "n_distinct": 3,
        "entropy": 0.3,
        "mode": "x",
        "mode_pct": 95.0,
        "top": [
            {"value": "x", "count": 95, "pct": 0.95},
            {"value": "y", "count": 3, "pct": 0.03},
            {"value": "z", "count": 2, "pct": 0.02},
        ],
        "imbalance": 47.5,
    }
    result = categorical_cardinality_block(cat, n_rows=100)
    assert result["mode_pct"] == 95.0
    assert result["dominated"] is True


def test_mode_pct_fallback_from_top_fraction():
    """Sin mode_pct: deriva del pct del primer top, fraccion <=1 escala a 0-100."""
    cat = {
        "n_distinct": 3,
        "top": [
            {"value": "x", "count": 95, "pct": 0.95},
            {"value": "y", "count": 5, "pct": 0.05},
        ],
    }
    result = categorical_cardinality_block(cat, n_rows=100)
    # 0.95 (fraction) -> 95.0 (percentage)
    assert abs(result["mode_pct"] - 95.0) < 1e-12
    assert result["dominated"] is True


def test_n_singletons_partial_when_top_truncated():
    """n_distinct > len(top): n_singletons cubre solo el top visible, partial True."""
    cat = {
        "n_distinct": 10,
        "top": [
            {"value": "a", "count": 4, "pct": 0.4},
            {"value": "b", "count": 1, "pct": 0.1},
            {"value": "c", "count": 1, "pct": 0.1},
        ],
        "entropy": 2.5,
    }
    result = categorical_cardinality_block(cat, n_rows=12)
    assert result["n_singletons"] == 2  # only b, c visible
    assert result["n_singletons_partial"] is True


def test_single_distinct_value_entropy_norm_none():
    """n_distinct=1: entropy_max=0.0 -> entropy_norm None (no division by zero)."""
    cat = {
        "n_distinct": 1,
        "entropy": 0.0,
        "mode": "only",
        "mode_pct": 1.0,
        "top": [{"value": "only", "count": 7, "pct": 1.0}],
        "imbalance": 1.0,
    }
    result = categorical_cardinality_block(cat, n_rows=7)
    assert result["entropy_max"] == 0.0
    assert result["entropy_norm"] is None
    assert result["n_singletons"] == 0
