import numpy
import pytest

import cupy
from cupy.random import _distributions
from cupy import testing


_regular_float_dtypes = (numpy.float64, numpy.float32)
_float_dtypes = _regular_float_dtypes + (numpy.float16,)
_signed_dtypes = tuple(numpy.dtype(i).type for i in 'bhilq')
_unsigned_dtypes = tuple(numpy.dtype(i).type for i in 'BHILQ')
_int_dtypes = _signed_dtypes + _unsigned_dtypes


class RandomDistributionsTestCase:
    def check_distribution(self, dist_name, params, dtype):
        cp_params = {k: cupy.asarray(params[k]) for k in params}
        np_out = numpy.asarray(
            getattr(numpy.random, dist_name)(size=self.shape, **params),
            dtype)
        cp_out = getattr(_distributions, dist_name)(
            size=self.shape, dtype=dtype, **cp_params)
        assert cp_out.shape == np_out.shape
        assert cp_out.dtype == np_out.dtype

    def check_generator_distribution(self, dist_name, params, dtype):
        cp_params = {k: cupy.asarray(params[k]) for k in params}
        np_gen = numpy.random.default_rng(0)
        cp_gen = cupy.random.default_rng(0)
        np_out = numpy.asarray(
            getattr(np_gen, dist_name)(size=self.shape, **params))
        cp_out = getattr(cp_gen, dist_name)(
            size=self.shape, **cp_params)
        assert cp_out.shape == np_out.shape
        assert cp_out.dtype == np_out.dtype


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'a_shape': [(), (3, 2)],
    'b_shape': [(), (3, 2)],
    'dtype': _float_dtypes,  # to escape timeout
})
)
class TestDistributionsBeta(RandomDistributionsTestCase):

    @cupy.testing.for_dtypes_combination(
        _float_dtypes, names=['a_dtype', 'b_dtype'])
    def test_beta(self, a_dtype, b_dtype):
        a = numpy.full(self.a_shape, 3, dtype=a_dtype)
        b = numpy.full(self.b_shape, 3, dtype=b_dtype)
        self.check_distribution('beta',
                                {'a': a, 'b': b}, self.dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'n_shape': [(), (3, 2)],
    'p_shape': [(), (3, 2)],
    'dtype': _int_dtypes,  # to escape timeout
})
)
class TestDistributionsBinomial(RandomDistributionsTestCase):

    @cupy.testing.for_signed_dtypes('n_dtype')
    @cupy.testing.for_float_dtypes('p_dtype')
    def test_binomial(self, n_dtype, p_dtype):
        if numpy.dtype('l') == numpy.int32 and n_dtype == numpy.int64:
            pytest.skip('n must be able to cast to long')
        n = numpy.full(self.n_shape, 5, dtype=n_dtype)
        p = numpy.full(self.p_shape, 0.5, dtype=p_dtype)
        self.check_distribution('binomial',
                                {'n': n, 'p': p}, self.dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'df_shape': [(), (3, 2)],
})
)
class TestDistributionsChisquare:

    def check_distribution(self, dist_func, df_dtype, dtype):
        df = cupy.full(self.df_shape, 5, dtype=df_dtype)
        out = dist_func(df, self.shape, dtype)
        assert self.shape == out.shape
        assert out.dtype == dtype

    @cupy.testing.for_float_dtypes('df_dtype')
    @cupy.testing.for_float_dtypes('dtype')
    def test_chisquare(self, df_dtype, dtype):
        self.check_distribution(_distributions.chisquare, df_dtype, dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2, 3), (3, 2, 3)],
    'alpha_shape': [(3,)],
})
)
class TestDistributionsDirichlet(RandomDistributionsTestCase):

    @cupy.testing.for_dtypes_combination(
        _float_dtypes, names=['alpha_dtype', 'dtype'])
    def test_dirichlet(self, alpha_dtype, dtype):
        alpha = numpy.ones(self.alpha_shape, dtype=alpha_dtype)
        self.check_distribution('dirichlet',
                                {'alpha': alpha}, dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2), None],
    'scale_shape': [(), (3, 2)],
})
)
class TestDistributionsExponential(RandomDistributionsTestCase):

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_float_dtypes('scale_dtype')
    def test_exponential(self, scale_dtype, dtype):
        scale = numpy.ones(self.scale_shape, dtype=scale_dtype)
        self.check_distribution('exponential',
                                {'scale': scale}, dtype)


class TestDistributionsExponentialError(RandomDistributionsTestCase):

    def test_negative_scale(self):
        scale = cupy.array([2, -1, 3], dtype=numpy.float32)
        with pytest.raises(ValueError):
            cupy.random.exponential(scale)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'dfnum_shape': [(), (3, 2)],
    'dfden_shape': [(), (3, 2)],
    'dtype': _float_dtypes,  # to escape timeout
})
)
class TestDistributionsF:

    def check_distribution(self, dist_func, dfnum_dtype, dfden_dtype, dtype):
        dfnum = cupy.ones(self.dfnum_shape, dtype=dfnum_dtype)
        dfden = cupy.ones(self.dfden_shape, dtype=dfden_dtype)
        out = dist_func(dfnum, dfden, self.shape, dtype)
        assert self.shape == out.shape
        assert out.dtype == dtype

    @cupy.testing.for_float_dtypes('dfnum_dtype')
    @cupy.testing.for_float_dtypes('dfden_dtype')
    def test_f(self, dfnum_dtype, dfden_dtype):
        self.check_distribution(_distributions.f,
                                dfnum_dtype, dfden_dtype, self.dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2), None],
    'shape_shape': [(), (3, 2)],
    'scale_shape': [(), (3, 2)],
    'dtype': _float_dtypes,  # to escape timeout
})
)
class TestDistributionsGamma:

    def check_distribution(self, dist_func, shape_dtype, scale_dtype,
                           dtype=None):
        shape = cupy.ones(self.shape_shape, dtype=shape_dtype)
        scale = cupy.ones(self.scale_shape, dtype=scale_dtype)
        if dtype is None:
            out = dist_func(shape, scale, self.shape)
        else:
            out = dist_func(shape, scale, self.shape, dtype)
        out_shape = self.shape
        if self.shape is None:
            out_shape = shape.shape if shape.shape != () else scale.shape
        assert out_shape == out.shape
        assert out.dtype == dtype

    @cupy.testing.for_dtypes_combination(
        _float_dtypes, names=['shape_dtype', 'scale_dtype'])
    def test_gamma_legacy(self, shape_dtype, scale_dtype):
        self.check_distribution(_distributions.gamma,
                                shape_dtype, scale_dtype, self.dtype)

    @cupy.testing.for_dtypes_combination(
        _float_dtypes, names=['shape_dtype', 'scale_dtype'])
    @pytest.mark.skipif(
        cupy.cuda.runtime.is_hip
        and (int(str(cupy.cuda.runtime.runtimeGetVersion())[:3]) < 403),
        reason="HIP<4.3 not supported ",
    )
    def test_gamma_generator(self, shape_dtype, scale_dtype):
        self.check_distribution(cupy.random.default_rng().gamma,
                                shape_dtype, scale_dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'p_shape': [(), (3, 2)],
    'dtype': _int_dtypes,  # to escape timeout
})
)
class TestDistributionsGeometric:

    def check_distribution(self, dist_func, p_dtype, dtype):
        p = 0.5 * cupy.ones(self.p_shape, dtype=p_dtype)
        out = dist_func(p, self.shape, dtype)
        assert self.shape == out.shape
        assert out.dtype == dtype

    @cupy.testing.for_float_dtypes('p_dtype')
    def test_geometric(self, p_dtype):
        self.check_distribution(_distributions.geometric,
                                p_dtype, self.dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2), None],
    'loc_shape': [(), (3, 2)],
    'scale_shape': [(), (3, 2)],
})
)
class TestDistributionsGumbel(RandomDistributionsTestCase):

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_dtypes_combination(
        _float_dtypes, names=['loc_dtype', 'scale_dtype'])
    def test_gumbel(self, loc_dtype, scale_dtype, dtype):
        loc = numpy.ones(self.loc_shape, dtype=loc_dtype)
        scale = numpy.ones(self.scale_shape, dtype=scale_dtype)
        self.check_distribution('gumbel',
                                {'loc': loc, 'scale': scale}, dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'ngood_shape': [(), (3, 2)],
    'nbad_shape': [(), (3, 2)],
    'nsample_shape': [(), (3, 2)],
    'nsample_dtype': [numpy.int32, numpy.int64],  # to escape timeout
    'dtype': [numpy.int32, numpy.int64],  # to escape timeout
})
)
class TestDistributionsHyperGeometric:

    def check_distribution(self, dist_func, ngood_dtype, nbad_dtype,
                           nsample_dtype, dtype):
        ngood = cupy.ones(self.ngood_shape, dtype=ngood_dtype)
        nbad = cupy.ones(self.nbad_shape, dtype=nbad_dtype)
        nsample = cupy.ones(self.nsample_shape, dtype=nsample_dtype)
        out = dist_func(ngood, nbad, nsample, self.shape, dtype)
        assert self.shape == out.shape
        assert out.dtype == dtype

    @cupy.testing.for_dtypes_combination(
        [numpy.int32, numpy.int64], names=['ngood_dtype', 'nbad_dtype'])
    def test_hypergeometric(self, ngood_dtype, nbad_dtype):
        self.check_distribution(_distributions.hypergeometric, ngood_dtype,
                                nbad_dtype, self.nsample_dtype, self.dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2), None],
    'loc_shape': [(), (3, 2)],
    'scale_shape': [(), (3, 2)],
})
)
class TestDistributionsuLaplace(RandomDistributionsTestCase):

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_dtypes_combination(
        _float_dtypes, names=['loc_dtype', 'scale_dtype'])
    def test_laplace(self, loc_dtype, scale_dtype, dtype):
        loc = numpy.ones(self.loc_shape, dtype=loc_dtype)
        scale = numpy.ones(self.scale_shape, dtype=scale_dtype)
        self.check_distribution('laplace',
                                {'loc': loc, 'scale': scale}, dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'loc_shape': [(), (3, 2)],
    'scale_shape': [(), (3, 2)],
})
)
class TestDistributionsLogistic(RandomDistributionsTestCase):

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_dtypes_combination(
        _float_dtypes, names=['loc_dtype', 'scale_dtype'])
    def test_logistic(self, loc_dtype, scale_dtype, dtype):
        loc = numpy.ones(self.loc_shape, dtype=loc_dtype)
        scale = numpy.ones(self.scale_shape, dtype=scale_dtype)
        self.check_distribution('logistic',
                                {'loc': loc, 'scale': scale}, dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2), None],
    'mean_shape': [(), (3, 2)],
    'sigma_shape': [(), (3, 2)],
})
)
class TestDistributionsLognormal(RandomDistributionsTestCase):

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_dtypes_combination(
        _float_dtypes, names=['mean_dtype', 'sigma_dtype'])
    def test_lognormal(self, mean_dtype, sigma_dtype, dtype):
        mean = numpy.ones(self.mean_shape, dtype=mean_dtype)
        sigma = numpy.ones(self.sigma_shape, dtype=sigma_dtype)
        self.check_distribution('lognormal',
                                {'mean': mean, 'sigma': sigma}, dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'p_shape': [()],
})
)
class TestDistributionsLogseries(RandomDistributionsTestCase):

    @cupy.testing.for_dtypes([numpy.int64, numpy.int32], 'dtype')
    @cupy.testing.for_float_dtypes('p_dtype', no_float16=True)
    def test_logseries(self, p_dtype, dtype):
        p = numpy.full(self.p_shape, 0.5, dtype=p_dtype)
        self.check_distribution('logseries',
                                {'p': p}, dtype)

    @cupy.testing.for_dtypes([numpy.int64, numpy.int32], 'dtype')
    @cupy.testing.for_float_dtypes('p_dtype', no_float16=True)
    def test_logseries_for_invalid_p(self, p_dtype, dtype):
        with pytest.raises(ValueError):
            cp_params = {'p': cupy.zeros(self.p_shape, dtype=p_dtype)}
            _distributions.logseries(size=self.shape, dtype=dtype, **cp_params)
        with pytest.raises(ValueError):
            cp_params = {'p': cupy.ones(self.p_shape, dtype=p_dtype)}
            _distributions.logseries(size=self.shape, dtype=dtype, **cp_params)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'd': [2, 4],
})
)
class TestDistributionsMultivariateNormal:

    def check_distribution(self, dist_func, mean_dtype, cov_dtype, dtype):
        mean = cupy.zeros(self.d, dtype=mean_dtype)
        cov = cupy.random.normal(size=(self.d, self.d), dtype=cov_dtype)
        cov = cov.T.dot(cov)
        out = dist_func(mean, cov, self.shape, dtype=dtype)
        assert self.shape+(self.d,) == out.shape
        assert out.dtype == dtype

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_float_dtypes('mean_dtype', no_float16=True)
    @cupy.testing.for_float_dtypes('cov_dtype', no_float16=True)
    def test_normal(self, mean_dtype, cov_dtype, dtype):
        self.check_distribution(_distributions.multivariate_normal,
                                mean_dtype, cov_dtype, dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'n_shape': [(), (3, 2)],
    'p_shape': [(), (3, 2)],
    'dtype': _int_dtypes,  # to escape timeout
})
)
class TestDistributionsNegativeBinomial(RandomDistributionsTestCase):

    @cupy.testing.for_float_dtypes('n_dtype')
    @cupy.testing.for_float_dtypes('p_dtype')
    def test_negative_binomial(self, n_dtype, p_dtype):
        n = numpy.full(self.n_shape, 5, dtype=n_dtype)
        p = numpy.full(self.p_shape, 0.5, dtype=p_dtype)
        self.check_distribution('negative_binomial',
                                {'n': n, 'p': p}, self.dtype)

    @cupy.testing.for_float_dtypes('n_dtype')
    @cupy.testing.for_float_dtypes('p_dtype')
    def test_negative_binomial_for_noninteger_n(self, n_dtype, p_dtype):
        n = numpy.full(self.n_shape, 5.5, dtype=n_dtype)
        p = numpy.full(self.p_shape, 0.5, dtype=p_dtype)
        self.check_distribution('negative_binomial',
                                {'n': n, 'p': p}, self.dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'df_shape': [(), (3, 2)],
    'nonc_shape': [(), (3, 2)],
    'dtype': _int_dtypes,  # to escape timeout
})
)
class TestDistributionsNoncentralChisquare(RandomDistributionsTestCase):

    @cupy.testing.for_dtypes_combination(
        _regular_float_dtypes, names=['df_dtype', 'nonc_dtype'])
    def test_noncentral_chisquare(self, df_dtype, nonc_dtype):
        df = numpy.full(self.df_shape, 1, dtype=df_dtype)
        nonc = numpy.full(self.nonc_shape, 1, dtype=nonc_dtype)
        self.check_distribution('noncentral_chisquare',
                                {'df': df, 'nonc': nonc}, self.dtype)

    @cupy.testing.for_float_dtypes('param_dtype', no_float16=True)
    def test_noncentral_chisquare_for_invalid_params(self, param_dtype):
        df = cupy.full(self.df_shape, -1, dtype=param_dtype)
        nonc = cupy.full(self.nonc_shape, 1, dtype=param_dtype)
        with pytest.raises(ValueError):
            _distributions.noncentral_chisquare(
                df, nonc, size=self.shape, dtype=self.dtype)

        df = cupy.full(self.df_shape, 1, dtype=param_dtype)
        nonc = cupy.full(self.nonc_shape, -1, dtype=param_dtype)
        with pytest.raises(ValueError):
            _distributions.noncentral_chisquare(
                df, nonc, size=self.shape, dtype=self.dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'dfnum_shape': [(), (3, 2)],
    'dfden_shape': [(), (3, 2)],
    'nonc_shape': [(), (3, 2)],
    'dtype': _int_dtypes,  # to escape timeout
})
)
class TestDistributionsNoncentralF(RandomDistributionsTestCase):

    @cupy.testing.for_dtypes_combination(
        _regular_float_dtypes,
        names=['dfnum_dtype', 'dfden_dtype', 'nonc_dtype'])
    def test_noncentral_f(self, dfnum_dtype, dfden_dtype, nonc_dtype):
        dfnum = numpy.full(self.dfnum_shape, 1, dtype=dfnum_dtype)
        dfden = numpy.full(self.dfden_shape, 1, dtype=dfden_dtype)
        nonc = numpy.full(self.nonc_shape, 1, dtype=nonc_dtype)
        self.check_distribution('noncentral_f',
                                {'dfnum': dfnum, 'dfden': dfden, 'nonc': nonc},
                                self.dtype)

    @cupy.testing.for_float_dtypes('param_dtype', no_float16=True)
    def test_noncentral_f_for_invalid_params(self, param_dtype):
        dfnum = numpy.full(self.dfnum_shape, -1, dtype=param_dtype)
        dfden = numpy.full(self.dfden_shape, 1, dtype=param_dtype)
        nonc = numpy.full(self.nonc_shape, 1, dtype=param_dtype)
        with pytest.raises(ValueError):
            _distributions.noncentral_f(
                dfnum, dfden, nonc, size=self.shape, dtype=self.dtype)

        dfnum = numpy.full(self.dfnum_shape, 1, dtype=param_dtype)
        dfden = numpy.full(self.dfden_shape, -1, dtype=param_dtype)
        nonc = numpy.full(self.nonc_shape, 1, dtype=param_dtype)
        with pytest.raises(ValueError):
            _distributions.noncentral_f(
                dfnum, dfden, nonc, size=self.shape, dtype=self.dtype)

        dfnum = numpy.full(self.dfnum_shape, 1, dtype=param_dtype)
        dfden = numpy.full(self.dfden_shape, 1, dtype=param_dtype)
        nonc = numpy.full(self.nonc_shape, -1, dtype=param_dtype)
        with pytest.raises(ValueError):
            _distributions.noncentral_f(
                dfnum, dfden, nonc, size=self.shape, dtype=self.dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2), None],
    'loc_shape': [(), (3, 2)],
    'scale_shape': [(), (3, 2)],
})
)
class TestDistributionsNormal(RandomDistributionsTestCase):

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_dtypes_combination(
        _float_dtypes, names=['loc_dtype', 'scale_dtype'])
    def test_normal(self, loc_dtype, scale_dtype, dtype):
        loc = numpy.ones(self.loc_shape, dtype=loc_dtype)
        scale = numpy.ones(self.scale_shape, dtype=scale_dtype)
        self.check_distribution('normal',
                                {'loc': loc, 'scale': scale}, dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2), None],
    'a_shape': [(), (3, 2)],
})
)
class TestDistributionsPareto:

    def check_distribution(self, dist_func, a_dtype, dtype):
        a = cupy.ones(self.a_shape, dtype=a_dtype)
        out = dist_func(a, self.shape, dtype)
        if self.shape is not None:
            assert self.shape == out.shape
        assert out.dtype == dtype

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_float_dtypes('a_dtype')
    def test_pareto(self, a_dtype, dtype):
        self.check_distribution(_distributions.pareto,
                                a_dtype, dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2), None],
    'lam_shape': [(), (3, 2)],
})
)
class TestDistributionsPoisson:

    def check_distribution(self, dist_func, lam_dtype, dtype=None):
        lam = cupy.full(self.lam_shape, 5, dtype=lam_dtype)
        if dtype is not None:
            out = dist_func(lam, self.shape, dtype)
            assert out.dtype == dtype
        else:
            out = dist_func(lam, self.shape)
        if self.shape is not None:
            assert self.shape == out.shape
        else:
            assert lam.shape == out.shape

    @cupy.testing.for_int_dtypes('dtype')
    @cupy.testing.for_float_dtypes('lam_dtype')
    def test_poisson_legacy(self, lam_dtype, dtype):
        self.check_distribution(_distributions.poisson, lam_dtype, dtype)

    @cupy.testing.for_float_dtypes('lam_dtype')
    @pytest.mark.skipif(
        cupy.cuda.runtime.is_hip
        and (int(str(cupy.cuda.runtime.runtimeGetVersion())[:3]) < 403),
        reason="HIP<4.3 not supported ",
    )
    def test_poisson_generator(self, lam_dtype):
        self.check_distribution(cupy.random.default_rng(0).poisson,
                                lam_dtype)


class TestDistributionsPoissonInvalid:

    @pytest.mark.skipif(
        cupy.cuda.runtime.is_hip
        and (int(str(cupy.cuda.runtime.runtimeGetVersion())[:3]) < 403),
        reason="HIP<4.3 not supported ",
    )
    def test_none_lam_generator(self):
        with pytest.raises(TypeError):
            cupy.random.default_rng(0).poisson(None)

    def test_none_lam_legacy(self):
        with pytest.raises(ValueError):
            _distributions.poisson(None)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'a_shape': [()],
})
)
class TestDistributionsPower(RandomDistributionsTestCase):

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_float_dtypes('a_dtype')
    def test_power(self, a_dtype, dtype):
        a = numpy.full(self.a_shape, 0.5, dtype=a_dtype)
        self.check_distribution('power', {'a': a}, dtype)

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_float_dtypes('a_dtype')
    def test_power_for_negative_a(self, a_dtype, dtype):
        a = numpy.full(self.a_shape, -0.5, dtype=a_dtype)
        with pytest.raises(ValueError):
            cp_params = {'a': cupy.asarray(a)}
            getattr(_distributions, 'power')(
                size=self.shape, dtype=dtype, **cp_params)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2), None],
    'scale_shape': [(), (3, 2)],
})
)
class TestDistributionsRayleigh(RandomDistributionsTestCase):

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_float_dtypes('scale_dtype')
    def test_rayleigh(self, scale_dtype, dtype):
        scale = numpy.full(self.scale_shape, 3, dtype=scale_dtype)
        self.check_distribution('rayleigh',
                                {'scale': scale}, dtype)

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_float_dtypes('scale_dtype')
    def test_rayleigh_for_zero_scale(self, scale_dtype, dtype):
        scale = numpy.zeros(self.scale_shape, dtype=scale_dtype)
        self.check_distribution('rayleigh',
                                {'scale': scale}, dtype)

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_float_dtypes('scale_dtype')
    def test_rayleigh_for_negative_scale(self, scale_dtype, dtype):
        scale = numpy.full(self.scale_shape, -0.5, dtype=scale_dtype)
        with pytest.raises(ValueError):
            cp_params = {'scale': cupy.asarray(scale)}
            _distributions.rayleigh(size=self.shape, dtype=dtype, **cp_params)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
})
)
class TestDistributionsStandardCauchy(RandomDistributionsTestCase):

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    def test_standard_cauchy(self, dtype):
        self.check_distribution('standard_cauchy', {}, dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2), None],
})
)
class TestDistributionsStandardExponential(RandomDistributionsTestCase):

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    def test_standard_exponential(self, dtype):
        self.check_distribution('standard_exponential', {}, dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2), None],
    'shape_shape': [(), (3, 2)],
})
)
class TestDistributionsStandardGamma(RandomDistributionsTestCase):

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_float_dtypes('shape_dtype')
    def test_standard_gamma_legacy(self, shape_dtype, dtype):
        shape = numpy.ones(self.shape_shape, dtype=shape_dtype)
        self.check_distribution('standard_gamma',
                                {'shape': shape}, dtype)

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_float_dtypes('shape_dtype')
    @pytest.mark.skipif(
        cupy.cuda.runtime.is_hip
        and (int(str(cupy.cuda.runtime.runtimeGetVersion())[:3]) < 403),
        reason="HIP<4.3 not supported ",
    )
    def test_standard_gamma_generator(self, shape_dtype, dtype):
        shape = numpy.ones(self.shape_shape, dtype=shape_dtype)
        self.check_generator_distribution('standard_gamma',
                                          {'shape': shape},
                                          dtype)


class TestDistributionsStandardGammaInvalid(RandomDistributionsTestCase):

    @pytest.mark.skipif(
        cupy.cuda.runtime.is_hip
        and (int(str(cupy.cuda.runtime.runtimeGetVersion())[:3]) < 403),
        reason="HIP<4.3 not supported ",
    )
    def test_none_shape_generator(self):
        with pytest.raises(TypeError):
            cupy.random.default_rng(0).standard_gamma(None)

    def test_none_shape_legacy(self):
        with pytest.raises(ValueError):
            _distributions.standard_gamma(None)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2), None],
})
)
class TestDistributionsStandardNormal(RandomDistributionsTestCase):

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    def test_standard_normal(self, dtype):
        self.check_distribution('standard_normal', {}, dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'df_shape': [(), (3, 2)],
})
)
class TestDistributionsStandardT:

    def check_distribution(self, dist_func, df_dtype, dtype):
        df = cupy.ones(self.df_shape, dtype=df_dtype)
        out = dist_func(df, self.shape, dtype)
        assert self.shape == out.shape
        assert out.dtype == dtype

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_float_dtypes('df_dtype')
    def test_standard_t(self, df_dtype, dtype):
        self.check_distribution(_distributions.standard_t, df_dtype, dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'left_shape': [(), (3, 2)],
    'mode_shape': [(), (3, 2)],
    'right_shape': [(), (3, 2)],
    'dtype': _regular_float_dtypes,  # to escape timeout
})
)
class TestDistributionsTriangular(RandomDistributionsTestCase):

    @cupy.testing.for_dtypes_combination(
        _regular_float_dtypes,
        names=['left_dtype', 'mode_dtype', 'right_dtype'])
    def test_triangular(self, left_dtype, mode_dtype, right_dtype):
        left = numpy.full(self.left_shape, -1, dtype=left_dtype)
        mode = numpy.full(self.mode_shape, 0, dtype=mode_dtype)
        right = numpy.full(self.right_shape, 2, dtype=right_dtype)
        self.check_distribution('triangular',
                                {'left': left, 'mode': mode, 'right': right},
                                self.dtype)

    @cupy.testing.for_float_dtypes('param_dtype', no_float16=True)
    def test_triangular_for_invalid_params(self, param_dtype):
        left = cupy.full(self.left_shape, 1, dtype=param_dtype)
        mode = cupy.full(self.mode_shape, 0, dtype=param_dtype)
        right = cupy.full(self.right_shape, 2, dtype=param_dtype)
        with pytest.raises(ValueError):
            _distributions.triangular(
                left, mode, right, size=self.shape, dtype=self.dtype)

        left = cupy.full(self.left_shape, -2, dtype=param_dtype)
        mode = cupy.full(self.mode_shape, 0, dtype=param_dtype)
        right = cupy.full(self.right_shape, -1, dtype=param_dtype)
        with pytest.raises(ValueError):
            _distributions.triangular(
                left, mode, right, size=self.shape, dtype=self.dtype)

        left = cupy.full(self.left_shape, 0, dtype=param_dtype)
        mode = cupy.full(self.mode_shape, 0, dtype=param_dtype)
        right = cupy.full(self.right_shape, 0, dtype=param_dtype)
        with pytest.raises(ValueError):
            _distributions.triangular(
                left, mode, right, size=self.shape, dtype=self.dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'low_shape': [(), (3, 2)],
    'high_shape': [(), (3, 2)],
})
)
class TestDistributionsUniform(RandomDistributionsTestCase):

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_dtypes_combination(
        _float_dtypes, names=['low_dtype', 'high_dtype'])
    def test_uniform(self, low_dtype, high_dtype, dtype):
        low = numpy.ones(self.low_shape, dtype=low_dtype)
        high = numpy.ones(self.high_shape, dtype=high_dtype) * 2.
        self.check_distribution('uniform',
                                {'low': low, 'high': high}, dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'mu_shape': [(), (3, 2)],
    'kappa_shape': [(), (3, 2)],
    'dtype': _float_dtypes,  # to escape timeout
})
)
class TestDistributionsVonmises:

    def check_distribution(self, dist_func, mu_dtype, kappa_dtype, dtype):
        mu = cupy.ones(self.mu_shape, dtype=mu_dtype)
        kappa = cupy.ones(self.kappa_shape, dtype=kappa_dtype)
        out = dist_func(mu, kappa, self.shape, dtype)
        assert self.shape == out.shape
        assert out.dtype == dtype

    @cupy.testing.for_dtypes_combination(
        _float_dtypes, names=['mu_dtype', 'kappa_dtype'])
    def test_vonmises(self, mu_dtype, kappa_dtype):
        self.check_distribution(_distributions.vonmises,
                                mu_dtype, kappa_dtype, self.dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'mean_shape': [(), (3, 2)],
    'scale_shape': [(), (3, 2)],
    'dtype': _regular_float_dtypes,  # to escape timeout
})
)
class TestDistributionsWald(RandomDistributionsTestCase):

    @cupy.testing.for_dtypes_combination(
        _float_dtypes, names=['mean_dtype', 'scale_dtype'])
    def test_wald(self, mean_dtype, scale_dtype):
        mean = numpy.full(self.mean_shape, 3, dtype=mean_dtype)
        scale = numpy.full(self.scale_shape, 3, dtype=scale_dtype)
        self.check_distribution('wald',
                                {'mean': mean, 'scale': scale}, self.dtype)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'a_shape': [(), (3, 2)],
})
)
class TestDistributionsWeibull(RandomDistributionsTestCase):

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_float_dtypes('a_dtype')
    def test_weibull(self, a_dtype, dtype):
        a = numpy.ones(self.a_shape, dtype=a_dtype)
        self.check_distribution('weibull',
                                {'a': a}, dtype)

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_float_dtypes('a_dtype')
    def test_weibull_for_inf_a(self, a_dtype, dtype):
        a = numpy.full(self.a_shape, numpy.inf, dtype=a_dtype)
        self.check_distribution('weibull', {'a': a}, dtype)

    @cupy.testing.for_float_dtypes('dtype', no_float16=True)
    @cupy.testing.for_float_dtypes('a_dtype')
    def test_weibull_for_negative_a(self, a_dtype, dtype):
        a = numpy.full(self.a_shape, -0.5, dtype=a_dtype)
        with pytest.raises(ValueError):
            cp_params = {'a': cupy.asarray(a)}
            getattr(_distributions, 'weibull')(
                size=self.shape, dtype=dtype, **cp_params)


@testing.parameterize(*testing.product({
    'shape': [(4, 3, 2), (3, 2)],
    'a_shape': [(), (3, 2)],
})
)
class TestDistributionsZipf(RandomDistributionsTestCase):

    @cupy.testing.for_dtypes([numpy.int32, numpy.int64], 'dtype')
    @cupy.testing.for_float_dtypes('a_dtype')
    def test_zipf(self, a_dtype, dtype):
        a = numpy.full(self.a_shape, 2, dtype=a_dtype)
        self.check_distribution('zipf', {'a': a}, dtype)
