import unittest
import warnings

import numpy
import pytest

import cupy
from cupy import testing


class TestRounding(unittest.TestCase):

    @testing.for_all_dtypes(no_complex=True)
    @testing.numpy_cupy_allclose(atol=1e-5)
    def check_unary(self, name, xp, dtype):
        a = testing.shaped_arange((2, 3), xp, dtype)
        return getattr(xp, name)(a)

    @testing.for_complex_dtypes()
    @testing.numpy_cupy_allclose(atol=1e-5)
    def check_unary_complex(self, name, xp, dtype):
        a = testing.shaped_arange((2, 3), xp, dtype)
        return getattr(xp, name)(a)

    @testing.for_complex_dtypes()
    def check_unary_complex_unsupported(self, name, dtype):
        for xp in (numpy, cupy):
            a = testing.shaped_arange((2, 3), xp, dtype)
            with pytest.raises(TypeError):
                getattr(xp, name)(a)

    @testing.for_dtypes(['?', 'b', 'h', 'i', 'q', 'e', 'f', 'd'])
    @testing.numpy_cupy_allclose(atol=1e-5)
    def check_unary_negative(self, name, xp, dtype):
        a = xp.array([-3, -2, -1, 1, 2, 3], dtype=dtype)
        return getattr(xp, name)(a)

    @testing.for_complex_dtypes()
    @testing.numpy_cupy_allclose(atol=1e-5)
    def check_unary_negative_complex(self, name, xp, dtype):
        a = xp.array([-3-3j, -2-2j, -1-1j, 1+1j, 2+2j, 3+3j], dtype=dtype)
        return getattr(xp, name)(a)

    def test_rint(self):
        self.check_unary('rint')
        self.check_unary_complex('rint')

    def test_rint_negative(self):
        self.check_unary_negative('rint')
        self.check_unary_negative_complex('rint')

    @testing.with_requires("numpy<2.0")
    def test_floor(self):
        self.check_unary('floor')
        self.check_unary_complex_unsupported('floor')

    @testing.with_requires("numpy<2.0")
    def test_ceil(self):
        self.check_unary('ceil')
        self.check_unary_complex_unsupported('ceil')

    @testing.with_requires("numpy<2.0")
    def test_trunc(self):
        self.check_unary('trunc')
        self.check_unary_complex_unsupported('trunc')

    @testing.with_requires("numpy<2.0")
    def test_fix(self):
        self.check_unary('fix')
        self.check_unary_complex_unsupported('fix')

    def test_around(self):
        self.check_unary('around')
        self.check_unary_complex('around')

    @testing.with_requires('numpy<2.0')
    @testing.for_all_dtypes()
    @testing.numpy_cupy_allclose(atol=1e-5)
    def test_round_(self, xp, dtype):
        a = testing.shaped_arange((2, 3), xp, dtype)
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            return xp.round_(a)

    def test_round(self):
        self.check_unary('round')
        self.check_unary_complex('round')


@testing.parameterize(*testing.product({
    'decimals': [-2, -1, 0, 1, 2],
}))
class TestRound(unittest.TestCase):

    shape = (20,)

    @testing.for_all_dtypes()
    @testing.numpy_cupy_allclose(atol=1e-5)
    def test_round(self, xp, dtype):
        if dtype == numpy.bool_:
            # avoid cast problem
            a = testing.shaped_random(self.shape, xp, scale=10, dtype=dtype)
            return xp.around(a, 0)
        if dtype == numpy.float16:
            # avoid accuracy problem
            a = testing.shaped_random(self.shape, xp, scale=10, dtype=dtype)
            return xp.around(a, 0)
        a = testing.shaped_random(self.shape, xp, scale=100, dtype=dtype)
        return xp.around(a, self.decimals)

    @testing.numpy_cupy_array_equal()
    def test_round_out(self, xp):
        a = testing.shaped_random(self.shape, xp, scale=100, dtype='d')
        out = xp.empty_like(a)
        xp.around(a, self.decimals, out)
        return out


@testing.parameterize(*testing.product({
    'decimals': [-100, -99, -90, 0, 90, 99, 100],
}))
class TestRoundExtreme(unittest.TestCase):

    shape = (20,)

    @testing.for_dtypes([numpy.float64, numpy.complex128])
    @testing.numpy_cupy_allclose()
    def test_round_large(self, xp, dtype):
        a = testing.shaped_random(self.shape, xp, scale=1e100, dtype=dtype)
        return xp.around(a, self.decimals)

    @testing.for_dtypes([numpy.float64, numpy.complex128])
    @testing.numpy_cupy_allclose()
    def test_round_small(self, xp, dtype):
        a = testing.shaped_random(self.shape, xp, scale=1e-100, dtype=dtype)
        return xp.around(a, self.decimals)


@testing.parameterize(*testing.product({
    'value': [
        (14, -1),
        (15, -1),
        (16, -1),
        (14.0, -1),
        (15.0, -1),
        (16.0, -1),
        (1.4, 0),
        (1.5, 0),
        (1.6, 0),
    ]
}))
class TestRoundBorder(unittest.TestCase):

    @testing.numpy_cupy_allclose(atol=1e-5)
    def test_around_positive1(self, xp):
        a, decimals = self.value
        return xp.around(a, decimals)

    @testing.numpy_cupy_allclose(atol=1e-5)
    def test_around_positive2(self, xp):
        a, decimals = self.value
        a = xp.asarray(a)
        return xp.around(a, decimals)

    @testing.numpy_cupy_allclose(atol=1e-5)
    def test_around_negative1(self, xp):
        a, decimals = self.value
        return xp.around(-a, decimals)

    @testing.numpy_cupy_allclose(atol=1e-5)
    def test_around_negative2(self, xp):
        a, decimals = self.value
        a = xp.asarray(a)
        return xp.around(-a, decimals)
