import operator
import unittest

import numpy
import pytest

import dpnp as cupy
from tests.third_party.cupy import testing


class TestArrayBoolOp(unittest.TestCase):
    @pytest.mark.skip("The truth value of an empty array is ambiguous")
    @testing.for_all_dtypes()
    def test_bool_empty(self, dtype):
        with testing.assert_warns(DeprecationWarning):
            assert not bool(cupy.array((), dtype=dtype))

    def test_bool_scalar_bool(self):
        assert bool(cupy.array(True, dtype=numpy.bool_))
        assert not bool(cupy.array(False, dtype=numpy.bool_))

    @testing.for_all_dtypes()
    def test_bool_scalar(self, dtype):
        assert bool(cupy.array(1, dtype=dtype))
        assert not bool(cupy.array(0, dtype=dtype))

    def test_bool_one_element_bool(self):
        assert bool(cupy.array([True], dtype=numpy.bool_))
        assert not bool(cupy.array([False], dtype=numpy.bool_))

    @testing.for_all_dtypes()
    def test_bool_one_element(self, dtype):
        assert bool(cupy.array([1], dtype=dtype))
        assert not bool(cupy.array([0], dtype=dtype))

    @testing.for_all_dtypes()
    def test_bool_two_elements(self, dtype):
        with self.assertRaises(ValueError):
            bool(cupy.array([1, 2], dtype=dtype))


class TestArrayUnaryOp(unittest.TestCase):
    @testing.for_all_dtypes(no_bool=True)
    @testing.numpy_cupy_allclose()
    def check_array_op(self, op, xp, dtype):
        a = testing.shaped_arange((2, 3), xp, dtype)
        return op(a)

    @testing.for_all_dtypes()
    @testing.numpy_cupy_allclose()
    def check_array_op_full(self, op, xp, dtype):
        a = testing.shaped_arange((2, 3), xp, dtype)
        return op(a)

    @testing.for_all_dtypes(no_bool=True)
    @testing.numpy_cupy_allclose()
    def test_neg_array(self, xp, dtype):
        a = testing.shaped_arange((2, 3), xp, dtype)
        return operator.neg(a)

    @testing.for_all_dtypes(no_bool=True)
    @testing.numpy_cupy_allclose()
    def test_pos_array(self, xp, dtype):
        a = testing.shaped_arange((2, 3), xp, dtype)
        assert a is not +a
        return +a

    @pytest.mark.skip(
        "dpnp boolean positive, the `+` operator, is not supported"
    )
    @testing.with_requires("numpy<1.25")
    def test_pos_boolarray(self):
        for xp in (numpy, cupy):
            a = xp.array(True, dtype=xp.bool_)
            with pytest.deprecated_call():
                assert a is not +a

    @testing.with_requires("numpy<1.16")
    def test_pos_array_full(self):
        self.check_array_op_full(operator.pos)

    def test_abs_array(self):
        self.check_array_op_full(operator.abs)

    @testing.for_all_dtypes(no_bool=True)
    @testing.numpy_cupy_allclose()
    def check_zerodim_op(self, op, xp, dtype):
        a = xp.array(-2).astype(dtype)
        return op(a)

    @testing.for_all_dtypes()
    @testing.numpy_cupy_allclose()
    def check_zerodim_op_full(self, op, xp, dtype):
        a = xp.array(-2).astype(dtype)
        return op(a)

    @testing.for_all_dtypes(no_bool=True)
    @testing.numpy_cupy_allclose()
    def test_neg_zerodim(self, xp, dtype):
        a = xp.array(-2).astype(dtype)
        return operator.neg(a)

    def test_pos_zerodim(self):
        self.check_zerodim_op(operator.pos)

    def test_abs_zerodim(self):
        self.check_zerodim_op_full(operator.abs)

    def test_abs_zerodim_full(self):
        self.check_zerodim_op_full(operator.abs)


class TestArrayIntUnaryOp(unittest.TestCase):
    @testing.for_int_dtypes()
    @testing.numpy_cupy_allclose()
    def check_array_op(self, op, xp, dtype):
        a = testing.shaped_arange((2, 3), xp, dtype)
        return op(a)

    def test_invert_array(self):
        self.check_array_op(operator.invert)

    @testing.for_all_dtypes()
    @testing.numpy_cupy_allclose(accept_error=TypeError)
    def check_zerodim_op(self, op, xp, dtype):
        a = xp.array(-2).astype(dtype)
        try:
            return op(a)
        except ValueError:
            # When op is operator.invert and dtype is inexact,
            # NumPy raises TypeError while DPNP raises ValueError.
            # With this logic, when ValueError is raised in DPNP,
            # it is changed to TypeError to align with NumPy.
            raise TypeError

    def test_invert_zerodim(self):
        self.check_zerodim_op(operator.invert)


@testing.parameterize(
    *testing.product({"xp": [numpy, cupy], "shape": [(3, 2), (), (3, 0, 2)]})
)
class TestBoolNeg(unittest.TestCase):
    def test_bool_neg(self):
        xp = self.xp
        if xp is numpy and not testing.numpy_satisfies(">=1.13.0"):
            raise unittest.SkipTest("NumPy<1.13.0")
        shape = self.shape
        x = testing.shaped_random(shape, xp, dtype=numpy.bool_)
        with pytest.raises(TypeError):
            -x
