"""Tests para classify_relationship_type."""

import os
import sys

import numpy as np

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

from classify_relationship_type import classify_relationship_type

# Claves que el dict de salida debe contener SIEMPRE.
_EXPECTED_KEYS = {
    "tipo", "pearson", "r2_linear", "spearman",
    "r2_poly2", "r2_poly3", "best_degree", "coeffs",
}


def _assert_shape(r):
    """Toda salida tiene exactamente las 8 claves canonicas."""
    assert isinstance(r, dict)
    assert set(r.keys()) == _EXPECTED_KEYS


def test_lineal():
    """Golden: y = 2x + 1 con ruido pequeno -> 'lineal', best_degree=1."""
    rng = np.random.default_rng(42)
    x = np.linspace(0.0, 10.0, 50)
    y = 2.0 * x + 1.0 + rng.normal(0.0, 0.3, 50)

    r = classify_relationship_type(list(x), list(y))
    _assert_shape(r)

    assert r["tipo"] == "lineal"
    assert r["best_degree"] == 1
    assert r["r2_linear"] >= 0.5
    # coeffs ~ [pendiente, intercepto] del ajuste de grado 1.
    assert r["coeffs"] is not None and len(r["coeffs"]) == 2
    assert abs(r["coeffs"][0] - 2.0) < 0.1   # pendiente ~2
    assert abs(r["coeffs"][1] - 1.0) < 0.3   # intercepto ~1


def test_polinomica_cuadratica():
    """Golden: y = x**2 sobre [-10, 10] -> 'polinómica', best_degree in (2, 3)."""
    x = np.linspace(-10.0, 10.0, 60)
    y = x ** 2

    r = classify_relationship_type(list(x), list(y))
    _assert_shape(r)

    assert r["tipo"].startswith("polinómica")
    assert r["best_degree"] in (2, 3)
    # Una parabola perfecta queda capturada por el grado 2 (parsimonia).
    assert r["best_degree"] == 2
    assert r["r2_poly2"] > 0.99
    assert r["coeffs"] is not None and len(r["coeffs"]) == r["best_degree"] + 1


def test_monotona_no_lineal():
    """Golden: monotona convexa de cola pesada -> 'monótona no-lineal'.

    y = 1/(N+1-i)**2 es estrictamente creciente (Spearman ~ 1) pero su cola
    explosiva hace que ni la recta ni un polinomio de grado 2/3 la ajusten
    (R^2 polinomico < 0.5), de modo que el Pearson lineal NO capta la relacion
    que el rango (Spearman) si ve. Construccion deterministica (sin azar).
    """
    n = 200
    i = np.arange(n, dtype=float)
    y = 1.0 / (n + 1 - i) ** 2

    r = classify_relationship_type(list(i), list(y))
    _assert_shape(r)

    assert r["tipo"] == "monótona no-lineal"
    assert r["best_degree"] is None
    assert r["coeffs"] is None
    # Spearman fuerte y claramente por encima del Pearson.
    assert abs(r["spearman"]) >= 0.5
    assert abs(r["spearman"]) - abs(r["pearson"]) >= 0.15


def test_monotona_exponencial():
    """DoD literal: y = exp(x) (monotona no-lineal) -> 'monótona no-lineal'.

    exp es estrictamente creciente (Spearman = 1) pero el Pearson lineal queda
    claramente por debajo (~0.86), así que la dominancia del rango la marca como
    monótona no-lineal en vez de lineal o polinómica.
    """
    x = np.linspace(0.0, 5.0, 80)
    y = np.exp(x)

    r = classify_relationship_type(list(x), list(y))
    _assert_shape(r)

    assert r["tipo"] == "monótona no-lineal"
    assert r["best_degree"] is None and r["coeffs"] is None
    assert abs(r["spearman"]) >= 0.9
    assert abs(r["spearman"]) - abs(r["pearson"]) >= 0.1


def test_debil_sin_forma():
    """Golden: x e y independientes (semilla fija) -> 'débil/sin forma'."""
    rng = np.random.default_rng(0)
    x = rng.normal(0.0, 1.0, 200)
    y = rng.normal(0.0, 1.0, 200)

    r = classify_relationship_type(list(x), list(y))
    _assert_shape(r)

    assert r["tipo"] == "débil/sin forma"
    assert r["best_degree"] is None
    assert r["coeffs"] is None
    # Todas las senales son bajas.
    assert abs(r["pearson"]) < 0.3
    assert r["r2_linear"] < 0.1


def test_lista_vacia_no_lanza():
    """Edge: listas vacias -> dict debil canonico, sin lanzar."""
    r = classify_relationship_type([], [])
    _assert_shape(r)
    assert r["tipo"] == "débil/sin forma"
    assert r["pearson"] is None
    assert r["r2_linear"] is None
    assert r["spearman"] is None
    assert r["r2_poly2"] is None
    assert r["r2_poly3"] is None
    assert r["best_degree"] is None
    assert r["coeffs"] is None


def test_longitudes_distintas_no_lanza():
    """Edge: listas de distinta longitud -> empareja por indice, no lanza."""
    # zip trunca a la longitud minima: solo 3 pares (< 5) -> debil.
    r = classify_relationship_type([1, 2, 3, 4, 5, 6, 7, 8], [1.0, 2.0, 3.0])
    _assert_shape(r)
    assert r["tipo"] == "débil/sin forma"
    assert r["best_degree"] is None


def test_todos_none_no_lanza():
    """Edge: todos los valores None -> ningun par valido -> debil, no lanza."""
    r = classify_relationship_type([None, None, None, None, None, None],
                                   [None, None, None, None, None, None])
    _assert_shape(r)
    assert r["tipo"] == "débil/sin forma"
    assert r["coeffs"] is None


def test_entradas_none_no_lanza():
    """Edge: xs/ys None directamente -> debil, no lanza."""
    assert classify_relationship_type(None, None)["tipo"] == "débil/sin forma"
    assert classify_relationship_type([1.0, 2.0], None)["tipo"] == "débil/sin forma"


def test_constante_no_lanza():
    """Edge: ys constante (varianza ~0) -> debil, no lanza."""
    r = classify_relationship_type([1, 2, 3, 4, 5, 6, 7], [5, 5, 5, 5, 5, 5, 5])
    _assert_shape(r)
    assert r["tipo"] == "débil/sin forma"


def test_filtra_nan_inf_bool():
    """Edge: pares con NaN/inf/bool/None se descartan por indice."""
    nan = float("nan")
    inf = float("inf")
    # Solo i=0,1,2,3,4 quedan validos (5 pares) y forman una recta perfecta.
    xs = [0.0, 1.0, 2.0, 3.0, 4.0, nan, inf, True, None]
    ys = [1.0, 3.0, 5.0, 7.0, 9.0, 1.0, 2.0, 3.0, 4.0]
    r = classify_relationship_type(xs, ys)
    _assert_shape(r)
    # Los 5 pares validos son y = 2x + 1 exacto -> lineal.
    assert r["tipo"] == "lineal"
    assert r["best_degree"] == 1
