import numpy as np
from numpy.testing import assert_allclose
import pytest

from statsmodels.robust import norms
from statsmodels.tools.numdiff import (  # _approx_fprime_cs_scalar,  # not yet
    _approx_fprime_scalar,
)

from .results import results_norms as res_r

cases = [
    (norms.Hampel, (1.5, 3.5, 8.), res_r.res_hampel),
    (norms.TukeyBiweight, (4,), res_r.res_biweight),
    (norms.HuberT, (1.345,), res_r.res_huber),
    ]

norms_other = [
    (norms.LeastSquares, ()),
    (norms.TrimmedMean, (1.9,)),  # avoid arg at integer used in example
    (norms.HuberT, ()),
    (norms.AndrewWave, ()),
    (norms.RamsayE, ()),
    (norms.Hampel, ()),
    (norms.TukeyBiweight, ()),
    (norms.TukeyQuartic, ()),
    (norms.StudentT, ()),
    # norms.MQuantileNorm,  # requires keywords in init
    ]

dtypes = ["int", np.float64, np.complex128]


@pytest.mark.parametrize("dtype", dtypes)
@pytest.mark.parametrize("case", cases)
def test_norm(case, dtype):
    ncls, args, res = case
    if ncls in [norms.HuberT] and dtype == np.complex128:
        # skip for now
        return

    norm = ncls(*args)
    x = np.array([-9, -6, -2, -1, 0, 1, 2, 6, 9], dtype=dtype)

    weights = norm.weights(x)
    rho = norm.rho(x)
    psi = norm.psi(x)
    psi_deriv = norm.psi_deriv(x)
    assert_allclose(weights, res.weights, rtol=1e-12, atol=1e-20)
    assert_allclose(rho, res.rho, rtol=1e-12, atol=1e-20)
    assert_allclose(psi, res.psi, rtol=1e-12, atol=1e-20)
    assert_allclose(psi_deriv, res.psi_deriv, rtol=1e-12, atol=1e-20)

    dtype2 = np.promote_types(dtype, "float")
    assert weights.dtype == dtype2
    assert rho.dtype == dtype2
    assert psi.dtype == dtype2
    assert psi_deriv.dtype == dtype2

    psid = _approx_fprime_scalar(x, norm.rho)
    assert_allclose(psid, res.psi, rtol=1e-6, atol=1e-8)
    psidd = _approx_fprime_scalar(x, norm.psi)
    assert_allclose(psidd, res.psi_deriv, rtol=1e-6, atol=1e-8)

    # complex step derivatives are not yet supported if method uses np.abs
    # psid = _approx_fprime_cs_scalar(x, norm.rho)
    # assert_allclose(psid, res.psi, rtol=1e-12, atol=1e-20)
    # psidd = _approx_fprime_cs_scalar(x, norm.psi)
    # assert_allclose(psidd, res.psi_deriv, rtol=1e-12, atol=1e-20)

    # check scalar value
    methods = ["weights", "rho", "psi", "psi_deriv"]
    for meth in methods:
        resm = [getattr(norm, meth)(xi) for xi in x]
        assert_allclose(resm, getattr(res, meth))


@pytest.mark.parametrize("case", norms_other)
def test_norms_consistent(case):
    # test that norm methods are consistent with each other
    ncls, args = case
    norm = ncls(*args)
    x = np.array([-9, -6, -2, -1, 0, 1, 2 - 1e-4, 6, 9], dtype=float)
    # 2 - 1e-4 because Hample psi has discontinuity at 2, numdiff problem

    weights = norm.weights(x)
    rho = norm.rho(x)  # not used
    psi = norm.psi(x)
    psi_deriv = norm.psi_deriv(x)

    # check location and u-shape of rho
    assert_allclose(rho[4], 0, atol=1e-12)
    assert np.all(np.diff(rho[4:]) >= 0)
    assert np.all(np.diff(rho[:4]) <= 0)

    # check weights at and around zero
    assert_allclose(weights[4], 1, atol=1e-12)
    assert np.all(norm.weights([-1e-6, 1e-6]) >= 1 - 1e-5)

    # avoid zero division nan:
    assert_allclose(weights, (psi + 1e-50) / (x + 1e-50), rtol=1e-6, atol=1e-8)
    psid = _approx_fprime_scalar(x, norm.rho)
    assert_allclose(psi, psid, rtol=1e-6, atol=1e-6)
    psidd = _approx_fprime_scalar(x, norm.psi)
    assert_allclose(psi_deriv, psidd, rtol=1e-6, atol=1e-8)

    # attributes
    if norm.redescending == "hard":
        assert_allclose(norm.max_rho(), norm.rho(100), rtol=1e-12)
    else:
        assert np.isposinf(norm.max_rho())

    if norm.redescending == "soft":
        # we don't have info where argmax psi is, use simple values for x
        assert norm.psi(100) < norm.psi(2)
