"""
Module for testing ODEInterface
"""
module ODEInterfaceTest

using Test

using ODEInterface
@ODEInterface.import_huge

const dl_solvers = (
    DL_DOPRI5, DL_DOPRI5_I32,
    DL_DOP853, DL_DOP853_I32,
    DL_ODEX, DL_ODEX_I32,
    DL_RADAU5, DL_RADAU5_I32, DL_RADAU, DL_RADAU_I32,
    DL_SEULEX, DL_SEULEX_I32,
    DL_RODAS, DL_RODAS_I32,
    DL_BVPSOL, DL_BVPSOL_I32,
    DL_DDEABM, DL_DDEABM_I32,
    DL_DDEBDF, DL_DDEBDF_I32,
    DL_BVPM2,
)
const solvers = (
    dopri5, dopri5_i32,
    dop853, dop853_i32,
    odex, odex_i32,
    radau5, radau5_i32, radau, radau_i32,
    seulex, seulex_i32,
    rodas, rodas_i32,
    ddeabm, ddeabm_i32,
    ddebdf, ddebdf_i32,
)

const solvers_without_dense_output = (ddeabm, ddeabm_i32, ddebdf, ddebdf_i32)

const solvers_without_special_struct_support = (ddebdf, ddebdf_i32)

const solvers_mas = (
    radau5, radau5_i32, radau, radau_i32,
    seulex, seulex_i32,
    rodas, rodas_i32,
)

const solvers_jac = (
    radau5, radau5_i32, radau, radau_i32,
    seulex, seulex_i32,
    rodas, rodas_i32,
    ddebdf, ddebdf_i32,
)

const solvers_rhsdt = (
    rodas, rodas_i32,
)

const solvers_bvpsol = (
    bvpsol, bvpsol_i32,
)

const solvers_colnew = (
    colnew, colnew_i32,
)

"""
create a callable-type in order to check, if solvers
can handle callable-types (which are not a subclass of
Function) as right-hand sides.
"""
mutable struct Callable_Type
    param::Float64
end

function (ct::Callable_Type)(t, x)
    return ct.param * x
end

function mylinspace(a, b, length::Integer)
    return collect(range(a, stop = b, length = length))
end

function test_ode1(solver::Function)
    opt = OptionsODE(
        "ode1",
        OPT_RTOL => 1.0e-10,
        OPT_ATOL => 1.0e-10
    )
    t0 = 0; T = 1; x0 = [1, 2]; rhs = (t, x) -> x

    (t, x, retcode, stats) = solver(rhs, t0, T, x0, opt)
    @assert 1 == retcode
    @assert t == T
    @assert x0 == [1, 2]
    @assert isapprox(x[1], exp(1), rtol = 1.0e-7, atol = 1.0e-7)
    @assert isapprox(x[2], 2 * exp(1), rtol = 1.0e-7, atol = 1.0e-7)
    if haskey(stats, "step_predict")
        @assert isa(stats["step_predict"], Number)
    end
    return true
end

function test_ode2(solver::Function)
    dense_flag = !(solver in solvers_without_dense_output)
    x3 = NaN
    called_init = false
    called_done = false

    function outputfcn(reason, told, t, x, eval_sol_fcn, extra_data)
        if reason == OUTPUTFCN_CALL_INIT
            called_init = true
            extra_data["test_ode2_data"] = 56
        end
        if reason == OUTPUTFCN_CALL_DONE
            called_done = true
            @assert extra_data["test_ode2_data"] == 56
        end
        if reason == OUTPUTFCN_CALL_STEP
            if told ≤ 3.0 ≤ t
                if dense_flag
                    x3 = eval_sol_fcn(3.0)[1]
                end
                return OUTPUTFCN_RET_STOP
            end
        end
        return OUTPUTFCN_RET_CONTINUE
    end
    opt = OptionsODE(
        "ode2",
        OPT_RTOL => 1.0e-8,
        OPT_ATOL => 1.0e-8,
        OPT_OUTPUTFCN => outputfcn,
        OPT_OUTPUTMODE => dense_flag ? OUTPUTFCN_DENSE : OUTPUTFCN_WODENSE,
    )
    t0 = 0; T = 5000; x0 = [1, 2]; rhs = (t, x) -> x
    (t, x, retcode, stats) = solver(rhs, t0, T, x0, opt)
    @assert called_init && called_done
    @assert 2 == retcode
    @assert t < T
    if dense_flag
        @assert isapprox(x3, exp(3), rtol = 1.0e-7, atol = 1.0e-7)
    end
    return true
end

function test_ode3(solver::Function)
    opt = OptionsODE(
        "ode3",
        OPT_RTOL => 1.0e-10,
        OPT_ATOL => 1.0e-10,
        OPT_RHS_CALLMODE => RHS_CALL_INSITU,
    )
    function rhs(t, x, dx)
        dx[1] = x[1]; dx[2] = x[2]
        return nothing
    end
    t0 = 0; T = 1; x0 = [1, 2]

    (t, x, retcode, stats) = solver(rhs, t0, T, x0, opt)
    @assert 1 == retcode
    @assert t == T
    @assert x0 == [1, 2]
    @assert isapprox(x[1], exp(1), rtol = 1.0e-7, atol = 1.0e-7)
    @assert isapprox(x[2], 2 * exp(1), rtol = 1.0e-7, atol = 1.0e-7)
    return true
end

function test_ode4(solver::Function)
    opt = OptionsODE(
        "ode1",
        OPT_RTOL => 1.0e-10,
        OPT_ATOL => 1.0e-10
    )
    t0 = 0; T = 1; x0 = [1, 2]
    rhs = Callable_Type(1.0)

    (t, x, retcode, stats) = solver(rhs, t0, T, x0, opt)
    @assert 1 == retcode
    @assert t == T
    @assert x0 == [1, 2]
    @assert isapprox(x[1], exp(1), rtol = 1.0e-7, atol = 1.0e-7)
    @assert isapprox(x[2], 2 * exp(1), rtol = 1.0e-7, atol = 1.0e-7)
    if haskey(stats, "step_predict")
        @assert isa(stats["step_predict"], Number)
    end
    return true
end

function test_massode1(solver::Function)
    mas = [ 2.0 1.0; 1.0 2.0]
    x1_exact = t -> 1.5 * exp(t / 3) - 0.5 * exp(t)
    x2_exact = t -> 1.5 * exp(t / 3) + 0.5 * exp(t)
    opt = OptionsODE(
        "massode1",
        OPT_RTOL => 1.0e-8,
        OPT_ATOL => 1.0e-8,
        OPT_MASSMATRIX => mas,
    )
    t0 = 0; T = 1; x0 = [1, 2]; rhs = (t, x) -> x
    (t, x, retcode, stats) = solver(rhs, t0, T, x0, opt)
    @assert 1 == retcode
    @assert isapprox(x[1], x1_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    @assert isapprox(x[2], x2_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    return true
end

function test_massode2(solver::Function)
    mas = BandedMatrix(5, 5, 1, 1, 0.0)
    setdiagonal!(mas, 0, 2); setdiagonal!(mas, 1, 1); setdiagonal!(mas, -1, 1)

    x1_exact = t -> 0.5 * (
        -3 * exp(t / 3) + 2 * exp(t / 2) - exp(t) +
            (2 + sqrt(3)) * exp((2 - sqrt(3)) * t) -
            (sqrt(3) - 2) * exp((2 + sqrt(3)) * t)
    )
    x3_exact = t -> -exp(t / 2) + (2 + sqrt(3)) * exp((2 - sqrt(3)) * t) -
        (sqrt(3) - 2) * exp((2 + sqrt(3)) * t)

    opt = OptionsODE(
        "massode2",
        OPT_RTOL => 1.0e-8,
        OPT_ATOL => 1.0e-8,
        OPT_MASSMATRIX => mas,
    )
    t0 = 0; T = 1; x0 = [1, 2, 3, 4, 5]; rhs = (t, x) -> x

    (t, x, retcode, stats) = solver(rhs, t0, T, x0, opt)
    @assert 1 == retcode
    @assert isapprox(x[1], x1_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    @assert isapprox(x[3], x3_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    return true
end

function test_massode3(solver::Function)
    mas = [2 1 ; 1 2 ]

    x5_exact = t -> -0.5 * exp(t / 3) * (exp(2 * t / 3) - 11)
    x6_exact = t -> 0.5 * exp(t / 3) * (exp(2 * t / 3) + 11)

    opt = OptionsODE(
        "massode3",
        OPT_RTOL => 1.0e-8,
        OPT_ATOL => 1.0e-8,
        OPT_M1 => 4,
        OPT_M2 => 2,
        OPT_MASSMATRIX => mas,
    )
    t0 = 0; T = 1; x0 = [1, 2, 3, 4, 5, 6]
    rhs = (t, x) -> [x[5], x[6]]

    (t, x, retcode, stats) = solver(rhs, t0, T, x0, opt)
    @assert 1 == retcode
    @assert isapprox(x[5], x5_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    @assert isapprox(x[6], x6_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    return true
end

function test_massode4(solver::Function)
    mas = BandedMatrix(3, 3, 1, 1, 0.0)
    setdiagonal!(mas, 0, 2); setdiagonal!(mas, 1, 1); setdiagonal!(mas, -1, 1)

    x4_exact = t -> 4 * exp(t) * (cosh(t / sqrt(2)) - sqrt(2) * sinh(t / sqrt(2)))

    opt = OptionsODE(
        "massode4",
        OPT_RTOL => 1.0e-10,
        OPT_ATOL => 1.0e-10,
        OPT_M1 => 2,
        OPT_M2 => 2,
        OPT_MASSMATRIX => mas,
    )
    t0 = 0; T = 1; x0 = [1, 2, 3, 4, 5]
    rhs = (t, x) -> [x[3], x[4], x[5]]

    (t, x, retcode, stats) = solver(rhs, t0, T, x0, opt)
    @assert 1 == retcode
    @assert isapprox(x[4], x4_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    return true
end

function test_jacode1(solver::Function)
    x1_exact = t -> exp(2 * t) / (2 - exp(2 * t))
    x2_exact = t -> 2 * sqrt(1.0 / (2 - exp(2 * t)))

    function myjac(t, x, J)
        @assert isa(J, Array{Float64})
        J[1, 1] = x[2]^2
        J[1, 2] = 2 * x[1] * x[2]
        J[2, 1] = x[2]
        return J[2, 2] = x[1]
    end

    opt = OptionsODE(
        "odejac1",
        OPT_RTOL => 1.0e-10,
        OPT_ATOL => 1.0e-10,
        OPT_JACOBIMATRIX => myjac,
    )
    t0 = 0; T = 0.2; x0 = [1, 2]
    (t, x, retcode, stats) = solver(
        (t, x) -> [x[1] * (x[2])^2, x[1] * x[2]],
        t0, T, x0, opt
    )
    @assert 1 == retcode
    @assert isapprox(x[1], x1_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    @assert isapprox(x[2], x2_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    return true
end

function test_jacode2(solver::Function)
    function myrhs(t, x)
        return [x[1], x[1] + x[2], x[2] + x[3], x[3] + x[4]]
    end

    function myjac(t, x, J)
        @assert isa(J, BandedMatrix{Float64})
        setdiagonal!(J, 0, 1.0)
        return setdiagonal!(J, -1, 1.0)
    end

    x1_exact = t -> exp(t)
    x2_exact = t -> (2 + t) * exp(t)
    x4_exact = t -> exp(t) / 6 * (24 + 18 * t + 6 * t^2 + t^3)

    opt = OptionsODE(
        "odejac2",
        OPT_RTOL => 1.0e-10,
        OPT_ATOL => 1.0e-10,
        OPT_JACOBIMATRIX => myjac,
        OPT_JACOBIBANDSTRUCT => (1, 0),
        OPT_JACRECOMPFACTOR => -1,
    )
    t0 = 0; T = 1; x0 = [1, 2, 3, 4]

    (t, x, retcode, stats) = solver(myrhs, t0, T, x0, opt)
    @assert 1 == retcode
    @assert isapprox(x[1], x1_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    @assert isapprox(x[2], x2_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    @assert isapprox(x[4], x4_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    return true
end

function test_jacode3(solver::Function)
    if solver ∈ solvers_without_special_struct_support
        return true
    end
    function myrhs(t, x)
        return [ # x[3], x[4],
            x[1] + x[2] + x[4],
            x[1] + x[2] + x[3],
        ]
    end

    function myjac(t, x, J)
        @assert isa(J, Array{Float64})
        @assert (2, 4) == size(J)
        J[1, 1] = 1; J[1, 2] = 1; J[1, 3] = 0; J[1, 4] = 1
        J[2, 1] = 1; J[2, 2] = 1; J[2, 3] = 1
        return J[2, 4] = 0
    end

    x1_exact = t -> exp(-t) / 3 * (1 - 3 * exp(t) + 5 * exp(3 * t))
    x4_exact = t -> 2 * exp(-t) / 3 * (1 + 5 * exp(3 * t))

    opt = OptionsODE(
        "odejac3",
        OPT_RTOL => 1.0e-8,
        OPT_ATOL => 1.0e-8,
        OPT_M1 => 2,
        OPT_M2 => 2,
        OPT_JACOBIMATRIX => myjac,
        OPT_JACOBIBANDSTRUCT => nothing,
        OPT_JACRECOMPFACTOR => -1,
    )
    t0 = 0; T = 1; x0 = [1, 2, 3, 4]

    (t, x, retcode, stats) = solver(myrhs, t0, T, x0, opt)
    @assert 1 == retcode
    @assert isapprox(x[1], x1_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    @assert isapprox(x[4], x4_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    return true
end

function test_jacode4(solver::Function)
    if solver ∈ solvers_without_special_struct_support
        return true
    end
    function myrhs(t, x)
        return [ # x[3], x[4],
            2 * x[1] + 2 * x[3],
            x[2] + x[4],
        ]
    end

    function myjac(t, x, J1, J2)
        @assert isa(J1, BandedMatrix{Float64}) && isa(J2, BandedMatrix{Float64})
        @assert (2, 2) == size(J1) && (2, 2) == size(J2)
        setdiagonal!(J1, 0, [2, 1])
        return setdiagonal!(J2, 0, [2, 2])
    end

    hp = 1 + sqrt(3); hm = 1 - sqrt(3)

    x3_exact = t -> (
        -5 * exp(hm * t) + 3 * sqrt(3) * exp(hm * t) +
            5 * exp(hp * t) + 3 * sqrt(3) * exp(hp * t)
    ) / (2 * sqrt(3))

    opt = OptionsODE(
        "odejac3",
        OPT_RTOL => 1.0e-8,
        OPT_ATOL => 1.0e-8,
        OPT_M1 => 2,
        OPT_M2 => 2,
        OPT_JACOBIMATRIX => myjac,
        OPT_JACOBIBANDSTRUCT => (0, 0),
        OPT_JACRECOMPFACTOR => -1,
    )
    t0 = 0; T = 1; x0 = [1, 2, 3, 4]

    (t, x, retcode, stats) = solver(myrhs, t0, T, x0, opt)
    @assert 1 == retcode
    @assert isapprox(x[3], x3_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    return true
end

function test_rhstimederiv1(solver::Function)
    myrhs = (t, x) -> [t * x[2], 4 * t * x[1]]

    function myjac(t, x, J)
        @assert isa(J, Array{Float64})
        @assert (2, 2) == size(J)
        J[1, 1] = 0; J[1, 2] = t
        J[2, 1] = 4 * t; J[2, 2] = 0
        return nothing
    end

    function myrhstimederiv(t, x, drhsdt)
        @assert isa(drhsdt, Array{Float64})
        @assert (2,) == size(drhsdt)
        drhsdt[1] = x[2]
        drhsdt[2] = 4 * x[1]
        return nothing
    end

    x1_exact = t -> cosh(t * t) + 0.5 * sinh(t * t)
    x2_exact = t -> cosh(t * t) + 2.0 * sinh(t * t)

    opt = OptionsODE(
        "odetimederiv1",
        OPT_RTOL => 1.0e-8,
        OPT_ATOL => 1.0e-8,
        OPT_JACOBIMATRIX => myjac,
        OPT_RHSTIMEDERIV => myrhstimederiv,
    )
    t0 = 0; T = 1; x0 = [1.0, 1]

    (t, x, retcode, stats) = solver(myrhs, t0, T, x0, opt)
    @assert 1 == retcode
    @assert isapprox(x[1], x1_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    @assert isapprox(x[2], x2_exact(T), rtol = 1.0e-7, atol = 1.0e-7)
    return true
end

function test_odecall1(solver::Function)
    opt = OptionsODE(
        "odecall1",
        OPT_RTOL => 1.0e-8,
        OPT_ATOL => 1.0e-8
    )
    t = mylinspace(0, 1, 10)
    x0 = [1, 2]; rhs = (t, x) -> x
    (tVec, xVec, retcode, stats) = odecall(solver, rhs, t, x0, opt)
    @assert 1 == retcode
    @assert t == tVec
    return true
end

function test_odecall2(solver::Function)
    opt = OptionsODE(
        "odecall2",
        OPT_RTOL => 1.0e-8,
        OPT_ATOL => 1.0e-8
    )
    t = [0, 1]
    x0 = [1, 2]; rhs = (t, x) -> x
    (tVec, xVec, retcode, stats) = odecall(solver, rhs, t, x0, opt)
    @assert 1 == retcode
    @assert length(tVec) > 2
    return true
end

function test_bvp1(solver::Function)
    ivpopt = OptionsODE(
        "ivpoptions",
        OPT_RHS_CALLMODE => RHS_CALL_INSITU
    )

    opt = OptionsODE(
        "bvp1",
        OPT_RHS_CALLMODE => RHS_CALL_INSITU,
        OPT_MAXSTEPS => 10,
        OPT_RTOL => 1.0e-6,
        OPT_BVPCLASS => 0,
        OPT_SOLMETHOD => 0,
        OPT_IVPOPT => ivpopt
    )

    tNodes = [0, 5]
    xInit = [ 5.0  0.45938665265299; 1  1]
    odesolver = nothing

    function f(t, x, dx)
        dx[1] = x[2]
        dx[2] = -x[1]
        return nothing
    end

    function bc(xa, xb, r)
        r[1] = xa[1] - 5
        r[2] = xb[1] - 0.45938665265299
        return nothing
    end

    (t, x, code, stats) = solver(f, bc, tNodes, xInit, odesolver, opt)
    @assert tNodes == [0, 5]
    @assert code > 0
    @assert t == tNodes
    @assert isapprox(x[1, 1], 5.0, rtol = 1.0e-4, atol = 1.0e-4)
    @assert isapprox(x[2, 1], 1.0, rtol = 1.0e-4, atol = 1.0e-4)
    @assert isapprox(x[1, 2], 0.459387, rtol = 1.0e-4, atol = 1.0e-4)
    @assert isapprox(x[2, 2], 5.07828, rtol = 1.0e-4, atol = 1.0e-4)

    return true
end

function test_bvp2(solver::Function)
    ivpopt = OptionsODE(
        "ivpoptions",
        OPT_RHS_CALLMODE => RHS_CALL_INSITU
    )

    opt = OptionsODE(
        "bvp2",
        OPT_RHS_CALLMODE => RHS_CALL_INSITU,
        OPT_MAXSTEPS => 10,
        OPT_RTOL => 1.0e-6,
        OPT_BVPCLASS => 0,
        OPT_SOLMETHOD => 0,
        OPT_IVPOPT => ivpopt
    )

    tNodes = [0, 5]
    xInit = [ 5.0  0.45938665265299; 1  1]
    odesolver = dop853

    function f(t, x, dx)
        dx[1] = x[2]
        dx[2] = -x[1]
        return nothing
    end

    function bc(xa, xb, r)
        r[1] = xa[1] - 5
        r[2] = xb[1] - 0.45938665265299
        return nothing
    end

    (t, x, code, stats) = solver(f, bc, tNodes, xInit, odesolver, opt)
    @assert tNodes == [0, 5]
    @assert code > 0
    @assert t == tNodes
    @assert isapprox(x[1, 1], 5.0, rtol = 1.0e-4, atol = 1.0e-4)
    @assert isapprox(x[2, 1], 1.0, rtol = 1.0e-4, atol = 1.0e-4)
    @assert isapprox(x[1, 2], 0.459387, rtol = 1.0e-4, atol = 1.0e-4)
    @assert isapprox(x[2, 2], 5.07828, rtol = 1.0e-4, atol = 1.0e-4)

    return true
end

function test_bvp3(solver::Function)
    ivpopt = OptionsODE(
        "ivpoptions",
        OPT_RHS_CALLMODE => RHS_CALL_INSITU
    )

    opt = OptionsODE(
        "bvp3",
        OPT_RHS_CALLMODE => RHS_CALL_INSITU,
        OPT_MAXSTEPS => 100,
        OPT_RTOL => 1.0e-6,
        OPT_BVPCLASS => 2,
        OPT_SOLMETHOD => 1,
        OPT_IVPOPT => ivpopt
    )

    tNodes = mylinspace(0, 5, 11)
    xInit = [ones(1, length(tNodes) - 1) 0 ; ones(1, length(tNodes)) ]
    odesolver = dop853

    function f(t, x, dx)
        dx[1] = t * x[2]
        dx[2] = 4 * max(0, x[1])^1.5
        return nothing
    end

    function bc(xa, xb, r)
        r[1] = xa[1] - 1
        r[2] = xb[1] - 0
        return nothing
    end

    (t, x, code, stats) = solver(f, bc, tNodes, xInit, odesolver, opt)
    @assert code > 0
    @assert isapprox(x[1, 1], 1, rtol = 1.0e-4, atol = 1.0e-4)
    @assert isapprox(x[2, 1], -3.17614, rtol = 1.0e-4, atol = 1.0e-4)
    @assert isapprox(x[1, 2], 0.755201, rtol = 1.0e-4, atol = 1.0e-4)

    return true
end

function test_colnew1(solver::Function)
    a, b = -pi / 2, pi / 2
    orders = [1, 1]
    ζ = [a, b]

    ε = nothing
    ε_old = nothing
    sol_old = nothing

    function rhs(x, z, f)
        s² = sin(x)^2
        f[1] = (s² - z[2] * s² * s² / z[1]) / ε
        return f[2] = 0.0
    end

    function Drhs(x, z, df)
        df[:] .= 0.0
        s⁴ = sin(x)^4
        df[1, 1] = z[2] * s⁴ / (z[1]^2)
        return df[1, 2] = -s⁴ / z[1]
    end

    function bc(i, z, bc)
        return bc[1] = z[1] - 1.0
    end

    function Dbc(i, z, dbc)
        dbc[1] = 1.0
        return dbc[2] = 0.0
    end

    function initial_guess(x, z, dmz)
        z[1] = 0.5
        z[2] = 1.0
        return rhs(x, z, dmz)
    end

    opt = OptionsODE(
        "colnew1",
        OPT_BVPCLASS => 2, OPT_COLLOCATIONPTS => 7,
        OPT_RTOL => [1.0e-4, 1.0e-4], OPT_MAXSUBINTERVALS => 200
    )

    sol = nothing
    for ε_new in [1.0, 0.5, 0.2, 0.1]
        ε = ε_new
        guess = sol_old !== nothing ? sol_old : initial_guess
        sol, retcode, stats = colnew(
            [a, b], orders, ζ, rhs, Drhs, bc, Dbc,
            guess, opt
        )
        @assert retcode > 0
        sol_old = sol; ε_old = ε
    end

    z₀ = evalSolution(sol, 0.0)
    @assert isapprox(z₀[1], 0.161671, rtol = 1.0e-3, atol = 1.0e-3)
    @assert isapprox(z₀[2], 1.01863, rtol = 1.0e-3, atol = 1.0e-3)
    return true
end

function test_bvpm2_1()
    ε = 0.1
    a, b = -pi / 2.0, pi / 2.0

    function rhs(x, y, p, f)
        @assert length(y) == length(f) == 1
        @assert length(p) == 1
        return f[1] = (sin(x)^2 - p[1] * sin(x)^4 / y[1]) / ε
    end

    function Drhs(x, y, p, dfdy, dfdp)
        @assert length(y) == 1
        @assert length(p) == 1
        @assert size(dfdy) == (1, 1)
        @assert size(dfdp) == (1, 1)

        dfdy[1, 1] = (p[1] * sin(x)^4 / y[1]^2) / ε
        return dfdp[1, 1] = (-sin(x)^4 / y[1]) / ε
    end

    function bc(ya, yb, p, bca, bcb)
        @assert length(ya) == length(yb) == 1
        @assert length(bca) == length(bcb) == 1
        @assert length(p) == 1
        bca[1] = ya[1] - 1.0
        return bcb[1] = yb[1] - 1.0
    end

    function Dbc(ya, yb, dya, dyb, p, dpa, dpb)
        @assert length(ya) == length(yb) == 1
        @assert size(dya) == size(dyb) == (1, 1)
        @assert length(p) == 1
        @assert size(dpa) == size(dpb) == (1, 1)
        dya[1, 1] = 1.0
        dyb[1, 1] = 1.0
        dpa[1, 1] = 0.0
        return dpb[1, 1] = 0.0
    end

    opt = OptionsODE(
        "test_bvpm2_1",
        OPT_RTOL => 1.0e-6,
        OPT_METHODCHOICE => 4,
    )
    guess_obj = Bvpm2()
    bvpm2_init(guess_obj, 1, 1, mylinspace(a, b, 20), [0.5], [1.0])
    retcode = Vector{Int64}(undef, 4)
    sol = Vector{ODEInterface.Bvpm2}(undef, 4)
    stat = Vector{Dict}(undef, 4)
    z = Vector{Matrix}(undef, 4)

    (sol[1], retcode[1], stat[1]) = bvpm2_solve(guess_obj, rhs, bc, opt)
    (sol[2], retcode[2], stat[2]) = bvpm2_solve(
        guess_obj, rhs, bc, opt,
        Drhs = Drhs
    )
    (sol[3], retcode[3], stat[3]) = bvpm2_solve(
        guess_obj, rhs, bc, opt,
        Dbc = Dbc
    )
    (sol[4], retcode[4], stat[4]) = bvpm2_solve(
        guess_obj, rhs, bc, opt,
        Drhs = Drhs, Dbc = Dbc
    )
    for k in 1:length(retcode)
        @assert retcode[k] == 0
        @assert stat[k]["no_rhs_calls"] > 0
        @assert stat[k]["no_bc_calls"] > 0
        z[k] = evalSolution(sol[k], mylinspace(a, b, 5))
    end
    @assert stat[1]["no_jac_calls"] == stat[3]["no_jac_calls"] == 0
    @assert stat[1]["no_Dbc_calls"] == stat[2]["no_Dbc_calls"] == 0
    @assert stat[2]["no_jac_calls"] > 0
    @assert stat[4]["no_jac_calls"] > 0
    @assert stat[1]["no_rhs_calls"] > stat[2]["no_rhs_calls"]
    @assert stat[1]["no_rhs_calls"] > stat[4]["no_rhs_calls"]

    for k in 1:(length(retcode) - 1)
        @assert isapprox(z[k], z[k + 1], rtol = 1.0e-4, atol = 1.0e-4)
    end

    bvpm2_destroy(guess_obj)
    for k in 1:length(retcode)
        bvpm2_destroy(sol[k])
    end
    return true
end

function test_bvpm2_2()
    exact_sol = x -> [1 / sqrt(1 + x^2 / 3), -9 / (9 + 3 * x^2)^1.5 * x]
    a, b = 0.0, 1.0

    function rhs(x, y, f)
        @assert length(y) == length(f) == 2
        f[1] = y[2]
        return f[2] = -y[1]^5
    end

    function Drhs(x, y, dfdy)
        @assert length(y) == 2
        @assert size(dfdy) == (2, 2)
        dfdy[:] .= 0
        dfdy[1, 2] = 1.0
        return dfdy[2, 1] = -5 * y[1]^4
    end

    function bc(ya, yb, bca, bcb)
        @assert length(ya) == length(yb) == 2
        @assert length(bca) == length(bcb) == 1
        bca[1] = ya[2]
        return bcb[1] = yb[1] - sqrt(0.75)
    end

    function Dbc(ya, yb, dya, dyb)
        @assert length(ya) == length(yb) == 2
        @assert size(dya) == (1, 2)
        @assert size(dyb) == (1, 2)
        dya[:] .= 0
        dya[1, 2] = 1.0
        dyb[:] .= 0
        return dyb[1, 1] = 1.0
    end

    opt = OptionsODE(
        "test_bvpm2_2",
        OPT_RTOL => 1.0e-6,
        OPT_METHODCHOICE => 6,
        OPT_SINGULARTERM => [ 0 0 ; 0 -2 ],
    )

    guess_obj = Bvpm2()
    bvpm2_init(guess_obj, 2, 1, mylinspace(a, b, 10), [sqrt(0.75), 1.0e-4])
    retcode = Vector{Int64}(undef, 4)
    sol = Vector{ODEInterface.Bvpm2}(undef, 4)
    stat = Vector{Dict}(undef, 4)
    z = Vector{Matrix}(undef, 4)

    (sol[1], retcode[1], stat[1]) = bvpm2_solve(guess_obj, rhs, bc, opt)
    (sol[2], retcode[2], stat[2]) = bvpm2_solve(
        guess_obj, rhs, bc, opt,
        Drhs = Drhs
    )
    (sol[3], retcode[3], stat[3]) = bvpm2_solve(
        guess_obj, rhs, bc, opt,
        Dbc = Dbc
    )
    (sol[4], retcode[4], stat[4]) = bvpm2_solve(
        guess_obj, rhs, bc, opt,
        Drhs = Drhs, Dbc = Dbc
    )
    xx = mylinspace(a, b, 5)
    for k in 1:length(retcode)
        @assert retcode[k] == 0
        @assert stat[k]["no_rhs_calls"] > 0
        @assert stat[k]["no_bc_calls"] > 0
        z[k] = evalSolution(sol[k], xx)
    end
    @assert stat[1]["no_jac_calls"] == stat[3]["no_jac_calls"] == 0
    @assert stat[1]["no_Dbc_calls"] == stat[2]["no_Dbc_calls"] == 0
    @assert stat[2]["no_jac_calls"] > 0
    @assert stat[4]["no_jac_calls"] > 0
    @assert stat[1]["no_rhs_calls"] > stat[2]["no_rhs_calls"]
    @assert stat[1]["no_rhs_calls"] > stat[4]["no_rhs_calls"]

    for k in 1:length(retcode)
        for j in 1:length(xx)
            @assert isapprox(z[k][:, j], exact_sol(xx[j]), rtol = 1.0e-4, atol = 1.0e-4)
        end
    end

    bvpm2_destroy(guess_obj)
    for k in 1:length(retcode)
        bvpm2_destroy(sol[k])
    end
    return true
end

function test_bvpm2_3()
    exact_sol = x -> [exp(x), exp(x)]
    a, b = 0.0, 1.0

    function rhs(x, y, f)
        @assert length(y) == length(f) == 2
        f[1] = y[2]
        return f[2] = y[1]
    end

    function Drhs(x, y, dfdy)
        @assert length(y) == 2
        @assert size(dfdy) == (2, 2)
        dfdy[:] .= 0
        dfdy[1, 2] = 1.0
        return dfdy[2, 1] = 1.0
    end

    function bc(ya, yb, bca, bcb)
        @assert length(ya) == length(yb) == 2
        @assert length(bca) == length(bcb) == 1
        bca[1] = ya[1] - 1.0
        return bcb[1] = yb[1] - exp(1)
    end

    function Dbc(ya, yb, dya, dyb)
        @assert length(ya) == length(yb) == 2
        @assert size(dya) == (1, 2)
        @assert size(dyb) == (1, 2)
        dya[:] .= 0
        dya[1, 1] = 1.0
        dyb[:] .= 0
        return dyb[1, 1] = 1.0
    end

    function guess(x, y)
        @assert length(y) == 2
        y[1] = 1 + x
        return y[2] = 1.0
    end

    opt = OptionsODE(
        "test_bvpm2_3",
        OPT_RTOL => 1.0e-6
    )

    guess_obj = Bvpm2()
    bvpm2_init(guess_obj, 2, 1, mylinspace(a, b, 3), guess)
    retcode = Vector{Int64}(undef, 4)
    sol = Vector{ODEInterface.Bvpm2}(undef, 4)
    stat = Vector{Dict}(undef, 4)
    z = Vector{Matrix}(undef, 4)

    (sol[1], retcode[1], stat[1]) = bvpm2_solve(guess_obj, rhs, bc, opt)
    (sol[2], retcode[2], stat[2]) = bvpm2_solve(
        guess_obj, rhs, bc, opt,
        Drhs = Drhs
    )
    (sol[3], retcode[3], stat[3]) = bvpm2_solve(
        guess_obj, rhs, bc, opt,
        Dbc = Dbc
    )
    (sol[4], retcode[4], stat[4]) = bvpm2_solve(
        guess_obj, rhs, bc, opt,
        Drhs = Drhs, Dbc = Dbc
    )
    xx = mylinspace(a, b, 5)
    for k in 1:length(retcode)
        @assert retcode[k] == 0
        @assert stat[k]["no_rhs_calls"] > 0
        @assert stat[k]["no_bc_calls"] > 0
        z[k] = evalSolution(sol[k], xx)
    end
    @assert stat[1]["no_jac_calls"] == stat[3]["no_jac_calls"] == 0
    @assert stat[1]["no_Dbc_calls"] == stat[2]["no_Dbc_calls"] == 0
    @assert stat[2]["no_jac_calls"] > 0
    @assert stat[4]["no_jac_calls"] > 0
    @assert stat[1]["no_rhs_calls"] > stat[2]["no_rhs_calls"]
    @assert stat[1]["no_rhs_calls"] > stat[4]["no_rhs_calls"]

    for k in 1:length(retcode)
        for j in 1:length(xx)
            @assert isapprox(z[k][:, j], exact_sol(xx[j]), rtol = 1.0e-4, atol = 1.0e-4)
        end
    end

    bvpm2_destroy(guess_obj)
    for k in 1:length(retcode)
        bvpm2_destroy(sol[k])
    end
    return true
end

function test_bvpm2_4()
    exact_sol = x -> [exp(x), exp(x)]
    a, b = 0.0, 1.0

    function rhs(x, y, p, f)
        @assert length(y) == length(f) == 2
        @assert length(p) == 1
        f[1] = y[2]
        return f[2] = y[1] * p[1]
    end

    function Drhs(x, y, p, dfdy, dfdp)
        @assert length(y) == 2
        @assert length(p) == 1
        @assert size(dfdy) == (2, 2)
        @assert size(dfdp) == (2, 1)
        dfdy[:] .= 0
        dfdy[1, 2] = 1.0
        dfdy[2, 1] = p[1]

        dfdp[:] .= 0
        return dfdp[2, 1] = y[1]
    end

    function bc(ya, yb, p, bca, bcb)
        @assert length(ya) == length(yb) == 2
        @assert length(bca) == 2
        @assert length(bcb) == 1
        @assert length(p) == 1
        bca[1] = ya[1] - 1.0
        bca[2] = ya[2] - 1.0
        return bcb[1] = yb[1] - exp(1)
    end

    function Dbc(ya, yb, dya, dyb, p, dpa, dpb)
        @assert length(ya) == length(yb) == 2
        @assert length(p) == 1
        @assert size(dya) == (2, 2)
        @assert size(dyb) == (1, 2)
        @assert size(dpa) == (2, 1)
        @assert size(dpb) == (1, 1)
        dya[:] .= 0
        dya[1, 1] = 1.0
        dya[2, 2] = 1.0
        dyb[:] .= 0
        dyb[1, 1] = 1.0
        return dpa[:] .= 0
    end

    function guess(x, y)
        y[1] = 1 + x
        return y[2] = 1.0
    end

    opt = OptionsODE(
        "test_bvpm2_4",
        OPT_RTOL => 1.0e-6
    )

    guess_obj = Bvpm2()
    bvpm2_init(guess_obj, 2, 2, mylinspace(a, b, 3), guess, [0.9])
    retcode = Vector{Int64}(undef, 4)
    sol = Vector{ODEInterface.Bvpm2}(undef, 4)
    stat = Vector{Dict}(undef, 4)
    z = Vector{Matrix}(undef, 4)

    (sol[1], retcode[1], stat[1]) = bvpm2_solve(guess_obj, rhs, bc, opt)
    (sol[2], retcode[2], stat[2]) = bvpm2_solve(
        guess_obj, rhs, bc, opt,
        Drhs = Drhs
    )
    (sol[3], retcode[3], stat[3]) = bvpm2_solve(
        guess_obj, rhs, bc, opt,
        Dbc = Dbc
    )
    (sol[4], retcode[4], stat[4]) = bvpm2_solve(
        guess_obj, rhs, bc, opt,
        Drhs = Drhs, Dbc = Dbc
    )
    xx = mylinspace(a, b, 5)
    for k in 1:length(retcode)
        @assert retcode[k] == 0
        @assert stat[k]["no_rhs_calls"] > 0
        @assert stat[k]["no_bc_calls"] > 0
        z[k] = evalSolution(sol[k], xx)
    end
    @assert stat[1]["no_jac_calls"] == stat[3]["no_jac_calls"] == 0
    @assert stat[1]["no_Dbc_calls"] == stat[2]["no_Dbc_calls"] == 0
    @assert stat[2]["no_jac_calls"] > 0
    @assert stat[4]["no_jac_calls"] > 0
    @assert stat[1]["no_rhs_calls"] > stat[2]["no_rhs_calls"]
    @assert stat[1]["no_rhs_calls"] > stat[4]["no_rhs_calls"]

    for k in 1:length(retcode)
        for j in 1:length(xx)
            @assert isapprox(z[k][:, j], exact_sol(xx[j]), rtol = 1.0e-4, atol = 1.0e-4)
        end
        @assert isapprox(bvpm2_get_params(sol[k]), [1.0], rtol = 1.0e-4, atol = 1.0e-4)
    end

    bvpm2_destroy(guess_obj)
    for k in 1:length(retcode)
        bvpm2_destroy(sol[k])
    end
    return true
end

function test_Banded()
    return @testset "Banded" begin
        @test_throws ArgumentErrorODE  BandedMatrix{Float64}(
            5, 4, 1, 2, zeros(Float64, (4, 5))
        )
        @test_throws ArgumentErrorODE  BandedMatrix{Float64}(
            5, 4, 1, 2, zeros(Float64, (5, 4))
        )
        @test_throws ArgumentErrorODE  BandedMatrix{Float64}(
            5, 4, 5, 1, zeros(Float64, (7, 5))
        )

        bm = BandedMatrix(5, 4, 1, 2, NaN)
        @test   (bm[1, 1] = 1.0) == 1.0
        @test   (bm[2, 1] = 2) == 2.0
        @test   (bm[2, 4] = 7) == 7.0
        @test   (bm[3, 2] = 8) == 8.0
        @test_throws BoundsError  bm[1, 4] = 1.1
        @test_throws BoundsError  bm[3, 1] = 1.1
        @test_throws BoundsError  bm[4, 1] = 1.1
        @test_throws BoundsError  bm[4, 2] = 1.1
        @test_throws BoundsError  bm[5, 3] = 1.1

        bm = BandedMatrix(5, 4, 1, 2, NaN)
        @test (bm[1:2, 1] = [1, 5]) == [1, 5]
        @test (bm[1:3, 2] = [4, 2, 4]) == [4, 2, 4]
        @test (bm[1:4, 3] = [2, 3, 3, 3]) == [2, 3, 3, 3]
        @test (bm[2:5, 4] = [1, 0, 4, 2]) == [1, 0, 4, 2]
        @test full(bm) == [1 4 2 0; 5 2 3 1; 0 4 3 0; 0 0 3 4; 0 0 0 2]
        bm_test = createBandedMatrix(
            [1.0 4 2 0; 5.0 2 3 1; 0 4.0 3 0; 0 0 3 4.0; 0 0 0 2.0]
        )
        @test bm_test == bm

        bm = BandedMatrix(5, 4, 1, 2, NaN)
        @test setdiagonal!(bm, 2, [2, 1]) == [2, 1]
        @test setdiagonal!(bm, 1, [4, 3, 0]) == [4, 3, 0]
        @test setdiagonal!(bm, 0, [1, 2, 3, 4]) == [1, 2, 3, 4]
        @test setdiagonal!(bm, -1, [5, 4, 3, 2]) == [5, 4, 3, 2]
        @test full(bm) == [1 4 2 0; 5 2 3 1; 0 4 3 0; 0 0 3 4; 0 0 0 2]

        bm = BandedMatrix(5, 4, 1, 2, NaN)
        diagonals = Any[[2 1], [4 3 0], [1, 2, 3, 4], [5 4 3 2]]
        @test setdiagonals!(bm, diagonals) == diagonals
        @test full(bm) == [1 4 2 0; 5 2 3 1; 0 4 3 0; 0 0 3 4; 0 0 0 2]

        bm2 = BandedMatrix(5, 4, 1, 2, NaN)
        setdiagonals!(bm2, bm)
        @test bm ≢ bm2
        @test bm == bm2

        buffer = IOBuffer()
        show(IOContext(buffer, :limit => true), MIME"text/plain"(), bm)
        shown = String(take!(buffer))
        @test occursin("BandedMatrix", shown)
        @test occursin("5×4", shown)
    end
end

function test_Options()
    return @testset "Options" begin
        opt1 = OptionsODE("test1")
        @test isa(opt1, OptionsODE)
        @test isa(opt1, ODEInterface.AbstractOptionsODE)
        @test setOption!(opt1, "test_key", 56) === nothing
        @test setOption!(opt1, "test_key", 82) == 56
        @test getOption(opt1, "test_key", 0) == 82
        @test getOption(opt1, "nokey", nothing) === nothing
        @test getOption(opt1, "nokey", "none") == "none"
        @test setOptions!(opt1, "test_key" => 100, "new_key" => "bla") ==
            [82, nothing]

        opt2 = OptionsODE("test2", opt1)
        @test getOption(opt1, "test_key", 0) == 100
    end
end

function test_DLSolvers()
    result = nothing
    @testset "DLSolvers" begin
        result = loadODESolvers()
        @testset for dl in dl_solvers
            @test result[dl].error === nothing
            @test result[dl].libhandle ≠ C_NULL
        end

        @testset for dl in dl_solvers
            @testset for method in result[dl].methods
                @test method.error === nothing
                @test method.method_ptr ≠ C_NULL
                @test method.generic_name ≠ ""
                @test method.methodname_found ≠ ""
            end
        end
    end
    return result
end

function test_vanilla()
    problems = (test_ode1, test_ode2, test_ode3, test_ode4)
    return @testset "solvers" begin
        @testset for solver in solvers,
                problem in problems
            @test problem(solver)
        end
    end
end

function test_mas_solvers()
    problems = (test_massode1, test_massode2, test_massode3, test_massode4)
    return @testset "mas-solvers" begin
        @testset for solver in solvers_mas,
                problem in problems
            @test problem(solver)
        end
    end
end

function test_jac_solvers()
    problems = (test_jacode1, test_jacode2, test_jacode3, test_jacode4)
    return @testset "jac-solvers" begin
        @testset for solver in solvers_jac,
                problem in problems
            @test problem(solver)
        end
    end
end

function test_rhsdt_solvers()
    problems = (test_rhstimederiv1,)
    return @testset "rhs_dt-sol." begin
        @testset for solver in solvers_rhsdt,
                problem in problems
            @test problem(solver)
        end
    end
end

function test_solvers()
    test_vanilla()
    test_mas_solvers()
    test_jac_solvers()
    return test_rhsdt_solvers()
end

function test_odecall()
    problems = (test_odecall1, test_odecall2)
    return @testset "odecall" begin
        @testset for solver in solvers,
                problem in problems
            @test problem(solver)
        end
    end
end

function test_bvp()
    problems = (test_bvp1, test_bvp2, test_bvp3)
    return @testset "bvpsol" begin
        @testset for solver in solvers_bvpsol,
                problem in problems
            @test problem(solver)
        end
    end
end

function test_colnew()
    problems = (test_colnew1,)
    return @testset "colnew" begin
        @testset for solver in solvers_colnew,
                problem in problems
            @test problem(solver)
        end
    end
end

function test_bvpm2()
    problems = (test_bvpm2_1, test_bvpm2_2, test_bvpm2_3, test_bvpm2_4)
    return @testset "bvpm2" begin
        @testset for problem in problems
            @test problem()
        end
    end
end

function test_all()
    test_Banded()
    test_Options()
    solvers = test_DLSolvers()
    test_solvers()
    test_odecall()
    test_bvp()
    test_colnew()
    test_bvpm2()

    println("Solver information:")
    for info in values(solvers)
        println(info.libname, ": ", info.libhandle, "; ", info.libfilepath)
    end
    return
end

test_all()

include("public_api_tests.jl")

end

# vim:syn=julia:cc=79:fdm=indent:
