using StatsFuns
using Rmath: Rmath
using Test

include("utils.jl")

function check_rmath(statsfun, rmathfun, params, a, isprob, rtol)
    v = @inferred(statsfun(params..., a))
    rv = @inferred(rmathfun(a, params...))
    @test v isa float(Base.promote_typeof(params..., a))
    return if isprob
        @test v ≈ oftype(v, rv) rtol = rtol nans = true
    else
        @test v ≈ oftype(v, rv) atol = rtol rtol = rtol nans = true
    end
end

function rmathcomp(basename::String, params, X::AbstractArray, rtol = _default_rtol(params, X))
    rbasename = if basename == "nfdist"
        "nf"
    elseif basename == "ntdist"
        "nt"
    elseif basename == "srdist"
        "tukey"
    else
        basename
    end

    # Rmath.ptukey / Rmath.qtukey take an extra `nranges` positional argument
    # (defaulting to 1.0) between the distribution parameters and the
    # `lower_tail`/`log_p` flags. Inject it explicitly for tukey so the test
    # lambdas line up with the Rmath C signature.
    extra_rmath_args = rbasename == "tukey" ? (1,) : ()

    if isdefined(Rmath, Symbol(:d, rbasename))
        stats_pdf = getproperty(@__MODULE__, Symbol(basename, :pdf))
        rmath_pdf = let f = getproperty(Rmath, Symbol(:d, rbasename))
            (a, params...) -> f(a, params..., false)
        end
        @testset "pdf with x=$x" for x in X
            check_rmath(
                stats_pdf, rmath_pdf,
                params, x, true, rtol
            )
        end

        stats_logpdf = getproperty(@__MODULE__, Symbol(basename, :logpdf))
        rmath_logpdf = let f = getproperty(Rmath, Symbol(:d, rbasename))
            (a, params...) -> f(a, params..., true)
        end
        @testset "logpdf with x=$x" for x in X
            check_rmath(
                stats_logpdf, rmath_logpdf,
                params, x, false, rtol
            )
        end
    end

    if isdefined(Rmath, Symbol(:p, rbasename))
        stats_cdf = getproperty(@__MODULE__, Symbol(basename, :cdf))
        rmath_cdf = let f = getproperty(Rmath, Symbol(:p, rbasename)), extra = extra_rmath_args
            (a, params...) -> f(a, params..., extra..., true, false)
        end
        @testset "cdf with x=$x" for x in X
            check_rmath(stats_cdf, rmath_cdf, params, x, true, rtol)
        end

        stats_ccdf = getproperty(@__MODULE__, Symbol(basename, :ccdf))
        rmath_ccdf = let f = getproperty(Rmath, Symbol(:p, rbasename)), extra = extra_rmath_args
            (a, params...) -> f(a, params..., extra..., false, false)
        end
        @testset "ccdf with x=$x" for x in X
            check_rmath(stats_ccdf, rmath_ccdf, params, x, true, rtol)
        end

        stats_logcdf = getproperty(@__MODULE__, Symbol(basename, :logcdf))
        rmath_logcdf = let f = getproperty(Rmath, Symbol(:p, rbasename)), extra = extra_rmath_args
            (a, params...) -> f(a, params..., extra..., true, true)
        end
        @testset "logcdf with x=$x" for x in X
            check_rmath(stats_logcdf, rmath_logcdf, params, x, false, rtol)
        end

        stats_logccdf = getproperty(@__MODULE__, Symbol(basename, :logccdf))
        rmath_logccdf = let f = getproperty(Rmath, Symbol(:p, rbasename)), extra = extra_rmath_args
            (a, params...) -> f(a, params..., extra..., false, true)
        end
        @testset "logccdf with x=$x" for x in X
            check_rmath(stats_logccdf, rmath_logccdf, params, x, false, rtol)
        end

        #=
        signrank and wilcox are implemented natively rather than by delegating to Rmath, so
        their inverse functions are not expected to reproduce the Rmath quantile functions
        bit for bit. The inverses locate a discontinuity by searching for the first argument
        at which the cdf reaches `q`, and the `q` tested here are themselves cdf values, so
        the comparison is decided by the last bit of the cdf and is not stable across
        platforms. R's own signrank already varies this way:
        julia> psignrank(18,10,false,true) # windows
        -0.2076393647782445
        julia> psignrank(18,10,false,true) # linux
        -0.20763936477824452
        As for the other natively implemented discrete distributions, the inverse functions
        are instead tested by round tripping through our own cdf further down.
        =#
        test_inv = basename != "signrank" && basename != "wilcox"
        if isdefined(Rmath, Symbol(:q, rbasename)) && test_inv
            stats_invcdf = getproperty(@__MODULE__, Symbol(basename, :invcdf))
            rmath_invcdf = let f = getproperty(Rmath, Symbol(:q, rbasename)), extra = extra_rmath_args
                (a, params...) -> f(a, params..., extra..., true, false)
            end
            p = rmath_cdf.(X, params...)
            @testset "invcdf with q=$_p" for _p in p
                check_rmath(stats_invcdf, rmath_invcdf, params, _p, false, rtol)
            end

            stats_invccdf = getproperty(@__MODULE__, Symbol(basename, :invccdf))
            rmath_invccdf = let f = getproperty(Rmath, Symbol(:q, rbasename)), extra = extra_rmath_args
                (a, params...) -> f(a, params..., extra..., false, false)
            end
            cp = rmath_ccdf.(X, params...)
            @testset "invccdf with q=$_p" for _p in cp
                check_rmath(stats_invccdf, rmath_invccdf, params, _p, false, rtol)
            end

            stats_invlogcdf = getproperty(@__MODULE__, Symbol(basename, :invlogcdf))
            rmath_invlogcdf = let f = getproperty(Rmath, Symbol(:q, rbasename)), extra = extra_rmath_args
                (a, params...) -> f(a, params..., extra..., true, true)
            end
            lp = rmath_logcdf.(X, params...)
            @testset "invlogcdf with log(q)=$_p" for _p in lp
                check_rmath(stats_invlogcdf, rmath_invlogcdf, params, _p, false, rtol)
            end

            stats_invlogccdf = getproperty(@__MODULE__, Symbol(basename, :invlogccdf))
            rmath_invlogccdf = let f = getproperty(Rmath, Symbol(:q, rbasename)), extra = extra_rmath_args
                (a, params...) -> f(a, params..., extra..., false, true)
            end
            lcp = rmath_logccdf.(X, params...)
            @testset "invlogccdf with log(q)=$_p" for _p in lcp
                check_rmath(stats_invlogccdf, rmath_invlogccdf, params, _p, false, rtol)
            end
        end
    end

    return nothing
end

function rmathcomp_tests(basename::String, configs)
    return @testset "$basename" begin
        @testset "params: $params" for (params, data) in configs
            rmathcomp(basename, params, data)
        end
    end
end

### Test cases

@testset "RMath" begin
    rmathcomp_tests(
        "beta", [
            ((0.1, 1.0), 0.0:0.01:1.0),
            ((1.0, 1.0), 0.0:0.01:1.0),
            ((2.0, 3.0), 0.0:0.01:1.0),
            ((10.0, 2.0), 0.0:0.01:1.0),
            ((10, 2), 0.0:0.01:1.0),
            ((1.0f0, 1.0f0), 0.0f0:0.01f0:1.0f0),
            ((1.0, 1.0), 0.0f0:0.01f0:1.0f0),
            ((Float16(1), Float16(1)), Float16(0):Float16(0.01):Float16(1)),
            ((1.0f0, 1.0f0), Float16(0):Float16(0.01):Float16(1)),
            ((10, 2), [0, 1]),
            ((10, 2), (0 // 1):(1 // 100):(1 // 1)),
        ]
    )
    # It is not possible to maintain a rtol of 1e-14 for the cdf when there is such a large difference
    # in the magnitude of the parameters. At least not with the current parameters. Furthermore, it
    # seems that Rmath is actually less accurate than we are but since we are comparing against Rmath
    # we have to use rtol=1e-12 although we are probably only off by around 1e-13.
    @testset "param: (1000, 2)" begin
        rmathcomp(
            "beta",
            (1000, 2),
            # We have to drop the 0.48 value since the R quantile function fails while we succeed.
            # It's tested separate below.
            setdiff(collect(0.0:0.01:1.0), 0.48),
            1.0e-12
        )
        # Test p=0.48 separately since R fails. (It's pretty slow, though, caused by the cdf being 9.0797754e-317)
        @test betainvcdf(1000, 2, betacdf(1000, 2, 0.48)) ≈ 0.48
    end
    @testset "$(f)(0, 0, $x) should error" for f in (betacdf, betaccdf, betalogcdf, betalogccdf),
            x in (0.0, 0.5, 1.0)
        @test_throws DomainError f(0.0, 0.0, x)
    end
    # Have to check them separately since Rmath is not completely consistent with our conventions:
    # - betacdf(α, β, x) = P(X ≤ x) where X ~ Beta(α, β)
    # - betaccdf(α, β, x) = P(X > x) where X ~ Beta(α, β)
    # - betainvcdf(α, β, p) = inf { x : p ≤ P(X ≤ x) } where X ~ Beta(α, β)
    # - betainvccdf(α, β, p) = sup { x : p ≤ P(X > x) } where X ~ Beta(α, β)
    @testset "beta: degenerate cases" begin
        # Check degenerate cases Beta(0, β) and Dirac(α, 0) with α, β > 0
        # Beta(0, β) is a Dirac distribution at x=0
        # Beta(α, 0) is a Dirac distribution at x=1
        α = β = 1 // 2

        for x in 0.0f0:0.01f0:1.0f0
            # Check betacdf
            @test @inferred(betacdf(0, β, x)) === 1.0f0
            @test @inferred(betacdf(α, 0, x)) === (x < 1 ? 0.0f0 : 1.0f0)

            # Check betaccdf, betalogcdf, and betalogccdf based on betacdf
            @test @inferred(betaccdf(0, β, x)) === 1 - betacdf(0, β, x)
            @test @inferred(betaccdf(α, 0, x)) === 1 - betacdf(α, 0, x)
            @test @inferred(betalogcdf(0, β, x)) === log(betacdf(0, β, x))
            @test @inferred(betalogcdf(α, 0, x)) === log(betacdf(α, 0, x))
            @test @inferred(betalogccdf(0, β, x)) === log(betaccdf(0, β, x))
            @test @inferred(betalogccdf(α, 0, x)) === log(betaccdf(α, 0, x))
        end

        for p in 0.0f0:0.01f0:1.0f0
            # Check betainvcdf
            @test @inferred(betainvcdf(0, β, p)) === 0.0f0
            @test @inferred(betainvcdf(α, 0, p)) === (p > 0 ? 1.0f0 : 0.0f0)

            # Check betainvccdf
            @test @inferred(betainvccdf(0, β, p)) === (p > 0 ? 0.0f0 : 1.0f0)
            @test @inferred(betainvccdf(α, 0, p)) === 1.0f0
        end
    end

    rmathcomp_tests(
        "binom", [
            ((1, 0.5), 0.0:1.0),
            ((1, 0.7), 0.0:1.0),
            ((8, 0.6), 0.0:8.0),
            ((20, 0.1), 0.0:20.0),
            ((20, 0.9), 0.0:20.0),
            ((20, 0.9), 0:20),
            ((1, 0.5f0), 0.0f0:1.0f0),
            ((1, 0.5), 0.0f0:1.0f0),
            ((1, Float16(0.5)), Float16(0):Float16(1)),
            ((1, 0.5f0), Float16(0):Float16(1)),
            ((10, 1 // 2), (0 // 1):(10 // 1)),
            ((1, 0.5), [-Inf, Inf]),
        ]
    )

    rmathcomp_tests(
        "chisq", [
            ((1,), 0.0:0.1:8.0),
            ((4,), 0.0:0.1:8.0),
            ((9,), 0.0:0.1:8.0),
            ((9,), 0:8),
            ((1,), 0.0f0:0.1f0:8.0f0),
            ((1,), Float16(0):Float16(0.1):Float16(8)),
            ((9,), (0 // 1):(8 // 1)),
            ((1), [-Inf, Inf]),
        ]
    )

    rmathcomp_tests(
        "fdist", [
            ((1, 1), (0.0:0.1:5.0)),
            ((2, 1), (0.0:0.1:5.0)),
            ((5, 2), (0.0:0.1:5.0)),
            ((10, 1), (0.0:0.1:5.0)),
            ((10, 3), (0.0:0.1:5.0)),
            ((10, 3), (0:5)),
            ((1, 1), (0.0f0:0.1f0:5.0f0)),
            ((1, 1), (Float16(0):Float16(0.1):Float16(5))),
            ((10, 3), (0 // 1):(5 // 1)),
        ]
    )

    rmathcomp_tests(
        "gamma", [
            ((1.0, 1.0), (0.0:0.05:12.0)),
            ((0.5, 1.0), (0.0:0.05:12.0)),
            ((3.0, 1.0), (0.0:0.05:12.0)),
            ((9.0, 1.0), (0.0:0.05:12.0)),
            ((2.0, 3.0), (0.0:0.05:12.0)),
            ((2, 3), (0:12)),
            ((1.0f0, 1.0f0), (0.0f0:0.05f0:12.0f0)),
            ((1.0, 1.0), (0.0f0:0.05f0:12.0f0)),
            ((Float16(1), Float16(1)), (Float16(0):Float16(0.05):Float16(12))),
            ((1.0f0, 1.0f0), (Float16(0):Float16(0.05):Float16(12))),
            ((2, 3), ((0 // 1):(12 // 1))),
            ((3.0, 1.0e-310), (0.0:0.05:12.0)),
            ((1, 1.0), [-Inf, Inf]),
        ]
    )

    rmathcomp_tests(
        "hyper", [
            ((2, 3, 4), 0.0:4.0),
            ((2, 3, 4), 0:4),
            ((2, 3, 4), 0.0f0:4.0f0),
            ((2, 3, 4), Float16(0):Float16(4)),
            ((2, 3, 4), (0 // 1):(4 // 1)),
        ]
    )

    rmathcomp_tests(
        "nbeta", [
            ((1.0, 1.0, 0.0), 0.01:0.01:0.99),
            ((2.0, 3.0, 0.0), 0.01:0.01:0.99),
            ((1.0, 1.0, 2.0), 0.01:0.01:0.99),
            ((3.0, 4.0, 2.0), 0.01:0.01:0.99),
            ((3, 4, 2), 0.01:0.01:0.99),
            ((1.0f0, 1.0f0, 0.0f0), 0.01f0:0.01f0:0.99f0),
            ((1.0, 1.0, 0.0), 0.01f0:0.01f0:0.99f0),
            ((Float16(1), Float16(1), Float16(0)), Float16(0.01):Float16(0.01):Float16(0.99)),
            ((1.0f0, 1.0f0, 0.0f0), Float16(0.01):Float16(0.01):Float16(0.99)),
            ((3, 4, 2), (1 // 100):(1 // 100):(99 // 100)),
            ((1.0, 1.0, 0.0), [-Inf, Inf]),
        ]
    )

    rmathcomp_tests(
        "nbinom", [
            ((1, 0.5), 0.0:20.0),
            ((3, 0.5), 0.0:20.0),
            ((3, 0.2), 0.0:20.0),
            ((3, 0.8), 0.0:20.0),
            ((1, 0.5f0), 0.0f0:20.0f0),
            ((1, 0.5), 0.0f0:20.0f0),
            ((1, Float16(0.5)), Float16(0):Float16(20)),
            ((3, 0.8), 0:20),
            ((3, 1 // 2), (0 // 1):(20 // 1)),
            ((1, 0.5), [-Inf, Inf, NaN]),
            ((1, 0.5f0), [-Inf32, Inf32, NaN32, -2, 1.1]),
        ]
    )

    rmathcomp_tests(
        "nchisq", [
            ((2, 1), 0.0:0.2:8.0),
            ((2, 3), 0.0:0.2:8.0),
            ((4, 1), 0.0:0.2:8.0),
            ((4, 3), 0.0:0.2:8.0),
            ((4, 3), 0:8),
            ((2, 1), 0.0f0:0.2f0:8.0f0),
            ((2, 1), Float16(0):Float16(0.2):Float16(8)),
            ((2, 1), (0 // 1):(1 // 5):(8 // 1)),
            ((2, 1), [-Inf, Inf]),
        ]
    )

    rmathcomp_tests(
        "nfdist", [
            ((1.0, 1.0, 0.0), 0.1:0.1:10.0),
            ((1.0, 1.0, 2.0), 0.1:0.1:10.0),
            ((2.0, 3.0, 1.0), 0.1:0.1:10.0),
            ((2, 3, 1), 1:10),
            ((1.0f0, 1.0f0, 0.0f0), 0.1f0:0.1f0:10.0f0),
            ((1.0, 1.0, 0.0), 0.1f0:0.1f0:10.0f0),
            ((Float16(1), Float16(1), Float16(0)), Float16(0.1):Float16(0.1):Float16(10)),
            ((1.0f0, 1.0f0, 0.0f0), Float16(0.1):Float16(0.1):Float16(10)),
            ((2, 3, 1), (1 // 1):(10 // 1)),
            ((1.0, 1.0, 0.0), [-Inf, Inf]),
        ]
    )

    rmathcomp_tests(
        "norm", [
            ((0.0, 1.0), -6.0:0.01:6.0),
            ((2.0, 1.0), -3.0:0.01:7.0),
            ((0.0, 0.5), -3.0:0.01:3.0),
            ((0, 1), -3.0:3.0),
            ((0, 1), -3.0:0.01:3.0),
            ((0, 1), -3:3),
            ((0.0, 0.0), -6.0:0.1:6.0),
            ((0.0f0, 1.0f0), -6.0f0:0.01f0:6.0f0),
            ((0.0, 1.0), -6.0f0:0.01f0:6.0f0),
            ((0, 2), (-6 // 1):(1 // 2):(6 // 1)),
            ((0.0f0, 2.0f0), (-6 // 1):(1 // 2):(6 // 1)),
            ((0.0, 1.0), [-Inf, Inf]),
            # Fail since `SpecialFunctions.erfcx` is not implemented for `Float16`
            #((Float16(0), Float16(1)), -Float16(6):Float16(0.01):Float16(6)),
            #((0f0, 1f0), -Float16(6):Float16(0.01):Float16(6)),
        ]
    )

    rmathcomp_tests(
        "ntdist", [
            ((0, 1), -4.0:0.1:10.0),
            ((0, 4), -4.0:0.1:10.0),
            ((2, 1), -4.0:0.1:10.0),
            ((2, 4), -4.0:0.1:10.0),
            ((2, 4), -4:10),
            ((0, 1), -4.0f0:0.1f0:10.0f0),
            ((0, 1), -Float16(4):Float16(0.1):Float16(10)),
            ((0, 1), (-4 // 1):(1 // 10):(10 // 1)),
            ((10, 1), [-Inf, Inf]),
        ]
    )

    rmathcomp_tests(
        "pois", [
            ((0.5,), 0.0:20.0),
            ((1.0,), 0.0:20.0),
            ((2.0,), 0.0:20.0),
            ((10.0,), 0.0:20.0),
            ((10,), 0:20),
            ((0.5f0,), 0.0f0:20.0f0),
            ((0.5,), 0.0f0:20.0f0),
            ((Float16(0.5),), Float16(0):Float16(20)),
            ((0.5f0,), Float16(0):Float16(20)),
            ((1 // 2,), (0 // 1):(20 // 1)),
        ]
    )

    rmathcomp_tests(
        "tdist", [
            ((1,), -5.0:0.1:5.0),
            ((2,), -5.0:0.1:5.0),
            ((5,), -5.0:0.1:5.0),
            ((5,), -5:5),
            ((1,), -5.0f0:0.1f0:5.0f0),
            ((1,), -Float16(5):Float16(0.1):Float16(5)),
            ((1,), (-5 // 1):(5 // 1)),
            ((Inf,), -5.0:0.1:5.0),
            ((Inf32,), -5.0f0:0.1f0:5.0f0),
            ((1,), [-Inf, Inf]),
        ]
    )

    rmathcomp_tests(
        "signrank", [
            ((4), -2:12),
            ((4), -2.0:0.25:12.0),
            ((10), -2:57),
        ]
    )

    # The R version does not roundtrip cdf->invcdf, while our version does
    @test_broken RFunctions.signrankinvcdf.(50, RFunctions.signrankcdf.(50, -1:1276)) == [0; 0:1275; 1275]
    @test signrankinvcdf.(50, signrankcdf.(50, -1:1276)) == [0; 0:1275; 1275]
    @test signrankinvccdf.(50, signrankccdf.(50, -1:1276)) == [0; 0:1275; 1275]
    @test signrankinvlogcdf.(50, signranklogcdf.(50, 0:1276)) == [0:1275; 1275]
    @test isnan(signrankinvlogcdf.(50, signranklogcdf(500, -1)))
    @test signrankinvlogccdf.(50, signranklogccdf.(50, -1:1274)) == [0; 0:1274]
    @test isnan(signrankinvlogccdf.(50, signranklogccdf.(50, 1275)))
    @test isnan(signrankinvlogccdf.(50, signranklogccdf.(50, 1276)))

    # The subset counts grow like 2^n, so accumulating them in `Int` silently wrapped
    # around and returned invalid probabilities from n = 72 onwards (#219)
    @testset "signrank: large n" begin
        @testset "n = $n" for n in (71, 72, 73, 74, 80, 100, 120, 200)
            # The distribution is symmetric about n * (n + 1) / 4
            W = (n * (n + 1)) ÷ 4
            @test signrankpdf(n, W) ≈ Rmath.dsignrank(W, n, false)
            @test signrankcdf(n, W) ≈ Rmath.psignrank(W, n, true, false)
            @test signrankccdf(n, W) ≈ Rmath.psignrank(W, n, false, false)
            # Deep in the left tail, where the probabilities are tiny but still normal
            W2 = W ÷ 3
            @test signrankpdf(n, W2) ≈ Rmath.dsignrank(W2, n, false)
            @test signrankcdf(n, W2) ≈ Rmath.psignrank(W2, n, true, false)
        end

        @testset "pdf sums to one, n = $n" for n in (70, 80, 100)
            @test sum(signrankpdf(n, W) for W in 0:((n * (n + 1)) ÷ 2)) ≈ 1
        end
    end

    rmathcomp_tests(
        "srdist", [
            ((1, 2), (0.0:0.2:5.0)),
            ((2, 2), (0.0:0.2:5.0)),
            ((5, 3), (0.0:0.2:5.0)),
            ((10, 2), (0.0:0.2:5.0)),
            ((10, 5), (0.0:0.2:5.0)),
            ((1, 2), (0.0f0:0.2f0:5.0f0)),
            ((1, 2), (Float16(0):Float16(0.2):Float16(5))),
            ((1, 2), ((0 // 1):(1 // 5):(5 // 1))),
        ]
    )

    rmathcomp_tests(
        "wilcox", [
            ((3, 4), -2:13),
            ((3, 4), -2.0:13.0),
            ((4, 3), -2:13),
            ((4, 4), -2:17),
            ((1, 7), -2:8),
            ((1, 7), -2:8),
        ]
    )


    @test wilcoxinvcdf.(10, 10, wilcoxcdf.(10, 10, -1:101)) == [0; 0:100; 100]
    @test wilcoxinvccdf.(10, 10, wilcoxccdf.(10, 10, -1:101)) == [0; 0:100; 100]
    @test wilcoxinvlogcdf.(10, 10, wilcoxlogcdf.(10, 10, 0:101)) == [0:100; 100]
    @test isnan(wilcoxinvlogcdf.(10, 10, wilcoxlogcdf(10, 10, -1)))
    @test wilcoxinvlogccdf.(10, 10, wilcoxlogccdf.(10, 10, -1:99)) == [0; 0:99]
    @test isnan(wilcoxinvlogccdf.(10, 10, wilcoxlogccdf.(10, 10, 100)))
    @test isnan(wilcoxinvlogccdf.(10, 10, wilcoxlogccdf.(10, 10, 101)))

    # Accumulating the partition counts in `Int` used to break down in two separate ways:
    # the counts themselves wrapped around and returned silently invalid probabilities (#220),
    # and their normaliser `binomial(nx + ny, nx)` threw an `OverflowError` from
    # nx + ny = 68 onwards (#219). The silent limit is the lower of the two, and it is set by
    # `binomial(nx + ny, nx)` rather than by `nx + ny`: the Löffler recurrence subtracts as
    # well as adds, so its intermediates overshoot the counts and wrap first. Unbalanced
    # groups therefore stayed correct much further out, which is why the cases below are not
    # all balanced.
    @testset "wilcox: large nx and ny" begin
        @testset "nx = $nx, ny = $ny" for (nx, ny) in (
                (32, 32), (33, 33), (34, 34), (40, 40), (50, 50),
                (30, 36), (20, 48), (20, 60), (10, 325), (10, 326), (5, 200), (150, 40),
            )
            # The distribution is symmetric about nx * ny / 2
            U = (nx * ny) ÷ 2
            @test wilcoxpdf(nx, ny, U) ≈ Rmath.dwilcox(U, nx, ny, false)
            @test wilcoxcdf(nx, ny, U) ≈ Rmath.pwilcox(U, nx, ny, true, false)
            @test wilcoxccdf(nx, ny, U) ≈ Rmath.pwilcox(U, nx, ny, false, false)
            # Deep in the left tail, where the probabilities are tiny but still normal
            U2 = U ÷ 3
            @test wilcoxpdf(nx, ny, U2) ≈ Rmath.dwilcox(U2, nx, ny, false)
            @test wilcoxcdf(nx, ny, U2) ≈ Rmath.pwilcox(U2, nx, ny, true, false)
        end

        @testset "pdf sums to one, nx = $nx, ny = $ny" for (nx, ny) in ((30, 30), (33, 33), (34, 34), (50, 50), (5, 200))
            @test sum(wilcoxpdf(nx, ny, U) for U in 0:(nx * ny)) ≈ 1
        end

        # `1 / binomial(nx + ny, nx)` is the probability of the least likely outcome and
        # leaves the normal range from nx + ny = 1030. It seeds the recurrence, which is
        # homogeneous, so once it reaches zero every probability follows it and the results
        # are silently zero rather than merely imprecise. The recurrence therefore works in
        # units of a power of two, which these check has not disturbed anything.
        @testset "seed below the normal range, nx = ny = $m" for m in (515, 520, 530, 535)
            # the pdf at U = 0 is exactly the seed, so this pins the scaling down directly
            @test wilcoxpdf(m, m, 0) ≈ Float64(1 / BigFloat(binomial(big(2m), big(m)), precision = 512))
        end

        @testset "seed at zero without scaling" begin
            # `1 / binomial(1080, 524)` is zero in `Float64`, so every one of these was
            # exactly 0.0 before the recurrence was rescaled
            @test 0 < wilcoxpdf(524, 556, 20000) < 1
            @test 0 < wilcoxcdf(524, 556, 20000) < 1
            @test wilcoxpdf(524, 556, 20000) <= wilcoxcdf(524, 556, 20000)
            @test wilcoxcdf(524, 556, 20000) < wilcoxcdf(524, 556, 20001)
        end
    end

    # Note: Convergence fails in srdist with cdf values below 0.16 with df = 10, k = 5.
    # Reduced df or k allows convergence. This test documents this behavior.
    x = 0.15
    q = srdistcdf(10, 5, 0.15)
    rx = srdistinvcdf(10, 5, q)
    rtol = 100eps(1.0)
    @test_broken x ≈ rx atol = rtol rtol = rtol nans = true

    # `nranges=1` must be passed to Rmath.ptukey/qtukey; without it the Bool
    # flags shift into the `nranges` slot and CDF↔CCDF invert.
    @testset "srdist boundary behaviour" begin
        @test iszero(srdistcdf(2.0, 2.0, 0.0))
        @test isone(srdistccdf(2.0, 2.0, 0.0))
        @test srdistlogcdf(2.0, 2.0, 0.0) == -Inf
        @test iszero(srdistlogccdf(2.0, 2.0, 0.0))
        @test isone(srdistcdf(2.0, 2.0, Inf))
        @test iszero(srdistccdf(2.0, 2.0, Inf))
        @test iszero(srdistlogcdf(2.0, 2.0, Inf))
        @test srdistlogccdf(2.0, 2.0, Inf) == -Inf
    end

    # Test values outside of the support
    rmathcomp_tests(
        "beta", [
            ((1.0, 1.0), [-10.0, -6.3, 2.1, 23.5]),
            ((1 // 1, 1 // 1), [-10 // 1, -63 // 10, 21 // 10, 47 // 2]),
            ((1, 1), [-10, -6, 2, 24]),
        ]
    )
    rmathcomp_tests(
        "binom", [
            ((5, 0.5), [-8, -2.3, 1.2, 5.4, 11.9]),
            ((5, 1 // 2), [-8, -23 // 10, 6 // 5, 27 // 5, 119 // 10]),
            ((5, 1 // 2), [-8, -2, 6, 12]),
        ]
    )
    rmathcomp_tests(
        "fdist", [
            ((1.0, 1.0), [-10.0, -6.3]),
            ((1 // 1, 1 // 1), [-10 // 1, -63 // 10]),
            ((1, 1), [-10, -6]),
        ]
    )
    rmathcomp_tests(
        "gamma", [
            ((1.0, 1.0), [-10.0, -6.3]),
            ((1 // 1, 1 // 1), [-10 // 1, -63 // 10]),
            ((1, 1), [-10, -6]),
        ]
    )
    rmathcomp_tests(
        "pois", [
            ((0.5,), [-10, -2.5, 1.3, 8.7]),
            ((1 // 2,), [-10, -5 // 2, 13 // 10, 87 // 10]),
            ((1,), [-10, -3]),
        ]
    )
end
