import re
import unittest

import numpy
import pytest

import cupy
from cupy import testing
from cupy.testing import _loops


class _Exception1(Exception):
    pass


class _Exception2(Exception):
    pass


class TestContainsSignedAndUnsigned(unittest.TestCase):

    def test_include(self):
        kw = {'x': numpy.int32, 'y': numpy.uint32}
        assert _loops._contains_signed_and_unsigned(kw)

        kw = {'x': numpy.float32, 'y': numpy.uint32}
        assert _loops._contains_signed_and_unsigned(kw)

    def test_signed_only(self):
        kw = {'x': numpy.int32}
        assert not _loops._contains_signed_and_unsigned(kw)

        kw = {'x': numpy.float32}
        assert not _loops._contains_signed_and_unsigned(kw)

    def test_unsigned_only(self):
        kw = {'x': numpy.uint32}
        assert not _loops._contains_signed_and_unsigned(kw)

    def test_ignore_not_dtype(self):
        kw = {'x': numpy.int32, 'y': numpy.uint32, 'a': [0, 1]}
        assert _loops._contains_signed_and_unsigned(kw)


class TestCheckCupyNumpyError(unittest.TestCase):

    tbs = {
        cupy: 'xxxx',
        numpy: 'yyyy'
    }

    def test_both_success(self):
        with testing.assert_warns(DeprecationWarning):
            @testing.numpy_cupy_raises()
            def dummy_both_success(self, xp):
                pass

        with self.assertRaises(AssertionError):
            dummy_both_success(self)

    def test_cupy_error(self):
        with testing.assert_warns(DeprecationWarning):
            @testing.numpy_cupy_raises()
            def dummy_cupy_error(self, xp):
                if xp is cupy:
                    raise Exception(self.tbs.get(cupy))

        with self.assertRaisesRegex(AssertionError, self.tbs.get(cupy)):
            dummy_cupy_error(self)

    def test_numpy_error(self):
        with testing.assert_warns(DeprecationWarning):
            @testing.numpy_cupy_raises()
            def dummy_numpy_error(self, xp):
                if xp is numpy:
                    raise Exception(self.tbs.get(numpy))

        with self.assertRaisesRegex(AssertionError, self.tbs.get(numpy)):
            dummy_numpy_error(self)

    def test_cupy_numpy_different_error(self):
        with testing.assert_warns(DeprecationWarning):
            @testing.numpy_cupy_raises()
            def dummy_cupy_numpy_different_error(self, xp):
                if xp is cupy:
                    raise TypeError(self.tbs.get(cupy))
                elif xp is numpy:
                    raise ValueError(self.tbs.get(numpy))

        # Use re.S mode to ignore new line characters
        pattern = re.compile(
            self.tbs.get(cupy) + '.*' + self.tbs.get(numpy), re.S)
        with self.assertRaisesRegex(AssertionError, pattern):
            dummy_cupy_numpy_different_error(self)

    def test_cupy_derived_error(self):
        with testing.assert_warns(DeprecationWarning):
            @testing.numpy_cupy_raises()
            def dummy_cupy_derived_error(self, xp):
                if xp is cupy:
                    raise _Exception1(self.tbs.get(cupy))
                elif xp is numpy:
                    raise _Exception2(self.tbs.get(numpy))

        dummy_cupy_derived_error(self)  # Assert no exceptions

    def test_numpy_derived_error(self):
        with testing.assert_warns(DeprecationWarning):
            @testing.numpy_cupy_raises()
            def dummy_numpy_derived_error(self, xp):
                if xp is cupy:
                    raise Exception(self.tbs.get(cupy))
                elif xp is numpy:
                    raise IndexError(self.tbs.get(numpy))

        # NumPy errors may not derive from CuPy errors, i.e. CuPy errors should
        # be at least as explicit as the NumPy error
        pattern = re.compile(
            self.tbs.get(cupy) + '.*' + self.tbs.get(numpy), re.S)
        with self.assertRaisesRegex(AssertionError, pattern):
            dummy_numpy_derived_error(self)

    def test_same_error(self):
        with testing.assert_warns(DeprecationWarning):
            @testing.numpy_cupy_raises(accept_error=Exception)
            def dummy_same_error(self, xp):
                raise Exception(self.tbs.get(xp))

        dummy_same_error(self)

    def test_cupy_derived_unaccept_error(self):
        with testing.assert_warns(DeprecationWarning):
            @testing.numpy_cupy_raises(accept_error=ValueError)
            def dummy_cupy_derived_unaccept_error(self, xp):
                if xp is cupy:
                    raise IndexError(self.tbs.get(cupy))
                elif xp is numpy:
                    raise Exception(self.tbs.get(numpy))

        # Neither `IndexError` nor `Exception` is derived from `ValueError`,
        # therefore expect an error
        pattern = re.compile(
            self.tbs.get(cupy) + '.*' + self.tbs.get(numpy), re.S)
        with self.assertRaisesRegex(AssertionError, pattern):
            dummy_cupy_derived_unaccept_error(self)

    def test_numpy_derived_unaccept_error(self):
        with testing.assert_warns(DeprecationWarning):
            @testing.numpy_cupy_raises(accept_error=ValueError)
            def dummy_numpy_derived_unaccept_error(self, xp):
                if xp is cupy:
                    raise Exception(self.tbs.get(cupy))
                elif xp is numpy:
                    raise ValueError(self.tbs.get(numpy))

        # `Exception` is not derived from `ValueError`, therefore expect an
        # error
        pattern = re.compile(
            self.tbs.get(cupy) + '.*' + self.tbs.get(numpy), re.S)
        with self.assertRaisesRegex(AssertionError, pattern):
            dummy_numpy_derived_unaccept_error(self)

    def test_forbidden_error(self):
        with testing.assert_warns(DeprecationWarning):
            @testing.numpy_cupy_raises(accept_error=False)
            def dummy_forbidden_error(self, xp):
                raise Exception(self.tbs.get(xp))

        pattern = re.compile(
            self.tbs.get(cupy) + '.*' + self.tbs.get(numpy), re.S)
        with self.assertRaisesRegex(AssertionError, pattern):
            dummy_forbidden_error(self)

    def test_axis_error_different_type(self):
        with testing.assert_warns(DeprecationWarning):
            @testing.numpy_cupy_raises()
            def dummy_axis_error(self, xp):
                if xp is cupy:
                    raise numpy.AxisError(self.tbs.get(cupy))
                elif xp is numpy:
                    raise TypeError(self.tbs.get(numpy))

        pattern = re.compile(
            self.tbs.get(cupy) + '.*' + self.tbs.get(numpy), re.S)
        with self.assertRaisesRegex(AssertionError, pattern):
            dummy_axis_error(self)

    def test_axis_error_value_different_type(self):
        with testing.assert_warns(DeprecationWarning):
            @testing.numpy_cupy_raises()
            def dummy_axis_error(self, xp):
                if xp is cupy:
                    raise numpy.AxisError(self.tbs.get(cupy))
                elif xp is numpy:
                    raise ValueError(self.tbs.get(numpy))

        pattern = re.compile(
            self.tbs.get(cupy) + '.*' + self.tbs.get(numpy), re.S)
        with self.assertRaisesRegex(AssertionError, pattern):
            dummy_axis_error(self)

    def test_axis_error_index_different_type(self):
        with testing.assert_warns(DeprecationWarning):
            @testing.numpy_cupy_raises()
            def dummy_axis_error(self, xp):
                if xp is cupy:
                    raise numpy.AxisError(self.tbs.get(cupy))
                elif xp is numpy:
                    raise IndexError(self.tbs.get(numpy))

        pattern = re.compile(
            self.tbs.get(cupy) + '.*' + self.tbs.get(numpy), re.S)
        with self.assertRaisesRegex(AssertionError, pattern):
            dummy_axis_error(self)


class NumPyCuPyDecoratorBase(object):

    def test_valid(self):
        decorator = getattr(testing, self.decorator)()
        decorated_func = decorator(type(self).valid_func)
        decorated_func(self)

    def test_invalid(self):
        decorator = getattr(testing, self.decorator)()
        decorated_func = decorator(type(self).invalid_func)
        with self.assertRaises(AssertionError):
            decorated_func(self)

    def test_name(self):
        decorator = getattr(testing, self.decorator)(name='foo')
        decorated_func = decorator(type(self).strange_kw_func)
        decorated_func(self)


def numpy_error(_, xp):
    if xp == numpy:
        raise ValueError()
    elif xp == cupy:
        return cupy.array(1)


def cupy_error(_, xp):
    if xp == numpy:
        return numpy.array(1)
    elif xp == cupy:
        raise ValueError()


class NumPyCuPyDecoratorBase2(object):

    def test_accept_error_numpy(self):
        decorator = getattr(testing, self.decorator)(accept_error=False)
        decorated_func = decorator(numpy_error)
        with self.assertRaises(AssertionError):
            decorated_func(self)

    def test_accept_error_cupy(self):
        decorator = getattr(testing, self.decorator)(accept_error=False)
        decorated_func = decorator(cupy_error)
        with self.assertRaises(AssertionError):
            decorated_func(self)


def make_result(xp, np_result, cp_result):
    if xp == numpy:
        return np_result
    elif xp == cupy:
        return cp_result


@testing.parameterize(
    {'decorator': 'numpy_cupy_allclose'},
    {'decorator': 'numpy_cupy_array_almost_equal'},
    {'decorator': 'numpy_cupy_array_almost_equal_nulp'},
    {'decorator': 'numpy_cupy_array_max_ulp'},
    {'decorator': 'numpy_cupy_array_equal'}
)
class TestNumPyCuPyEqual(unittest.TestCase, NumPyCuPyDecoratorBase,
                         NumPyCuPyDecoratorBase2):

    def valid_func(self, xp):
        return make_result(xp, numpy.array(1), cupy.array(1))

    def invalid_func(self, xp):
        return make_result(xp, numpy.array(1), cupy.array(2))

    def strange_kw_func(self, foo):
        return make_result(foo, numpy.array(1), cupy.array(1))


@testing.parameterize(
    {'decorator': 'numpy_cupy_array_equal'}
)
class TestNumPyCuPyListEqual(unittest.TestCase, NumPyCuPyDecoratorBase):

    def valid_func(self, xp):
        return make_result(xp, [numpy.array(1)], [cupy.array(1)])

    def invalid_func(self, xp):
        return make_result(xp, [numpy.array(1)], [cupy.array(2)])

    def strange_kw_func(self, foo):
        return make_result(foo, [numpy.array(1)], [cupy.array(1)])


@testing.parameterize(
    {'decorator': 'numpy_cupy_array_less'}
)
class TestNumPyCuPyLess(unittest.TestCase, NumPyCuPyDecoratorBase,
                        NumPyCuPyDecoratorBase2):

    def valid_func(self, xp):
        return make_result(xp, numpy.array(2), cupy.array(1))

    def invalid_func(self, xp):
        return make_result(xp, numpy.array(1), cupy.array(2))

    def strange_kw_func(self, foo):
        return make_result(foo, numpy.array(2), cupy.array(1))


class TestNumPyCuPyAllCloseTolPerDtype(unittest.TestCase):

    def _test_rtol(self, xp, dtype):
        if xp is numpy:
            return numpy.array(1, dtype=dtype)
        else:
            finfo = numpy.finfo(dtype)
            return cupy.array(1 + finfo.eps, dtype=dtype)

    @testing.for_float_dtypes()
    @testing.numpy_cupy_allclose(rtol={numpy.float16: 1e-3, 'default': 1e-6})
    def test_rtol_per_dtype(self, xp, dtype):
        return self._test_rtol(xp, dtype)

    @pytest.mark.xfail(strict=True)
    @testing.for_float_dtypes()
    @testing.numpy_cupy_allclose(rtol=1e-6)
    def test_rtol_fail(self, xp, dtype):
        return self._test_rtol(xp, dtype)

    def test_rtol_invalid_key(self):
        with self.assertRaises(TypeError):
            testing.numpy_cupy_allclose(rtol={'float16': 1e-3})

    def _test_atol(self, xp, dtype):
        if xp is numpy:
            return numpy.array(0, dtype=dtype)
        else:
            finfo = numpy.finfo(dtype)
            return cupy.array(finfo.eps, dtype=dtype)

    @testing.for_float_dtypes()
    @testing.numpy_cupy_allclose(atol={numpy.float16: 1e-3, 'default': 1e-6})
    def test_atol_per_dtype(self, xp, dtype):
        return self._test_atol(xp, dtype)

    @pytest.mark.xfail(strict=True)
    @testing.for_float_dtypes()
    @testing.numpy_cupy_allclose(atol=1e-6)
    def test_atol_fail(self, xp, dtype):
        return self._test_atol(xp, dtype)

    def test_atol_invalid_key(self):
        with self.assertRaises(TypeError):
            testing.numpy_cupy_allclose(atol={'float16': 1e-3})


class TestIgnoreOfNegativeValueDifferenceOnCpuAndGpu(unittest.TestCase):

    @testing.numpy_cupy_allclose()
    def correct_failure(self, dtype1, dtype2, xp):
        if xp == numpy:
            return xp.array(-1, dtype=numpy.float32)
        else:
            return xp.array(-2, dtype=numpy.float32)

    @testing.for_unsigned_dtypes('dtype1')
    @testing.for_signed_dtypes('dtype2')
    def test_correct_failure(self, dtype1, dtype2):
        with pytest.raises(AssertionError):
            self.correct_failure(dtype1, dtype2)

    @testing.for_unsigned_dtypes('dtype1')
    @testing.for_signed_dtypes('dtype2')
    @testing.numpy_cupy_allclose()
    def test_correct_success(self, xp, dtype1, dtype2):
        # Behavior of assigning a negative value to an unsigned integer
        # variable is undefined.
        # nVidia GPUs and Intel CPUs behave differently.
        # To avoid this difference, we need to ignore dimensions whose
        # values are negative.
        if xp == numpy:
            return xp.array(-1).astype(dtype1)
        else:
            return xp.array(-2).astype(dtype1)


@testing.parameterize(*testing.product({
    'framework': ['unittest', 'pytest']
}))
class TestSkip(unittest.TestCase):

    def _skip(self, reason):
        if self.framework == 'unittest':
            self.skipTest(reason)
        else:
            pytest.skip(reason)

    @testing.numpy_cupy_allclose()
    def test_allclose(self, xp):
        self._skip('Test for skip with @numpy_cupy_allclose')
        assert False

    @testing.numpy_cupy_array_almost_equal()
    def test_array_almost_equal(self, xp):
        raise self._skip('Test for skip with @numpy_cupy_array_almost_equal')
        assert False

    @testing.numpy_cupy_array_almost_equal_nulp()
    def test_array_almost_equal_nulp(self, xp):
        raise self._skip(
            'Test for skip with @numpy_cupy_array_almost_equal_nulp')
        assert False

    @testing.numpy_cupy_array_max_ulp()
    def test_array_max_ulp(self, xp):
        raise self._skip('Test for skip with @numpy_cupy_array_max_ulp')
        assert False

    @testing.numpy_cupy_array_equal()
    def test_array_equal(self, xp):
        raise self._skip('Test for skip with @numpy_cupy_array_equal')
        assert False

    @testing.numpy_cupy_array_less()
    def test_less(self, xp):
        raise self._skip('Test for skip with @numpy_cupy_array_less')
        assert False

    @testing.numpy_cupy_equal()
    def test_equal(self, xp):
        raise self._skip('Test for skip with @numpy_cupy_equal')
        assert False

    @testing.for_all_dtypes()
    def test_dtypes(self, dtype):
        if dtype is cupy.float32:
            raise self._skip('Test for skipping a dtype in @for_all_dtypes')
            assert False
        else:
            assert True

    @testing.for_all_dtypes()
    @testing.numpy_cupy_allclose()
    def test_dtypes_allclose(self, xp, dtype):
        if dtype is xp.float32:
            raise self._skip('Test for skipping a dtype in @for_all_dtypes')
            assert False
        else:
            return xp.array(True)


@testing.parameterize(*testing.product({
    'framework': ['unittest', 'pytest']
}))
class TestSkipFail(unittest.TestCase):

    def _skip(self, reason):
        if self.framework == 'unittest':
            raise unittest.SkipTest(reason)
        else:
            pytest.skip(reason)

    @pytest.mark.xfail(strict=True)
    @testing.numpy_cupy_allclose()
    def test_different_reason(self, xp):
        if xp is numpy:
            self._skip('skip1')
        else:
            self._skip('skip2')

    @pytest.mark.xfail(strict=True)
    @testing.numpy_cupy_allclose()
    def test_only_numpy(self, xp):
        if xp is numpy:
            self._skip('skip')
        else:
            return xp.array(True)

    @pytest.mark.xfail(strict=True)
    @testing.numpy_cupy_allclose()
    def test_only_cupy(self, xp):
        if xp is numpy:
            return xp.array(True)
        else:
            self._skip('skip')

    @pytest.mark.xfail(strict=True)
    @testing.for_all_dtypes()
    @testing.numpy_cupy_allclose()
    def test_dtype_only_cupy(self, xp, dtype):
        if dtype is not xp.float32:
            return xp.array(True)

        if xp is numpy:
            return xp.array(True)
        else:
            self._skip('skip')
