@testset "Simple tests" begin
    # Default
    rts = roots(sin, interval(-5, 5))
    @test length(rts) == 3
    @test all(isunique, rts)

    # Bisection
    rts = roots(sin, interval(-5, 6) ; contractor = Bisection, abstol = 1e-3)
    @test length(rts) == 3

    @testset for contractor in newtonlike_methods
        # Refinement
        rts = roots(sin, interval(-5, 6) ; contractor = Bisection, abstol = 1e-3)
        for rt in rts
            refined = roots(sin, rt ; contractor)
            @test length(refined) == 1
            @test isunique(only(refined))
        end

        test_newtonlike(sin, cos, interval(-5, 5), contractor, 3)

        # abs
        g(p) = abs(1 / ( (1+p)^30 ) * 10_000 - 100)
        g2(p) = (1 / ( (1+p)^30 ) * 10_000 - 100)^2
        X = interval(-1000, 1000)
        rts = roots(g, X ; contractor)
        rts2 = roots(g2, X ; contractor)
        rr = intersect_interval.(root_region.(rts), root_region.(rts2))
        @test !any(isempty(rr))

        # Hard problem with a known zero at zero
        h(E) = tan(√(2E)) + √(E / (3π^2 - E))
        rts = roots(h, interval(-1e100, 1e100) ; contractor)
        @test any(rts) do rt
            in_interval(0, root_region(rt))
        end

        # Singularity
        s(x) = x + zero(x)/x
        rts = roots(s, interval(-1, 1) ; contractor)
        @test length(rts) == 1
        @test root_status(only(rts)) == :unknown
        @test in_interval(0, root_region(only(rts)))

        # Double reciprocal from #131
        rr(x) = 1/(1/(1 + x)*(1 - x))
        rts = roots(rr, interval(-10, 10) ; contractor)
        @test length(rts) == 2
        @test all(root_status(rt) == :unknown for rt in rts)
        @test in_interval(-1, root_region(rts[1]))
        @test in_interval(1, root_region(rts[2]))
    end
end

@testset "Multiple roots" begin
    f(x) = (x-1) * (x^2 - 2)^3 * (x^3 - 2)^4
    g(x) = exp(x) - x - 1  # Double root at 0 from Burden & Faires, 9th ed, p.84

    for contractor in newtonlike_methods
        rts = roots(f, interval(-5, 5) ; contractor)

        @test length(rts) == 4
        @test rts[1].status == :unknown
        @test in_interval(-sqrt(2), rts[1].region)

        @test rts[2].status == :unique
        @test in_interval(1, rts[2].region)

        @test rts[3].status == :unknown
        @test in_interval(cbrt(2), rts[3].region)

        @test rts[4].status == :unknown
        @test in_interval(sqrt(2), rts[4].region)

        rts = roots(g, interval(-5, 5) ; contractor)
        @test all(rt.status == :unknown for rt in rts)
        @test in_interval(0, hull([rt.region for rt in rts]...))
    end
end
@testset "Out of domain" begin
    s(x) = sqrt(x^2 - 4) - 2  # root: -6 and 6
    a(xy) = [asin(xy[1]), acos(xy[2]) - π/2]  # root: (0, 0)

    for contractor in newtonlike_methods
        @suppress @test length(roots(log, interval(-100, 2) ; contractor)) == 1
        @suppress @test length(roots(log, interval(-100, -1) ; contractor)) == 0

        @suppress @test length(roots(s, interval(-20, 20) ; contractor)) == 2
        @suppress @test length(roots(s, interval(0, 20) ; contractor)) == 1
        @suppress @test length(roots(s, interval(-1, 1) ; contractor))  == 0

        @suppress @test length(roots(a, fill(interval(-20, 20), 2) ; contractor)) == 1
        @suppress @test length(roots(a, fill(interval(0.5, 20), 2) ; contractor)) == 0
    end
end

@testset "Infinite domain" begin
    # Infinite interval
    f(xy) = [sin(1/xy[1]), xy[1]*xy[2]]  # largest roots: [1/π, 0] and [infinity, 0]

    for contractor in newtonlike_methods
        rts = @suppress roots(x -> x^2 - 2, interval(-Inf, Inf) ; contractor)
        @test length(rts) == 2
        @test all(isbounded, [rt.region for rt in rts])
        @test all(rt.region.decoration == dac for rt in rts)
        @test all(isunique, rts)

        rts = @suppress roots(f, [interval(0.9/π, Inf), interval(-Inf, Inf)] ; contractor)
        @test length(rts) == 2
        sort!(rts, by = mig)
        @test isunique(rts[1])
        @test all(isbounded, rts[1].region)
        @test minimum(X.decoration for X in rts[1].region) == dac

        @test !isunique(rts[2])
        @test !all(isbounded, rts[2].region)
        @test minimum(X.decoration for X in rts[2].region) == dac
    end
end

@testset "NaN return value" begin
    f1(x) = x^2/x - 1
    f(xy) = [log(xy[2]/xy[1]) + 3xy[1], xy[2] - 2xy[1]]
    X = [interval(-100, 100), interval(-100, 100)]

    for contractor in newtonlike_methods
        rts = roots(f1, interval(-2, 2) ; contractor)
        @test length(rts) == 2
        @test !isunique(rts[1])
        @test in_interval(0, root_region(rts[1]))
        @test isunique(rts[2])

        rts = roots(f, X ; contractor)
        @test length(filter(isunique, rts)) == 1
        @test length(filter(x -> all(in_interval.(0, x)), root_region.(rts))) == 1
    end
end

@testset "Root at infinity" begin
    for contractor in newtonlike_methods
        pb = RootProblem(x -> 1/x, interval(1, Inf) ; contractor)

        state = nothing
        for (k, s) in enumerate(pb)
            state = s
            
            k > 1000 && break
        end

        rts = [leaf.region for leaf in BranchAndPrune.Leaves(state.tree)]
        for rt in rts
            # Some roots have an empty trivial interval for unknown reason,
            # which is unhelpful but not incorrect
            if decoration(rt.region) != trv
                @test sup(rt.region) == Inf
            end
        end
    end
end

@testset "Complex roots" begin
    X = interval(-5, 5)
    Xc = Complex(X, X)
    f(z) = z^3 - 1
    df(z) = 3z^2

    # Default
    for contractor in newtonlike_methods
        rts = roots(f, Xc ; derivative = df, contractor, abstol = 1e-3)
        @test count(isunique, rts) == 3
    end

    # Bisection
    rts = roots(f, Xc ; contractor = Bisection, abstol = 1e-3)
end

@testset "@exact" begin
    @testset "1D" begin
        @exact f(x) = x^2 - 2.25
        @exact g(x) = sin(1.57x)

        f0(x) = x^2 - 2.25
        g0(x) = sin(1.57x)

        X = interval(-2.2, 2.2)

        @testset for method in newtonlike_methods
            for (func, func0, nsol) in [(f, f0, 2), (g, g0, 3)]
                rts = roots(func, X ; contractor = method)
                @test length(rts) == nsol

                rts0 = roots(func0, X ; contractor = method)
                for (rt, rt0) in zip(rts, rts0)
                    @test isequal_interval(root_region(rt), root_region(rt0))
                end

                @test all(isguaranteed.(root_region.(rts)))
            end
        end
    end

    @testset "2D" begin
        @exact f(xx) = [xx[1]^2 - 1, xx[2]^2 - 5]
        X = [interval(-2, 2), interval(-10, 10)]
        @testset for method in newtonlike_methods
            rts = roots(f, X ; contractor = method)
            @test all(rts) do rt
                all(isguaranteed.(root_region(rt)))
            end
        end
    end
end

function f_vec(X)
    x, y = X
    return [x^2 - 2, y^2 - 2]
end

function df_vec(X)
    x, y = X
    return [2x 0.0 ; 0.0 2y]
end

function f_svec(X)
    x, y = X
    return SVector(x^2 - 2, y^2 - 2)
end

function df_svec(X)
    x, y = X
    return SMatrix{2, 2}(2x, 0.0, 0.0, 2y)
end

@testset "Type stability" begin
    f(x) = x^2 - 2
    df(x) = 2x

    X = interval(0, 5)
    x = Root(X, :unknown)
    x_vec = Root([X, X], :unknown)
    x_svec = Root(SVector(X, X), :unknown)

    for contractor in newtonlike_methods
        @testset "$contractor" begin
            @inferred roots(f, x ; contractor)
            @inferred roots(f, x ; contractor, derivative = df)

            rts_vec1 = @inferred roots(f_vec, x_vec ; contractor)
            rts_vec2 = @inferred roots(f_vec, x_vec ; contractor, derivative = df_vec)
            @test eltype(rts_vec1) <: Root{<:Vector}
            @test eltype(rts_vec2) <: Root{<:Vector}

            rts_svec1 = @inferred roots(f_svec, x_svec ; contractor)
            rts_svec2 = @inferred roots(f_svec, x_svec ; contractor, derivative = df_svec)
            @test eltype(rts_svec1) <: Root{<:SVector}
            @test eltype(rts_svec2) <: Root{<:SVector}

            # Check that the root type is determined by the return type of the function
            @test eltype(roots(f_vec, SVector(X, X) ; contractor)) <: Root{<:Vector}
            @test eltype(roots(f_svec, [X, X] ; contractor)) <: Root{<:SVector}
        end
    end
end

@testset "Stop criteria" begin
    abstols = [1e-4, 1e-7, 1e-10]
    reltols = [1e-4, 1e-7, 1e-10]

    f(x) = x*sin(1/x)

    for abstol in abstols
        for reltol in reltols
            rts = roots(f, interval(0, 1) ; abstol, reltol, max_iteration = typemax(Int))
            regions = [rt.region for rt in rts if root_status(rt) == :unknown]

            @test all(
                (diam.(regions) .< abstol) .|
                (diam.(regions) .< (reltol .* mag.(regions)))
            )
        end
    end

    for max_iteration in [10, 100, 1000, 10_000]
        rts = roots(f, interval(0, 1) ; abstol = 1e-10, max_iteration)
        @test length(rts) <= max_iteration
    end
end

# straightforward method for unpacking the elements of a root.
function unpacking(r::Root{T}) where T
    return (r.region, r.status)
end

@testset "Unpacking roots" begin
    @testset "1D" begin
        g(x) = cos(x) * sin(1 / x)

        for r in roots(g, interval(0.05, 1))
            x, status = r
            @test isa(x, Interval)
            @test isa(status, Symbol)

            # unpack using the straightforward method. 
            unx, uns = unpacking(r)
            @test isequal_interval(x, unx)
            @test status == uns
        end

        # function with two roots
        f(x) = x^2 - 1
        for r in roots(f, interval(-2, 2))
            x, status = r
            @test status == :unique
        end
    end

    @testset "2D" begin
        f(x, y) = SVector(x^2 + y^2 - 1, y - 2x)
        f(X) = f(X...)
        X = SVector(interval(-6, 6), interval(-6, 6))

        for (x, status) in roots(f, X)
            @test isa(x, SVector{2, <:Interval})
            @test isa(status, Symbol)
        end
    end

    @testset "3D" begin
        h(x, y, z) = SVector(x^2 + y^2 + z^2 - 1, y - 2x, z - 2x)
        h(X) = h(X...)
        X = SVector(interval(-6, 6), interval(-6, 6), interval(-6, 6)) # cube in R^3

        myroots = []
        unroots = []

        for (x, status) in roots(h, X)
            @test isa(x, SVector{3, <:Interval})
            @test isa(status, Symbol)
            append!(myroots, x)
        end

        for (x, status) in unpacking.(roots(h, X))
            append!(unroots, x)
        end

        @test all(isequal_interval.(myroots, unroots))
    end
end

@testset "Bisect on error" begin
    f(x) = (x < 1 ? x : x^2)
    
    rts = roots(f, interval(-10, 10))
    @test length(rts) == 2
    @test rts[1].status == :unique
    @test rts[1].convergence == :converged
    @test isnothing(rts[1].error)

    @test rts[2].status == :unknown
    @test rts[2].convergence == :tolerance
    @test !isnothing(rts[2].error)
    @test rts[2].error[1] isa IntervalArithmetic.InconclusiveBooleanOperation

    @test_throws IntervalArithmetic.InconclusiveBooleanOperation roots(f, interval(-10, 10) ; ignored_errors = [])

    g(x) = (x < 1 ? x : error())
    rts = roots(g, interval(-10, 10) ; ignored_errors = [ErrorException, IntervalArithmetic.InconclusiveBooleanOperation])
    @test any(isunique, rts)
end

# Wilkinson-type polynomial defined by its roots:
# Wn(x) = (x-1)⋅(x-2)⋅ ⋯ ⋅ (x-n)
W3(x) = prod(x .- (1:3))
W7(x) = prod(x .- (1:7))

# Format:  (function, derivative, lower_bound, upper_bound, [true_roots])
function_list = [
    (sin, cos, -5,  5, [-big(π), 0, big(π)]) ,
    (cos, x -> -sin(x), -7.5, 7.5, [-3big(π)/2, -big(π)/2, big(π)/2, 3big(π)/2]),
    (W3, nothing, -10, 10, [1, 2, 3]),
    (W7, nothing, -10, 10, collect(1:7)),
    (x -> exp(x) - 2, exp, -20, 20, [log(big(2))]),
    (x -> asin(sin(x)) - big"0.1", Returns(1.0), 0, 1, [big"0.1"]),
]

@testset "Various precisions" begin
    @testset "$T" for T in [Float64, BigFloat]
        @testset "Function $(func[1])" for func in function_list
            @testset "$contractor" for contractor in newtonlike_methods
                f, f_prime, a_lower, a_upper, true_roots = func
                a = interval(T, a_lower, a_upper)

                @testset "autodiff = $autodiff" for autodiff in (false, true)
                    isnothing(f_prime) && continue

                    if autodiff
                        rts = roots(f, a ; contractor)
                    else
                        rts = roots(f, a ; contractor, derivative = f_prime)
                    end

                    @test length(rts) == length(true_roots)

                    for (root, true_root) in zip(rts, true_roots)
                        @test isa(root, Root)
                        @test isunique(root)
                        @test in_interval(true_root, root.region)
                    end
                end
            end
        end
    end
end


@testset "Interval parameters" begin
    a = interval(1.9, 2.1)
    for contractor in newtonlike_methods
        rts = roots(x -> x^2 - a^2, interval(0, 10) ; contractor)

        @test length(rts) == 1
        @test issubset_interval(a, rts[1].region)
    end
end