"""
Test the scalar constructors, which also do type-coercion
"""
import fractions
import inspect
import platform
import sys
import types
from typing import Any, Literal

import pytest

import numpy as np
from numpy._core import sctypes
from numpy.testing import IS_PYPY, assert_equal, assert_raises


class TestAsIntegerRatio:
    # derived in part from the cpython test "test_floatasratio"

    @pytest.mark.parametrize("ftype", [
        np.half, np.single, np.double, np.longdouble])
    @pytest.mark.parametrize("f, ratio", [
        (0.875, (7, 8)),
        (-0.875, (-7, 8)),
        (0.0, (0, 1)),
        (11.5, (23, 2)),
        ])
    def test_small(self, ftype, f, ratio):
        assert_equal(ftype(f).as_integer_ratio(), ratio)

    @pytest.mark.parametrize("ftype", [
        np.half, np.single, np.double, np.longdouble])
    def test_simple_fractions(self, ftype):
        R = fractions.Fraction
        assert_equal(R(0, 1),
                     R(*ftype(0.0).as_integer_ratio()))
        assert_equal(R(5, 2),
                     R(*ftype(2.5).as_integer_ratio()))
        assert_equal(R(1, 2),
                     R(*ftype(0.5).as_integer_ratio()))
        assert_equal(R(-2100, 1),
                     R(*ftype(-2100.0).as_integer_ratio()))

    @pytest.mark.parametrize("ftype", [
        np.half, np.single, np.double, np.longdouble])
    def test_errors(self, ftype):
        assert_raises(OverflowError, ftype('inf').as_integer_ratio)
        assert_raises(OverflowError, ftype('-inf').as_integer_ratio)
        assert_raises(ValueError, ftype('nan').as_integer_ratio)

    def test_against_known_values(self):
        R = fractions.Fraction
        assert_equal(R(1075, 512),
                     R(*np.half(2.1).as_integer_ratio()))
        assert_equal(R(-1075, 512),
                     R(*np.half(-2.1).as_integer_ratio()))
        assert_equal(R(4404019, 2097152),
                     R(*np.single(2.1).as_integer_ratio()))
        assert_equal(R(-4404019, 2097152),
                     R(*np.single(-2.1).as_integer_ratio()))
        assert_equal(R(4728779608739021, 2251799813685248),
                     R(*np.double(2.1).as_integer_ratio()))
        assert_equal(R(-4728779608739021, 2251799813685248),
                     R(*np.double(-2.1).as_integer_ratio()))
        # longdouble is platform dependent

    @pytest.mark.parametrize("ftype, frac_vals, exp_vals", [
        # dtype test cases generated using hypothesis
        # first five generated cases per dtype
        (np.half, [0.0, 0.01154830649280303, 0.31082276347447274,
                   0.527350517124794, 0.8308562335072596],
                  [0, 1, 0, -8, 12]),
        (np.single, [0.0, 0.09248576989263226, 0.8160498218131407,
                     0.17389442853722373, 0.7956044195067877],
                    [0, 12, 10, 17, -26]),
        (np.double, [0.0, 0.031066908499895136, 0.5214135908877832,
                     0.45780736035689296, 0.5906586745934036],
                    [0, -801, 51, 194, -653]),
        pytest.param(
            np.longdouble,
            [0.0, 0.20492557202724854, 0.4277180662199366, 0.9888085019891495,
             0.9620175814461964],
            [0, -7400, 14266, -7822, -8721],
            marks=[
                pytest.mark.skipif(
                    np.finfo(np.double) == np.finfo(np.longdouble),
                    reason="long double is same as double"),
                pytest.mark.skipif(
                    platform.machine().startswith("ppc"),
                    reason="IBM double double"),
            ]
        )
    ])
    def test_roundtrip(self, ftype, frac_vals, exp_vals):
        for frac, exp in zip(frac_vals, exp_vals):
            f = np.ldexp(ftype(frac), exp)
            assert f.dtype == ftype
            n, d = f.as_integer_ratio()

            try:
                nf = np.longdouble(n)
                df = np.longdouble(d)
                if not np.isfinite(df):
                    raise OverflowError
            except (OverflowError, RuntimeWarning):
                # the values may not fit in any float type
                pytest.skip("longdouble too small on this platform")

            assert_equal(nf / df, f, f"{n}/{d}")


class TestIsInteger:
    @pytest.mark.parametrize("str_value", ["inf", "nan"])
    @pytest.mark.parametrize("code", np.typecodes["Float"])
    def test_special(self, code: str, str_value: str) -> None:
        cls = np.dtype(code).type
        value = cls(str_value)
        assert not value.is_integer()

    @pytest.mark.parametrize(
        "code", np.typecodes["Float"] + np.typecodes["AllInteger"]
    )
    def test_true(self, code: str) -> None:
        float_array = np.arange(-5, 5).astype(code)
        for value in float_array:
            assert value.is_integer()

    @pytest.mark.parametrize("code", np.typecodes["Float"])
    def test_false(self, code: str) -> None:
        float_array = np.arange(-5, 5).astype(code)
        float_array *= 1.1
        for value in float_array:
            if value == 0:
                continue
            assert not value.is_integer()


class TestClassGetItem:
    @pytest.mark.parametrize("cls", [
        np.number,
        np.integer,
        np.inexact,
        np.unsignedinteger,
        np.signedinteger,
        np.floating,
    ])
    def test_abc(self, cls: type[np.number]) -> None:
        alias = cls[Any]
        assert isinstance(alias, types.GenericAlias)
        assert alias.__origin__ is cls

    def test_abc_complexfloating(self) -> None:
        alias = np.complexfloating[Any, Any]
        assert isinstance(alias, types.GenericAlias)
        assert alias.__origin__ is np.complexfloating

    @pytest.mark.parametrize("arg_len", range(4))
    def test_abc_complexfloating_subscript_tuple(self, arg_len: int) -> None:
        arg_tup = (Any,) * arg_len
        if arg_len in (1, 2):
            assert np.complexfloating[arg_tup]
        else:
            match = f"Too {'few' if arg_len == 0 else 'many'} arguments"
            with pytest.raises(TypeError, match=match):
                np.complexfloating[arg_tup]

    @pytest.mark.parametrize("cls", [np.generic, np.flexible, np.character])
    def test_abc_non_numeric(self, cls: type[np.generic]) -> None:
        with pytest.raises(TypeError):
            cls[Any]

    @pytest.mark.parametrize("code", np.typecodes["All"])
    def test_concrete(self, code: str) -> None:
        cls = np.dtype(code).type
        if cls in {np.bool, np.datetime64}:
            # these are intentionally subscriptable
            assert cls[Any]
        else:
            with pytest.raises(TypeError):
                cls[Any]

    @pytest.mark.parametrize("arg_len", range(4))
    def test_subscript_tuple(self, arg_len: int) -> None:
        arg_tup = (Any,) * arg_len
        if arg_len == 1:
            assert np.number[arg_tup]
        else:
            with pytest.raises(TypeError):
                np.number[arg_tup]

    def test_subscript_scalar(self) -> None:
        assert np.number[Any]

    @pytest.mark.parametrize("subscript", [Literal[True], Literal[False]])
    def test_subscript_bool(self, subscript: Literal[True, False]) -> None:
        assert isinstance(np.bool[subscript], types.GenericAlias)


class TestBitCount:
    # derived in part from the cpython test "test_bit_count"

    @pytest.mark.parametrize("itype", sctypes['int'] + sctypes['uint'])
    def test_small(self, itype):
        for a in range(max(np.iinfo(itype).min, 0), 128):
            msg = f"Smoke test for {itype}({a}).bit_count()"
            assert itype(a).bit_count() == a.bit_count(), msg

    def test_bit_count(self):
        for exp in [10, 17, 63]:
            a = 2**exp
            assert np.uint64(a).bit_count() == 1
            assert np.uint64(a - 1).bit_count() == exp
            assert np.uint64(a ^ 63).bit_count() == 7
            assert np.uint64((a - 1) ^ 510).bit_count() == exp - 8


class TestDevice:
    """
    Test scalar.device attribute and scalar.to_device() method.
    """
    scalars = [np.bool(True), np.int64(1), np.uint64(1), np.float64(1.0),
               np.complex128(1 + 1j)]

    @pytest.mark.parametrize("scalar", scalars)
    def test_device(self, scalar):
        assert scalar.device == "cpu"

    @pytest.mark.parametrize("scalar", scalars)
    def test_to_device(self, scalar):
        assert scalar.to_device("cpu") is scalar

    @pytest.mark.parametrize("scalar", scalars)
    def test___array_namespace__(self, scalar):
        assert scalar.__array_namespace__() is np


@pytest.mark.parametrize("scalar", [np.bool(True), np.int8(1), np.float64(1)])
def test_array_wrap(scalar):
    # Test scalars array wrap as long as it exists.  NumPy itself should
    # probably not use it, so it may not be necessary to keep it around.

    arr0d = np.array(3, dtype=np.int8)
    # Third argument not passed, None, or True "decays" to scalar.
    # (I don't think NumPy would pass `None`, but it seems clear to support)
    assert type(scalar.__array_wrap__(arr0d)) is np.int8
    assert type(scalar.__array_wrap__(arr0d, None, None)) is np.int8
    assert type(scalar.__array_wrap__(arr0d, None, True)) is np.int8

    # Otherwise, result should be the input
    assert scalar.__array_wrap__(arr0d, None, False) is arr0d

    # An old bug.  A non 0-d array cannot be converted to scalar:
    arr1d = np.array([3], dtype=np.int8)
    assert scalar.__array_wrap__(arr1d) is arr1d
    assert scalar.__array_wrap__(arr1d, None, True) is arr1d


@pytest.mark.skipif(sys.flags.optimize == 2, reason="Python running -OO")
@pytest.mark.skipif(IS_PYPY, reason="PyPy does not modify tp_doc")
class TestSignature:
    # test that scalar types have a valid __text_signature__ or __signature__ set
    @pytest.mark.parametrize(
        "sctype",
        [
            *sctypes["int"],
            *sctypes["uint"],
            *sctypes["float"],
            *sctypes["complex"],
            *sctypes["others"],
            np.datetime64,
            np.timedelta64,
        ],
    )
    def test_constructor_signatures(self, sctype: type[np.generic]):
        try:
            sig = inspect.signature(sctype)
        except ValueError:
            pytest.fail(f"missing signature: {sctype}")

        assert sig.parameters

    @pytest.mark.parametrize(
        "sctype",
        [np.integer, *sctypes["int"], *sctypes["uint"], *sctypes["float"]],
    )
    def test_method_signatures_is_integer(self, sctype: type[np.integer | np.floating]):
        try:
            sig = inspect.signature(sctype.is_integer)
        except ValueError:
            pytest.fail(f"missing signature: {sctype.__name__}.is_integer")

        assert len(sig.parameters) == 1
        assert sig.parameters["self"].kind == inspect.Parameter.POSITIONAL_ONLY

    @pytest.mark.parametrize("sctype", sctypes["float"])
    def test_method_signatures_as_integer_ratio(self, sctype: type[np.floating]):
        try:
            sig = inspect.signature(sctype.as_integer_ratio)
        except ValueError:
            pytest.fail(f"missing signature: {sctype.__name__}.as_integer_ratio")

        assert len(sig.parameters) == 1
        assert sig.parameters["self"].kind == inspect.Parameter.POSITIONAL_ONLY

    @pytest.mark.parametrize(
        "method_name",
        [
            "__array_namespace__", "__copy__", "__deepcopy__", "all", "any", "argmax",
            "argmin", "argsort", "astype", "byteswap", "choose", "clip", "compress",
            "conj", "conjugate", "copy", "cumprod", "cumsum", "diagonal", "dump",
            "dumps", "fill", "flatten", "getfield", "item", "max", "mean", "min",
            "nonzero", "prod", "put", "ravel", "repeat", "reshape", "resize", "round",
            "searchsorted", "setfield", "setflags", "sort", "squeeze", "std", "sum",
            "swapaxes", "take", "to_device", "tobytes", "tofile", "tolist", "trace",
            "transpose", "var", "view",
        ],
    )
    def test_array_scalar_method_signatures(self, method_name: str):
        # methods shared by np.generic and np.ndarray should have the same signature
        fn_generic = getattr(np.generic, method_name)
        sig_generic = inspect.signature(fn_generic)
        assert "self" in sig_generic.parameters
        assert sig_generic.parameters["self"].kind is inspect.Parameter.POSITIONAL_ONLY

        fn_ndarray = getattr(np.ndarray, method_name)
        sig_ndarray = inspect.signature(fn_ndarray)
        assert sig_generic == sig_ndarray
