import unittest
from unittest import mock

import numpy
import pytest

import cupy
from cupy import cuda
from cupy.cuda import runtime
from cupy import random
from cupy import testing
from cupy.testing import _condition
from cupy.testing import _hypothesis


class TestRandint(unittest.TestCase):

    def test_lo_hi_reversed(self):
        with self.assertRaises(ValueError):
            random.randint(100, 1)

    def test_lo_hi_equal(self):
        with self.assertRaises(ValueError):
            random.randint(3, 3, size=0)

        with self.assertRaises(ValueError):
            # int(-0.2) is not less than int(0.3)
            random.randint(-0.2, 0.3)

    def test_lo_hi_nonrandom(self):
        a = random.randint(-0.9, 1.1, size=3)
        testing.assert_array_equal(a, cupy.full((3,), 0))

        a = random.randint(-1.1, -0.9, size=(2, 2))
        testing.assert_array_equal(a, cupy.full((2, 2), -1))

    def test_zero_sizes(self):
        a = random.randint(10, size=(0,))
        testing.assert_array_equal(a, cupy.array(()))

        a = random.randint(10, size=0)
        testing.assert_array_equal(a, cupy.array(()))


@testing.fix_random()
class TestRandint2(unittest.TestCase):

    @_condition.repeat(3, 10)
    def test_bound_1(self):
        vals = [random.randint(0, 10, (2, 3)).get() for _ in range(20)]
        for val in vals:
            assert val.shape == (2, 3)
        assert min(_.min() for _ in vals) == 0
        assert max(_.max() for _ in vals) == 9

    @_condition.repeat(3, 10)
    def test_bound_2(self):
        vals = [random.randint(0, 2).get() for _ in range(20)]
        for val in vals:
            assert val.shape == ()
        assert min(vals) == 0
        assert max(vals) == 1

    @_condition.repeat(3, 10)
    def test_bound_overflow(self):
        # 100 - (-100) exceeds the range of int8
        val = random.randint(numpy.int8(-100), numpy.int8(100), size=20).get()
        assert val.shape == (20,)
        assert val.min() >= -100
        assert val.max() < 100

    @_condition.repeat(3, 10)
    def test_bound_float1(self):
        # generate floats s.t. int(low) < int(high)
        low, high = sorted(numpy.random.uniform(-5, 5, size=2))
        low -= 1
        high += 1
        vals = [random.randint(low, high, (2, 3)).get() for _ in range(10)]
        for val in vals:
            assert val.shape == (2, 3)
        assert min(_.min() for _ in vals) == int(low)
        assert max(_.max() for _ in vals) == int(high) - 1

    def test_bound_float2(self):
        vals = [random.randint(-1.0, 1.0, (2, 3)).get() for _ in range(10)]
        for val in vals:
            assert val.shape == (2, 3)
        assert min(_.min() for _ in vals) == -1
        assert max(_.max() for _ in vals) == 0

    @_condition.repeat(3, 10)
    def test_goodness_of_fit(self):
        mx = 5
        trial = 100
        vals = [random.randint(mx).get() for _ in range(trial)]
        counts = numpy.histogram(vals, bins=numpy.arange(mx + 1))[0]
        expected = numpy.array([float(trial) / mx] * mx)
        assert _hypothesis.chi_square_test(counts, expected)

    @_condition.repeat(3, 10)
    @pytest.mark.xfail(runtime.is_hip, reason='ROCm/HIP may have a bug')
    def test_goodness_of_fit_2(self):
        mx = 5
        vals = random.randint(mx, size=(5, 20)).get()
        counts = numpy.histogram(vals, bins=numpy.arange(mx + 1))[0]
        expected = numpy.array([float(vals.size) / mx] * mx)
        assert _hypothesis.chi_square_test(counts, expected)


class TestRandintDtype(unittest.TestCase):

    @testing.with_requires("numpy<2.0")
    @testing.for_dtypes([
        numpy.int8, numpy.uint8, numpy.int16, numpy.uint16, numpy.int32])
    def test_dtype(self, dtype):
        size = (1000,)
        low = numpy.iinfo(dtype).min
        high = numpy.iinfo(dtype).max + 1
        x = random.randint(low, high, size, dtype).get()
        assert low <= min(x)
        assert max(x) <= high

    @testing.for_int_dtypes(no_bool=True)
    def test_dtype2(self, dtype):
        dtype = numpy.dtype(dtype)

        # randint does not support 64 bit integers
        if dtype in (numpy.int64, numpy.uint64):
            return

        iinfo = numpy.iinfo(dtype)
        size = (10000,)

        x = random.randint(iinfo.min, iinfo.max + 1, size, dtype).get()
        assert x.dtype == dtype
        assert iinfo.min <= min(x)
        assert max(x) <= iinfo.max

        # Lower bound check
        with self.assertRaises(ValueError):
            random.randint(iinfo.min - 1, iinfo.min + 10, size, dtype)

        # Upper bound check
        with self.assertRaises(ValueError):
            random.randint(iinfo.max - 10, iinfo.max + 2, size, dtype)


class TestRandomIntegers(unittest.TestCase):

    def test_normal(self):
        with mock.patch('cupy.random._sample.randint') as m:
            random.random_integers(3, 5)
        m.assert_called_with(3, 6, None)

    def test_high_is_none(self):
        with mock.patch('cupy.random._sample.randint') as m:
            random.random_integers(3, None)
        m.assert_called_with(1, 4, None)

    def test_size_is_not_none(self):
        with mock.patch('cupy.random._sample.randint') as m:
            random.random_integers(3, 5, (1, 2, 3))
        m.assert_called_with(3, 6, (1, 2, 3))


@testing.fix_random()
class TestRandomIntegers2(unittest.TestCase):

    @_condition.repeat(3, 10)
    def test_bound_1(self):
        vals = [random.random_integers(0, 10, (2, 3)).get() for _ in range(10)]
        for val in vals:
            assert val.shape == (2, 3)
        assert min(_.min() for _ in vals) == 0
        assert max(_.max() for _ in vals) == 10

    @_condition.repeat(3, 10)
    def test_bound_2(self):
        vals = [random.random_integers(0, 2).get() for _ in range(20)]
        for val in vals:
            assert val.shape == ()
        assert min(vals) == 0
        assert max(vals) == 2

    @_condition.repeat(3, 10)
    def test_goodness_of_fit(self):
        mx = 5
        trial = 100
        vals = [random.randint(0, mx).get() for _ in range(trial)]
        counts = numpy.histogram(vals, bins=numpy.arange(mx + 1))[0]
        expected = numpy.array([float(trial) / mx] * mx)
        assert _hypothesis.chi_square_test(counts, expected)

    @_condition.repeat(3, 10)
    @pytest.mark.xfail(runtime.is_hip, reason='ROCm/HIP may have a bug')
    def test_goodness_of_fit_2(self):
        mx = 5
        vals = random.randint(0, mx, (5, 20)).get()
        counts = numpy.histogram(vals, bins=numpy.arange(mx + 1))[0]
        expected = numpy.array([float(vals.size) / mx] * mx)
        assert _hypothesis.chi_square_test(counts, expected)


class TestChoice(unittest.TestCase):

    def setUp(self):
        self.rs_tmp = random._generator._random_states
        device_id = cuda.Device().id
        self.m = mock.Mock()
        self.m.choice.return_value = 0
        random._generator._random_states = {device_id: self.m}

    def tearDown(self):
        random._generator._random_states = self.rs_tmp

    def test_size_and_replace_and_p_are_none(self):
        random.choice(3)
        self.m.choice.assert_called_with(3, None, True, None)

    def test_size_and_replace_are_none(self):
        random.choice(3, None, None, [0.1, 0.1, 0.8])
        self.m.choice.assert_called_with(3, None, None, [0.1, 0.1, 0.8])

    def test_size_and_p_are_none(self):
        random.choice(3, None, True)
        self.m.choice.assert_called_with(3, None, True, None)

    def test_replace_and_p_are_none(self):
        random.choice(3, 1)
        self.m.choice.assert_called_with(3, 1, True, None)

    def test_size_is_none(self):
        random.choice(3, None, True, [0.1, 0.1, 0.8])
        self.m.choice.assert_called_with(3, None, True, [0.1, 0.1, 0.8])

    def test_replace_is_none(self):
        random.choice(3, 1, None, [0.1, 0.1, 0.8])
        self.m.choice.assert_called_with(3, 1, None, [0.1, 0.1, 0.8])

    def test_p_is_none(self):
        random.choice(3, 1, True)
        self.m.choice.assert_called_with(3, 1, True, None)

    def test_no_none(self):
        random.choice(3, 1, True, [0.1, 0.1, 0.8])
        self.m.choice.assert_called_with(3, 1, True, [0.1, 0.1, 0.8])


class TestRandomSample(unittest.TestCase):

    def test_rand(self):
        with mock.patch('cupy.random._sample.random_sample') as m:
            random.rand(1, 2, 3, dtype=numpy.float32)
        m.assert_called_once_with(
            size=(1, 2, 3), dtype=numpy.float32)

    def test_rand_default_dtype(self):
        with mock.patch('cupy.random._sample.random_sample') as m:
            random.rand(1, 2, 3)
        m.assert_called_once_with(
            size=(1, 2, 3), dtype=float)

    def test_rand_invalid_argument(self):
        with self.assertRaises(TypeError):
            random.rand(1, 2, 3, unnecessary='unnecessary_argument')

    def test_randn(self):
        with mock.patch('cupy.random._distributions.normal') as m:
            random.randn(1, 2, 3, dtype=numpy.float32)
        m.assert_called_once_with(
            size=(1, 2, 3), dtype=numpy.float32)

    def test_randn_default_dtype(self):
        with mock.patch('cupy.random._distributions.normal') as m:
            random.randn(1, 2, 3)
        m.assert_called_once_with(
            size=(1, 2, 3), dtype=float)

    def test_randn_invalid_argument(self):
        with self.assertRaises(TypeError):
            random.randn(1, 2, 3, unnecessary='unnecessary_argument')


@testing.parameterize(
    {'size': None},
    {'size': ()},
    {'size': 4},
    {'size': (0,)},
    {'size': (1, 0)},
)
@testing.fix_random()
class TestMultinomial(unittest.TestCase):

    @_condition.repeat(3, 10)
    @testing.for_float_dtypes()
    @testing.numpy_cupy_allclose(rtol=0.05)
    def test_multinomial(self, xp, dtype):
        pvals = xp.array([0.2, 0.3, 0.5], dtype)
        x = xp.random.multinomial(100000, pvals, self.size)
        assert x.dtype == 'l'
        return x / 100000
