import pytest
from pytest import approx

from _plotly_utils.basevalidators import NumberValidator
import numpy as np
import pandas as pd
from ...test_optional.test_utils.test_utils import np_nan, np_inf


# Fixtures


@pytest.fixture
def validator(request):
    return NumberValidator("prop", "parent")


@pytest.fixture
def validator_min_max(request):
    return NumberValidator("prop", "parent", min=-1.0, max=2.0)


@pytest.fixture
def validator_min(request):
    return NumberValidator("prop", "parent", min=-1.0)


@pytest.fixture
def validator_max(request):
    return NumberValidator("prop", "parent", max=2.0)


@pytest.fixture
def validator_aok():
    return NumberValidator("prop", "parent", min=-1, max=1.5, array_ok=True)


# Array not ok


# Acceptance
@pytest.mark.parametrize(
    "val", [1.0, 0.0, 1, -1234.5678, 54321, np.pi, np_nan(), np_inf(), -np_inf()]
)
def test_acceptance(val, validator):
    assert validator.validate_coerce(val) == approx(val, nan_ok=True)


# Rejection by value
@pytest.mark.parametrize("val", ["hello", (), [], [1, 2, 3], set(), "34"])
def test_rejection_by_value(val, validator):
    with pytest.raises(ValueError) as validation_failure:
        validator.validate_coerce(val)

    assert "Invalid value" in str(validation_failure.value)


# With min/max
@pytest.mark.parametrize("val", [0, 0.0, -0.5, 1, 1.0, 2, 2.0, np.pi / 2.0])
def test_acceptance_min_max(val, validator_min_max):
    assert validator_min_max.validate_coerce(val) == approx(val)


@pytest.mark.parametrize("val", [-1.01, -10, 2.1, 234, -np_inf(), np_nan(), np_inf()])
def test_rejection_min_max(val, validator_min_max):
    with pytest.raises(ValueError) as validation_failure:
        validator_min_max.validate_coerce(val)

    assert "in the interval [-1.0, 2.0]" in str(validation_failure.value)


# With min only
@pytest.mark.parametrize("val", [0, 0.0, -0.5, 99999, np_inf()])
def test_acceptance_min(val, validator_min):
    assert validator_min.validate_coerce(val) == approx(val)


@pytest.mark.parametrize("val", [-1.01, -np_inf(), np_nan()])
def test_rejection_min(val, validator_min):
    with pytest.raises(ValueError) as validation_failure:
        validator_min.validate_coerce(val)

    assert "in the interval [-1.0, inf]" in str(validation_failure.value)


# With max only
@pytest.mark.parametrize("val", [0, 0.0, -np_inf(), -123456, np.pi / 2])
def test_acceptance_max(val, validator_max):
    assert validator_max.validate_coerce(val) == approx(val)


@pytest.mark.parametrize("val", [2.01, np_inf(), np_nan()])
def test_rejection_max(val, validator_max):
    with pytest.raises(ValueError) as validation_failure:
        validator_max.validate_coerce(val)

    assert "in the interval [-inf, 2.0]" in str(validation_failure.value)


# Array ok


# Acceptance
@pytest.mark.parametrize("val", [1.0, 0.0, 1, 0.4])
def test_acceptance_aok_scalars(val, validator_aok):
    assert validator_aok.validate_coerce(val) == val


@pytest.mark.parametrize("val", [[1.0, 0.0], [1], [-0.1234, 0.41, -1.0]])
def test_acceptance_aok_list(val, validator_aok):
    assert np.array_equal(
        validator_aok.validate_coerce(val), np.array(val, dtype="float")
    )


# Coerce
# Coerced to general consistent numeric type
@pytest.mark.parametrize(
    "val,expected",
    [
        ([1.0, 0], (1.0, 0)),
        (np.array([1, -1]), np.array([1, -1])),
        (pd.Series([1, -1]), np.array([1, -1])),
        (pd.Index([1, -1]), np.array([1, -1])),
        ((-0.1234, 0, -1), (-0.1234, 0.0, -1.0)),
    ],
)
def test_coercion_aok_list(val, expected, validator_aok):
    v = validator_aok.validate_coerce(val)
    if isinstance(val, (np.ndarray, pd.Series, pd.Index)):
        assert np.array_equal(v, expected)
    else:
        assert isinstance(v, list)
        assert validator_aok.present(v) == tuple(val)


# Rejection
@pytest.mark.parametrize("val", [["a", 4]])
def test_rejection_aok(val, validator_aok):
    with pytest.raises(ValueError) as validation_failure:
        validator_aok.validate_coerce(val)

    assert "Invalid element(s)" in str(validation_failure.value)


# Rejection by element
@pytest.mark.parametrize(
    "val",
    [
        [-1.6, 0.0],
        [1, 1.5, 2],
        [-0.1234, 0.41, np_nan()],
        [0, np_inf()],
        [0, -np_inf()],
    ],
)
def test_rejection_aok_min_max(val, validator_aok):
    with pytest.raises(ValueError) as validation_failure:
        validator_aok.validate_coerce(val)

    assert "Invalid element(s)" in str(validation_failure.value)
    assert "in the interval [-1, 1.5]" in str(validation_failure.value)
