import Zygote

function linearIndex(nz, iz, ix)
    return (ix-1)*nz + iz
end

@NonlinearEquations.equations exclude=(nz, nx) function wave2diter(c, f, u, uim1, uim2, nz, nx, nt, dz, dx, dt) # 1st order
    NonlinearEquations.setnumequations(nz*nx)

    ## TOP ROW #########################
    # top row (x, not corners)
    iz = 1
    for ix=2:nx-1
        v1 = c[linearIndex(nz, 1, ix)] * dt/dz
        v2 = c[linearIndex(nz, 1, ix)] * dt/dx
        diff1 = v1*(-(uim1[linearIndex(nz, 2, ix)] - uim1[linearIndex(nz, 1, ix)] - uim2[linearIndex(nz, 2, ix)] - uim2[linearIndex(nz, 1, ix)]))
        diff2 = 0.5*v2^2*(uim1[linearIndex(nz, 1, ix-1)] -2*uim1[linearIndex(nz, 1, ix)] + uim1[linearIndex(nz, 1, ix+1)])
        NonlinearEquations.addterm((ix-1)*nz+iz, u[linearIndex(nz, 1, ix)] - 2*uim1[linearIndex(nz, 1, ix)] -diff1 - diff2)

    end

    ## SECOND FROM TOP ROW #########################
    # second to top row (x, not corners)
    iz = 2
    for ix=3:nx-2
        NonlinearEquations.addterm((ix-1)*nz + iz, -1*(-uim1[linearIndex(nz, 2, ix-2)] +16*uim1[linearIndex(nz, 2, ix-1)] + -30*uim1[linearIndex(nz, 2, ix)] +16*uim1[linearIndex(nz, 2, ix+1)] - uim1[linearIndex(nz, 2, ix+2)])/dx^2/12*c[linearIndex(nz, 2, ix)]^2)
    end
    # second from top row (z)
    for ix=1:nx
        NonlinearEquations.addterm((ix-1)*nz + iz, -1*(uim1[linearIndex(nz, 1, ix)] - 2*uim1[linearIndex(nz, 2, ix)] + uim1[linearIndex(nz, 3, ix)])/dz^2*c[linearIndex(nz, 2, ix)]^2)
    end

    ## BOTTOM ROW #########################

    for ix=2:nx-1
        v1 = c[linearIndex(nz, nz, ix)] * dt/dz
        v2 = c[linearIndex(nz, nz, ix)] * dt/dx
        diff1 = v1*(-(uim1[linearIndex(nz, nz, ix)] - uim1[linearIndex(nz, nz-1, ix)] - uim2[linearIndex(nz, nz, ix)] - uim2[linearIndex(nz, nz-1, ix)]))
        diff2 = 0.5*v2^2*(uim1[linearIndex(nz, nz, ix-1)] -2*uim1[linearIndex(nz, nz, ix)] + uim1[linearIndex(nz, nz, ix+1)])
        NonlinearEquations.addterm((ix-1)*nz+iz, u[linearIndex(nz, nz, ix)] - 2*uim1[linearIndex(nz, nz, ix)] -diff1 - diff2)

    end

    ## SECOND FROM BOTTOM ROW #########################
    # second to bottom row (x, not corners)
    iz = nz - 1
    for ix=3:nx-2
        NonlinearEquations.addterm((ix-1)*nz + iz, -1*(-uim1[linearIndex(nz, nz-1, ix-2)] +16*uim1[linearIndex(nz, nz-1, ix-1)] + -30*uim1[linearIndex(nz, nz-1, ix)] +16*uim1[linearIndex(nz, nz-1, ix+1)] - uim1[linearIndex(nz, nz-1, ix+2)])/dx^2/12*c[linearIndex(nz, nz-1, ix)]^2)
    end
    # second from bottom row (z)
    for ix=1:nx
        NonlinearEquations.addterm((ix-1)*nz + iz, -1*(uim1[linearIndex(nz, nz, ix)] - 2*uim1[linearIndex(nz, nz-1, ix)] + uim1[linearIndex(nz, nz-2, ix)])/dz^2*c[linearIndex(nz, nz-1, ix)]^2)
    end

    # LEFT COLUMN ####################
    ix = 1
    for iz=2:nz-1
        v1 = c[linearIndex(nz, iz, 1)] * dt/dz
        v2 = c[linearIndex(nz, iz, 1)] * dt/dx
        diff1 = 0.5*v1^2*(uim1[linearIndex(nz, iz-1, 1)] -2*uim1[linearIndex(nz, iz, 1)] + uim1[linearIndex(nz, iz+1, 1)])
        diff2 = v2*((uim1[linearIndex(nz, iz, 2)] - uim1[linearIndex(nz, iz, 1)] - uim2[linearIndex(nz, iz, 2)] - uim2[linearIndex(nz, iz, 1)]))
        NonlinearEquations.addterm((ix-1)*nz+iz, u[linearIndex(nz, iz, 1)] - 2*uim1[linearIndex(nz, iz, 1)] -diff1 - diff2)

    end

    # SECOND FROM LEFT COLUMN ####################
    # second to left edge (x)
    ix = 2
    for iz=1:nz
        NonlinearEquations.addterm((ix-1)*nz + iz, -1*(uim1[linearIndex(nz, iz, 1)] - 2*uim1[linearIndex(nz, iz, 2)] + uim1[linearIndex(nz, iz, 3)])/dx^2*c[linearIndex(nz, iz, 2)]^2)
    end

    # second to left edge (z, not corners)
    for iz=3:nz-2
        NonlinearEquations.addterm((ix-1)*nz + iz, -1*(-uim1[linearIndex(nz, iz-2, 2)] +16*uim1[linearIndex(nz, iz-1, 2)] + -30*uim1[linearIndex(nz, iz, 2)] +16*uim1[linearIndex(nz, iz+1, 2)] - uim1[linearIndex(nz, iz+2, 2)])/dz^2/12*c[linearIndex(nz, iz, 2)]^2)
    end

    # RIGHT COLUMN ####################
    ix = nx
    for iz=2:nz-1
        v1 = c[linearIndex(nz, iz, nx)] * dt/dz
        v2 = c[linearIndex(nz, iz, nx)] * dt/dx
        diff1 = 0.5*v1^2*(uim1[linearIndex(nz, iz-1, nx)] -2*uim1[linearIndex(nz, iz, nx)] + uim1[linearIndex(nz, iz+1, nx)])
        diff2 = v2*(-(uim1[linearIndex(nz, iz, nx)] - uim1[linearIndex(nz, iz, nx-1)] - uim2[linearIndex(nz, iz, nx)] - uim2[linearIndex(nz, iz, nx-1)]))
        NonlinearEquations.addterm((ix-1)*nz+iz, u[linearIndex(nz, iz, nx)] - 2*uim1[linearIndex(nz, iz, nx)] -diff1 - diff2)

    end

    # SECOND FROM RIGHT COLUMN ####################
    # second to right edge (x)
    ix = nx - 1
    for iz=1:nz
        NonlinearEquations.addterm((ix-1)*nz + iz, -1*(uim1[linearIndex(nz, iz, nx)] - 2*uim1[linearIndex(nz, iz, nx-1)] + uim1[linearIndex(nz, iz, nx-2)])/dx^2*c[linearIndex(nz, iz, nx-1)]^2)
    end

    # second to right edge (z, not corners)
    for iz=3:nz-2
        NonlinearEquations.addterm((ix-1)*nz + iz, -1*(-uim1[linearIndex(nz, iz-2, nx-1)] +16*uim1[linearIndex(nz, iz-1, nx-1)] + -30*uim1[linearIndex(nz, iz, nx-1)] +16*uim1[linearIndex(nz, iz+1, nx-1)] - uim1[linearIndex(nz, iz+2, nx-1)])/dz^2/12*c[linearIndex(nz, iz, nx-1)]^2)
    end

    # CENTER ####################
    for iz=3:nz-2
        for ix=3:nx-2
            NonlinearEquations.addterm((ix-1)*nz + iz, -1*(-uim1[linearIndex(nz, iz-2, ix)] +16*uim1[linearIndex(nz, iz-1, ix)] + -30*uim1[linearIndex(nz, iz, ix)] +16*uim1[linearIndex(nz, iz+1, ix)] - uim1[linearIndex(nz, iz+2, ix)])/(dz^2*12)*c[linearIndex(nz, iz, ix)]^2) # z
            NonlinearEquations.addterm((ix-1)*nz + iz, -1*(-uim1[linearIndex(nz, iz, ix-2)] +16*uim1[linearIndex(nz, iz, ix-1)] + -30*uim1[linearIndex(nz, iz, ix)] +16*uim1[linearIndex(nz, iz, ix+1)] - uim1[linearIndex(nz, iz, ix+2)])/(dx^2*12)*c[linearIndex(nz, iz, ix)]^2) # x
        end
    end

    ################################################################################
    # time derivative and source ####################
    for iz=1:nz
        for ix=1:nx
            # forcing term
            NonlinearEquations.addterm((ix-1)*nz + iz, f[linearIndex(nz, iz, ix)])

            # 1st order time derivative
            NonlinearEquations.addterm((ix-1)*nz + iz, (u[linearIndex(nz, iz, ix)] - 2*uim1[linearIndex(nz, iz, ix)] + uim2[linearIndex(nz, iz, ix)])/dt^2 )
        end
    end
end

## WAVE PROPAGATION ####################
function uoneiter(ui, uim1, uim2, c, f, nz, nx, nt, dz, dx, dt) # 1st order, one iteration of wave propagation
    return ui - dt^2 * wave2diter_residuals(c, f, ui, uim1, uim2, nz, nx, nt, dz, dx, dt)
end

# multiple iterations
function getuIters(cval, f, nz, nx, nt, dz, dx, dt)
    u = zeros(nz*nx, nt + 2)
    for i=3:nt + 2
        u[:, i] = uoneiter(zeros(size(u, 1)), u[:, i - 1], u[:, i - 2], cval, f[:, i - 2], nz, nx, nt, dz, dx, dt)
        #if mod(i, 10) == 0
        #   @printf("Iteration %i\n", i)
        #end
    end
    return u
end

function ChainRulesCore.rrule(::typeof(getuIters), c, f, nz, nx, nt, dz, dx, dt)
    u = getuIters(c, f, nz, nx, nt, dz, dx, dt)
    return u, delta->begin
        ds = []
        #last iteration
        _, pb = Zygote.pullback(uoneiter, zeros(size(u, 1)), u[:, size(u, 2) - 1], u[:, size(u, 2) - 2], c, f[:, end], nz, nx, nt, dz, dx, dt)
        d1, d2, d3, d4, d5 = pb(delta[:, size(u, 2)])
        push!(ds, (d1, d2, d3))
        dc = d4
        df = hcat(d5)
        #second to last iteration
        _, pb = Zygote.pullback(uoneiter, zeros(size(u, 1)), u[:, size(u, 2) - 2], u[:, size(u, 2) - 3], c, f[:, end - 1], nz, nx, nt, dz, dx, dt)
        d1, d2, d3, d4, d5 = pb(delta[:, size(u, 2) - 1] + ds[1][2])
        push!(ds, (d1, d2, d3))
        dc += d4
        df = hcat(d5, df)
        #now do the general iteration
        for i = size(u, 2) - 2:-1:3
            _, pb = Zygote.pullback(uoneiter, zeros(size(u, 1)), u[:, i - 1], u[:, i - 2], c, f[:, i - 2], nz, nx, nt, dz, dx, dt)
            d1, d2, d3, d4, d5 = pb(delta[:, i] + ds[2][2] + ds[1][3])
            dc += d4
            df = hcat(d5, df)
            push!(ds, (d1, d2, d3))
            ds = ds[2:end]
        end
        return (ChainRulesCore.NO_FIELDS,#getuIters
                dc,
                df,
                ChainRulesCore.NO_FIELDS,#nz
                ChainRulesCore.NO_FIELDS,#nx
                ChainRulesCore.NO_FIELDS,#nt
                ChainRulesCore.NO_FIELDS,#dz
                ChainRulesCore.NO_FIELDS,#dx
                ChainRulesCore.NO_FIELDS)#dt
    end
end

## PULLBACK FUNCTIONS ####################
function make_wave2diter_pullback(args...)
    function wave2diter_pullback(delta)
		retval = (ChainRulesCore.NO_FIELDS,#function
                @ChainRulesCore.thunk(wave2diter_c(args...)' * delta),#c
				@ChainRulesCore.thunk(wave2diter_f(args...)' * delta),#f
				@ChainRulesCore.thunk(wave2diter_u(args...)' * delta),#u
				@ChainRulesCore.thunk(wave2diter_uim1(args...)' * delta),#uim1
				@ChainRulesCore.thunk(wave2diter_uim2(args...)' * delta),#uim2
                ChainRulesCore.NO_FIELDS,#nz
                ChainRulesCore.NO_FIELDS,#nx
                ChainRulesCore.NO_FIELDS,#nt
                ChainRulesCore.NO_FIELDS,#dz
                ChainRulesCore.NO_FIELDS,#dx
                ChainRulesCore.NO_FIELDS)#dt
        return retval
    end
    return wave2diter_pullback
end

function ChainRulesCore.rrule(::typeof(wave2diter_residuals), c, f, u, uim1, uim2, nz, nx, nt, dz, dx, dt)
    args = (c, f, u, uim1, uim2, nz, nx, nt, dz, dx, dt)
    residuals = wave2diter_residuals(args...)
    pullback = make_wave2diter_pullback(args...)
    return residuals, pullback
end


function smolyak_barycentric_points(nodes::Array{T,2},x::Array{T,1},num_points::S) where {T<:AbstractFloat,S<:Integer}

  n = size(nodes,1)

  if num_points > n
    println("smolyak_barycentric_points: Too many points")
    throw(ErrorException())
  end

  dist_vector    = zeros(T,n)
  index_vector_k = Array{S}(num_points)

  for i = 1:n
    dist_vector[i] = norm(x-nodes[i,:])
  end

  for i = 1:num_points
    (dist,index)       = findmin(dist_vector)
    index_vector_k[i]  = index
    dist_vector[index] = Inf
  end

  return index_vector_k

end

function smolyak_barycentric_evaluate(y::AbstractArray{T,1},nodes::Array{T,2},x::Array{T,1}) where T<:AbstractFloat

  k = size(nodes,2)+1
  index_vector_k = smolyak_barycentric_points(nodes,x,k)

  l = vcat(nodes[index_vector_k,:]',ones(1,k))
  println(l)
  weights = l\[x;1.0]

  y_hat = zero(T)

  for i = 1:k
    y_hat += y[index_vector_k[i]]*weights[i]
  end

  return y_hat

end


println("********** FWI::testGetData ****************")
# get a small mesh
domain = [0 2.0 0 2.2]
n      = [12;8;]
M      = getRegularMesh(domain,n)
omega  = 0.1;

n_nodes = n.+1;
nn = prod(n_nodes);
# sources / receivers on top edge
idx    = reshape(collect(1:nn),tuple(n_nodes...))
ib     = idx[:,1];
Q      = SparseMatrixCSC(1.0I, nn, nn);
Q      = Q[:,vec(ib)]
R      = copy(Q)

# get param without parallelizaion
ABLpad = 2;
# This is to get the gamma to be cell centered.
M.n.-=1;
gamma = Helmholtz.getABL(M,true,ones(Int64,M.dim)*ABLpad,8*pi);
M.n.+=1;

attenuation = 0.01*4*pi;
gamma .+= attenuation; # adding Attenuation.
gamma = gamma[:];
Ainv = getJuliaSolver();

pFor = FWI.getFWIparam(omega, one(ComplexF64),gamma,Q,R,M,Ainv,[workers()[1]])[1];

m0   = rand(Float64,tuple(n...)).+1.0
dobs, = getData(vec(m0),fetch(pFor[1]));

# parallelize over sources
pForp,continuationDivision,SourcesSubInd = getFWIparam(omega, one(ComplexF64),gamma,Q,R,M,Ainv,workers())
dobsRF, = getData(vec(m0),pForp)
Dobs = zeros(eltype(dobs),size(dobs));
for k=1:length(dobsRF)
	dobsk = fetch(dobsRF[k]);
	Dobs[:,SourcesSubInd[k]] = dobsk;
end

@test norm(Dobs-dobs)/norm(dobs) < 1e-8


module GraphMatFun

include("compgraph_struct.jl")
include("compgraph_struct_IO.jl")
include("eval.jl")
include("compress_graph.jl")
include("merge.jl")
include("degopt.jl")

# Optimization routines
include("optimization/opt_common.jl")
include("optimization/gauss_newton.jl")
include("optimization/linear_fit.jl")

# Generators
include("generators/denman_beavers_gen.jl")
include("generators/ps_poly.jl")
include("generators/degopt_poly.jl")
include("generators/bbc_exp.jl")
include("generators/bbcs_cheb_exp.jl")
include("generators/sid.jl")
include("generators/monomial_poly.jl")
include("generators/rational.jl")
include("generators/horner.jl")
include("generators/newton_schulz.jl")
include("generators/exp.jl")
include("generators/sastre.jl")

# Error bounds
include("error_bounds.jl")

# Code generations
include("code_gen/gen_code.jl")

end


module Rootionals
using UnicodeFun
using Primes

import Base: numerator, denominator, one, zero, show, ==,
    *, /, ^, sqrt, cbrt,
    float

mutable struct Rootional
    b::Vector{Int}
    e::Vector{Rational}
end

Rootional(z::Int) = simplify!(Rootional([z], [1]))
Rootional(q::Rational) = simplify!(Rootional([numerator(q), denominator(q)], [1, -1]))

one(::Type{Rootional}) = Rootional(1)
zero(::Type{Rootional}) = Rootional(0)

==(a::Rootional, b::Rootional) = (a.b == b.b) && (a.e == b.e)
==(a::Rootional, b::Union{Int,Rational}) = a == Rootional(b)
==(a::Union{Int,Rational}, b::Rootional) = Rootional(a) == b

*(a::Rootional, b::Rootional) =
    simplify!(Rootional(vcat(a.b,b.b), vcat(a.e, b.e)))
*(a::Rootional, b::Union{Int,Rational}) = a*Rootional(b)
*(a::Union{Int,Rational}, b::Rootional) = Rootional(a)*b

/(a::Rootional, b::Rootional) =
    simplify!(Rootional(vcat(a.b,b.b), vcat(a.e, -b.e)))
/(a::Rootional, b::Union{Int,Rational}) = a/Rootional(b)
/(a::Union{Int,Rational}, b::Rootional) = Rootional(a)/b

^(a::Rootional, e::Union{Int,Rational}) = simplify!(Rootional(a.b, e*a.e))

sqrt(a::Rootional) = a^(1//2)
cbrt(a::Rootional) = a^(1//3)

function format_factor(n,e)
    en = numerator(e)
    ed = denominator(e)
    core = if ed == 1
        "$(n)"
    else
        root = if ed == 2
            "√"
        elseif ed == 3
            "∛"
        elseif ed == 4
            "∜"
        else
            "$(to_superscript(ed))√"
        end
        "$(root)$(n)"
    end
    if en == 1
        core
    else
        es = to_superscript(en)
        if ed == 1
            "$(core)$(es)"
        else
            "($(core))$(es)"
        end
    end
end

function show(stream::IO, r::Rootional)
    ni = findall(r.e .> 0)
    di = findall(r.e .< 0)
    if length(ni) > 0
        for i in ni
            write(stream, format_factor(r.b[i],r.e[i]))
        end
    else
        write(stream, "1")
    end
    if length(di) > 0
        write(stream, "/")
        for i in di
            write(stream, format_factor(r.b[i],-r.e[i]))
        end
    end
end

function numerator(r::Rootional)
    ni = findall(r.e .> 0)
    if length(ni) > 0
        Rootional(r.b[ni], r.e[ni])
    else
        one(Rootional)
    end
end

function denominator(r::Rootional)
    di = findall(r.e .< 0)
    if length(di) > 0
        Rootional(r.b[di], -r.e[di])
    else
        one(Rootional)
    end
end

function float(r::Rootional)
    f = 1.0
    for i in eachindex(r.b)
        f *= float(r.b[i])^r.e[i]
    end
    f
end

"Simplify a rootional by cancelling and merging common bases."
function simplify!(r::Rootional)
    d = Dict{Int,Rational}()
    for i in eachindex(r.b)
        for (f,e) in factor(r.b[i])
            d[f] = get(d, f, 0) + e*r.e[i]
        end
    end
    delete!(d, 1)
    for b in keys(d)
        d[b] == 0 && delete!(d, b)
    end
    length(d) == 0 && (d[1]=1)
    r.b = sort(collect(keys(d)))
    r.e = [d[b] for b in r.b]
    r
end

export Rootional, simplify!

end # module


using DataFrames

export
    LinearRegressionModel,
    fit!,
    predict

mutable struct LinearRegressionModel{T, S}
    y::Vector{T}
    xs::AbstractArray{S}
    argv::Vector{Float64}
    n::Integer
end

function LinearRegressionModel(
    df::DataFrame,
    label::Symbol,
    features::Vector{Symbol};
    β::Vector{<:Real}=rand(length(features)+1)
)
    n = nrow(df)
    y = df[!, label]
    xs = Matrix(df[!, features])
    if length(β) != length(features) + 1
        throw(DimensionMismatch("Number of features and arguments mismatch."))
    end

    return LinearRegressionModel(y, xs, β, n)
end

function LinearRegressionModel(
    df::DataFrame,
    label::Symbol,
    feature::Symbol;
    β::Vector{<:Real}=rand(2)
)
    n = nrow(df)
    y = df[!, label]
    x = Matrix(df[!, [feature]])
    if length(β) != 2
        throw(DimensionMismatch("Number of features and arguments mismatch."))
    end

    return LinearRegressionModel(y, x, β, n)
end

function y(model::LinearRegressionModel, xs::Matrix{T}) where {T<:Real}
    xs = hcat(ones(T, size(xs, 1)), xs)

    return xs * model.argv
end

residual(model::LinearRegressionModel) = model.y .- y(model, model.xs)

function loss(model::LinearRegressionModel)
    l = sum(x -> 0.5 * x^2, residual(model))

    return l/model.n
end

function ∇L(model::LinearRegressionModel)
    xs = hcat(ones(size(model.xs, 1)), model.xs)

    return vec(sum(-residual(model) .* xs, dims=1))
end

function gradient_descent(model::LinearRegressionModel, η::Real=1e-4, atol::Real=1e-6, show::Bool=false)
    β = model.argv
    while (l = loss(model)) > atol
        show && println("Loss: $l")
        β .-= η .* ∇L(model)
    end

    return β
end

function fit!(model::LinearRegressionModel; method=gradient_descent, η::Real=1e-4, atol::Real=1e-6, show=false)
    β = method(model, η, atol, show)
    model.argv .= β

    return model
end

function predict(model::LinearRegressionModel, xsᵢ::Vector{T}) where {T<:Real}
    xsᵢ = vcat(ones(T, 1), xsᵢ)'

    return xsᵢ * model.argv
end

function predict(model::LinearRegressionModel, xsᵢ::T) where {T<:Real}
    xsᵢ = vcat(ones(T, 1), xsᵢ)'

    return xsᵢ * model.argv
end


using StatsBase

input = joinpath(@__DIR__, "input")
lines = readlines(input)

function endpoints(line)
    components = map(n -> parse(Int, n), split(line, r",| -> "))
    return ((components[1], components[2]), (components[3], components[4]))
end

function orthogonals(coords)
    map(c -> begin
        (start, finish) = c
        if start[1] == finish[1]
            [(start[1], i) for i in minimum([start[2], finish[2]]):maximum([start[2], finish[2]])]
        elseif start[2] == finish[2]
            [(i, start[2]) for i in minimum([start[1], finish[1]]):maximum([start[1], finish[1]])]
        else
            nothing
        end
    end, coords)
end

function diagonals(coords)
    map(c -> begin
        (start, finish) = c
        if start[1] < finish[1] && start[2] < finish[2]
            return collect(zip(start[1]:finish[1], start[2]:finish[2]))
        elseif start[1] > finish[1] && start[2] > finish[2]
            return collect(zip(start[1]:-1:finish[1], start[2]:-1:finish[2]))
        elseif start[1] < finish[1] && start[2] > finish[2]
            return collect(zip(start[1]:finish[1], start[2]:-1:finish[2]))
        elseif start[1] > finish[1] && start[2] < finish[2]
            return collect(zip(start[1]:-1:finish[1], start[2]:finish[2]))
        else
            nothing
        end
    end, coords)
end

coords = map(endpoints, lines)
orthos = filter(!isnothing, orthogonals(coords))
dias = filter(!isnothing, diagonals(coords))

p1 = length(filter(p -> last(p) > 1, countmap(Iterators.flatten(orthos))))
p2 = length(filter(p -> last(p) > 1, countmap(Iterators.flatten(append!(orthos, dias)))))

println("-----------------------------------------------------------------------")
println("hydrothermal venture -- part one :: $p1")
println("hydrothermal venture -- part two :: $p2")
println("-----------------------------------------------------------------------")

@assert(p1 == 6005)
@assert(p2 == 23864)


## Functions
# Pendulum equations of motion
function pendulum!(dx, x, p, t; M=0)
	θ, ω = x
	g, m, L, c, I = p

	dx[1] = ω
	dx[2] = (M + m*g*sin(θ)*L/2 - c*ω) / I
	return nothing
end
function pendulum!(D, vars::ComponentArray, params, t; M=0)
	@unpack θ, ω = vars
	@unpack g, m, L, c, I = params

	D.θ = ω
	D.ω = (M + m*g*sin(θ)*L/2 - c*ω) / I
	return nothing
end

pendulum_with_control(u) = (dx,x,p,t) -> pendulum!(dx,x,p,t; M=u(x,p.control,t))


using ExactD, LinearAlgebra, Test

include("./test_basis_states.jl")
include("./test_expectation_values.jl")
include("./test_many_body_operators.jl")
include("./test_exponential_ops.jl")


@doc doc"""
    Metric

Abstract type for the pseudo-Riemannian metric tensor $g$, a family of smoothly
varying inner products on the tangent space. See [`inner`](@ref).
"""
abstract type Metric end

"""
    MetricManifold{M<:Manifold,G<:Metric} <: Manifold

Equip a [`Manifold`](@ref) explicitly with a [`Metric`](@ref) `G`.

For a Metric Manifold, by default, assumes, that you implement the linear form
from [`local_metric`](@ref) in order to evaluate the exponential map.

If the corresponding [`Metric`](@ref) `G` yields closed form formulae for e.g.
the exponential map and this is implemented directly (without solving the ode),
you can of course still implement that directly.

# Constructor
    MetricManifold(M, G)

Generate the [`Manifold`](@ref) `M` as a manifold with the [`Metric`](@ref) `G`.
"""
struct MetricManifold{M<:Manifold,G<:Metric} <: Manifold
    manifold::M
    metric::G
end

@doc doc"""
    LorentzMetric <: Metric

Abstract type for Lorentz metrics, which have a single time dimension. These
metrics assume the spacelike convention with the time dimension being last,
giving the signature $(++...+-)$.
"""
abstract type LorentzMetric <: Metric end

@doc doc"""
    RiemannianMetric <: Metric

Abstract type for Riemannian metrics, a family of positive definite inner
products. The positive definite property means that for $v \in T_x M$, the
inner product $g(v, v) > 0$ whenever $v$ is not the zero vector.
"""
abstract type RiemannianMetric <: Metric end

function check_manifold_point(M::MetricManifold, x; kwargs...)
    return check_manifold_point(M.manifold, x; kwargs...)
end

function check_tangent_vector(M::MetricManifold, x, v; kwargs...)
    return check_tangent_vector(M.manifold, x, v; kwargs...)
end

@doc doc"""
    christoffel_symbols_first(M::MetricManifold, x; backend=:default)

Compute the Christoffel symbols of the first kind in local coordinates.
The Christoffel symbols are (in Einstein summation convention)

$\Gamma_{ijk} = \frac{1}{2} \left[g_{kj,i} + g_{ik,j} - g_{ij,k}\right],$

where $g_{ij,k}=\frac{\partial}{\partial x^k} g_{ij}$ is the coordinate
derivative of the local representation of the metric tensor. The dimensions of
the resulting multi-dimensional array are ordered $(i,j,k)$.
"""
function christoffel_symbols_first(M::MetricManifold, x; backend = :default)
    ∂g = local_metric_jacobian(M, x; backend = backend)
    n = size(∂g, 1)
    Γ = allocate(∂g, Size(n, n, n))
    @einsum Γ[i, j, k] = 1 / 2 * (∂g[k, j, i] + ∂g[i, k, j] - ∂g[i, j, k])
    return Γ
end

@doc doc"""
    christoffel_symbols_second(M::MetricManifold, x; backend=:default)

Compute the Christoffel symbols of the second kind in local coordinates.
The Christoffel symbols are (in Einstein summation convention)

$\Gamma^{l}_{ij} = g^{kl} \Gamma_{ijk},$

where $\Gamma_{ijk}$ are the Christoffel symbols of the first kind, and
$g^{kl}$ is the inverse of the local representation of the metric tensor.
The dimensions of the resulting multi-dimensional array are ordered $(l,i,j)$.
"""
function christoffel_symbols_second(M::MetricManifold, x; backend = :default)
    ginv = inverse_local_metric(M, x)
    Γ₁ = christoffel_symbols_first(M, x; backend = backend)
    Γ₂ = allocate(Γ₁)
    @einsum Γ₂[l, i, j] = ginv[k, l] * Γ₁[i, j, k]
    return Γ₂
end

@doc doc"""
    christoffel_symbols_second_jacobian(M::MetricManifold, x; backend = :default)

Get partial derivatives of the Christoffel symbols of the second kind
for manifold `M` at `x` with respect to the coordinates of `x`,
$\frac{\partial}{\partial x^l} \Gamma^{k}_{ij} = \Gamma^{k}_{ij,l}.$
The dimensions of the resulting multi-dimensional array are ordered $(i,j,k,l)$.
"""
function christoffel_symbols_second_jacobian(M::MetricManifold, x; backend = :default)
    n = size(x, 1)
    ∂Γ = reshape(
        _jacobian(x -> christoffel_symbols_second(M, x; backend = backend), x, backend),
        n,
        n,
        n,
        n,
    )
    return ∂Γ
end

@doc doc"""
    det_local_metric(M::MetricManifold, x)

Return the determinant of local matrix representation of the metric tensor $g$.
"""
det_local_metric(M::MetricManifold, x) = det(local_metric(M, x))

"""
    einstein_tensor(M::MetricManifold, x; backend = :default)

Compute the Einstein tensor of the manifold `M` at the point `x`.
"""
function einstein_tensor(M::MetricManifold, x; backend = :default)
    Ric = ricci_tensor(M, x; backend = backend)
    g = local_metric(M, x)
    ginv = inverse_local_metric(M, x)
    S = sum(ginv .* Ric)
    G = Ric - g .* S / 2
    return G
end

@doc doc"""
    exp(N::MetricManifold{M,G}, x, v)

Copute the exponential map on the [`Manifold`](@ref) `M` equipped with the [`Metric`](@ref) `G`.

If the metric was declared the default metric using [`is_default_metric`](@ref), this method
falls back to `exp(M,x,v)`.

Otherwise it numerically integrates the underlying ODE, see [`solve_exp_ode`](@ref).
Currently, the numerical integration is only accurate when using a single
coordinate chart that covers the entire manifold. This excludes coordinates
in an embedded space.
"""
exp(::MetricManifold, ::Any...)
function exp(M::MMT, x, v, T::AbstractVector{T} where {T}) where {MMT<:MetricManifold}
    return exp(M, is_default_metric(M), x, v, T)
end
function exp(
    M::MMT,
    ::Val{true},
    x,
    v,
    T::AbstractVector{T} where {T},
) where {MMT<:MetricManifold}
    return exp(base_manifold(M), x, v, T)
end
function exp(
    M::MMT,
    ::Val{false},
    x,
    v,
    T::AbstractVector{T} where {T},
) where {MMT<:MetricManifold}
    sol = solve_exp_ode(M, x, v, extrema(T); dense = false, saveat = T)
    n = length(x)
    return map(i -> sol.u[i][n+1:end], 1:length(T))
end

exp!(M::MMT, y, x, v) where {MMT<:MetricManifold} = exp!(M, is_default_metric(M), y, x, v)
function exp!(M::MMT, ::Val{true}, y, x, v) where {MMT<:MetricManifold}
    return exp!(base_manifold(M), y, x, v)
end
function exp!(M::MMT, ::Val{false}, y, x, v) where {MMT<:MetricManifold}
    tspan = (0.0, 1.0)
    sol = solve_exp_ode(M, x, v, tspan; dense = false, saveat = [1.0])
    n = length(x)
    return copyto!(y, sol.u[1][n+1:end])
end

@doc doc"""
    flat(N::MetricManifold{M,G}, x, w::FVector{TangentSpaceType})

Compute the musical isomorphism to transform the tangent vector `w` from the
[`Manifold`](@ref) `M` equipped with [`Metric`](@ref) `G` to a cotangent by
computing

````math
w^\flat = G_xw,
````
where $G_x$ is the local matrix representation of `G`, see [`local_metric`](@ref)
"""
flat(::MetricManifold, ::Any...)

function flat!(M::MMT, v::CoTFVector, x, w::TFVector) where {MMT<:MetricManifold}
    g = local_metric(M, x)
    copyto!(v.data, g * w.data)
    return v
end

"""
    gaussian_curvature(M::MetricManifold, x; backend = :default)

Compute the Gaussian curvature of the manifold `M` at the point `x`.
"""
gaussian_curvature(M::MetricManifold, x; kwargs...) = ricci_curvature(M, x; kwargs...) / 2

function get_basis(M::MMT, x, B::ArbitraryOrthonormalBasis) where {MMT<:MetricManifold}
    return invoke(get_basis, Tuple{MMT,Any,AbstractBasis}, M, x, B)
end
function get_basis(M::MMT, x, B::AbstractBasis) where {MMT<:MetricManifold}
    return get_basis(M, is_default_metric(M), x, B)
end
function get_basis(M::MMT, ::Val{true}, x, B::AbstractBasis) where {MMT<:MetricManifold}
    return get_basis(base_manifold(M), x, B)
end
function get_basis(M::MMT, ::Val{false}, x, B::AbstractBasis) where {MMT<:MetricManifold}
    error("tangent_orthogonal_basis not implemented on $(typeof(M)) for point $(typeof(x)) and basis type $(typeof(B)).")
end

function injectivity_radius(M::MMT, args...) where {MMT<:MetricManifold}
    return injectivity_radius(base_manifold(M), args...)
end

@doc doc"""
    inverse_local_metric(M::MetricManifold, x)

Return the local matrix representation of the inverse metric (cometric) tensor, usually
written $g^{ij}$.
"""
inverse_local_metric(M::MetricManifold, x) = inv(local_metric(M, x))

is_decorator_manifold(M::MMT) where {MMT<:MetricManifold} = Val(true)

"""
    is_default_metric(M,G)

Indicate whether the [`Metric`](@ref) `G` is the default metric for
the [`Manifold`](@ref) `M`. This means that any occurence of
[`MetricManifold`](@ref)(M,G) where `typeof(is_default_metric(M,G)) = Val{true}`
falls back to just be called with `M` such that the [`Manifold`](@ref) `M`
implicitly has this metric, for example if this was the first one implemented
or is the one most commonly assumed to be used.
"""
is_default_metric(M::Manifold, G::Metric) = Val(false)

"""
    is_default_metric(MM)

Indicate whether the [`Metric`](@ref) `MM.G` is the default metric for
the [`Manifold`](@ref) `MM.M` within the [`MetricManifold`](@ref) `MM`.
This means that any occurence of
[`MetricManifold`](@ref)`(MM.M,MM.G)` where `typeof(is_default_metric(MM.M,MM.G)) = Val{true}`
falls back to just be called with `MM.MM` such that the [`Manifold`](@ref) `MM.M`
implicitly has the metric `MM.G`, for example if this was the first one
implemented or is the one most commonly assumed to be used.
"""
function is_default_metric(M::MMT) where {MMT<:MetricManifold}
    return is_default_metric(base_manifold(M), metric(M))
end

function convert(T::Type{MetricManifold{MT,GT}}, M::MT) where {MT,GT}
    return _convert_with_default(M, GT, is_default_metric(M, GT()))
end

function _convert_with_default(M::MT, T::Type{<:Metric}, ::Val{true}) where {MT<:Manifold}
    return MetricManifold(M, T())
end
function _convert_with_default(M::MT, T::Type{<:Metric}, ::Val{false}) where {MT<:Manifold}
    error("Can not convert $(M) to a MetricManifold{$(MT),$(T)}, since $(T) is not the default metric.")
end

@doc doc"""
    inner(N::MetricManifold{M,G}, x, v, w)

Compute the inner product of `v`, `w` from the tangent space at `x` on the
[`Manifold`](@ref) `M` using the [`Metric`](@ref) `G`. If `G` is the default
metric (see [`is_default_metric`](@ref)) this is done using `log(M, x, v, w)`,
otherwise the [`local_metric`](@ref)`(M, x)` is employed as

````math
g_x(v,w) = \langle v, G_x w\rangle,
````
where $G_x$ is the local matrix representation of the [`Metric`](@ref) `G`.
"""
inner(M::MMT, x, v, w) where {MMT<:MetricManifold} = inner(M, is_default_metric(M), x, v, w)
function inner(M::MMT, ::Val{false}, x, v, w) where {MMT<:MetricManifold}
    return dot(v, local_metric(M, x) * w)
end
function inner(M::MMT, ::Val{true}, x, v, w) where {MMT<:MetricManifold}
    return inner(base_manifold(M), x, v, w)
end
function inner(
    B::VectorBundleFibers{<:CotangentSpaceType,MMT},
    x,
    v,
    w,
) where {MMT<:MetricManifold}
    ginv = inverse_local_metric(B.M, x)
    return dot(v, ginv * w)
end

@doc doc"""
    local_metric(M::MetricManifold, x)

Return the local matrix representation at the point `x` of the metric tensor $g$ on the
[`Manifold`](@ref) `M`, usually written $g_{ij}$. The matrix has the property that
$g(v, w)=v^T [g_{ij}] w = g_{ij} v^i w^j$, where the latter expression uses
Einstein summation convention.
"""
function local_metric(M::MetricManifold, x)
    error("Local metric not implemented on $(typeof(M)) for point $(typeof(x))")
end

@doc doc"""
    local_metric_jacobian(M::MetricManifold, x; backend=:default)

Get partial derivatives of the local metric of `M` at `x` with respect to the
coordinates of `x`, $\frac{\partial}{\partial x^k} g_{ij} = g_{ij,k}$. The
dimensions of the resulting multi-dimensional array are ordered $(i,j,k)$.
"""
function local_metric_jacobian(M, x; backend = :default)
    n = size(x, 1)
    ∂g = reshape(_jacobian(x -> local_metric(M, x), x, backend), n, n, n)
    return ∂g
end

log!(M::MMT, w, x, y) where {MMT<:MetricManifold} = log!(M, is_default_metric(M), w, x, y)
function log!(M::MMT, ::Val{true}, w, x, y) where {MMT<:MetricManifold}
    return log!(base_manifold(M), w, x, y)
end
function log!(M::MMT, ::Val{false}, w, x, y) where {MMT<:MetricManifold}
    error("Logarithmic map not implemented on $(typeof(M)) for points $(typeof(x)) and $(typeof(y)).")
end

@doc doc"""
    log_local_metric_density(M::MetricManifold, x)

Return the natural logarithm of the metric density $\rho$ of `M` at `x`, which
is given by $\rho=\log \sqrt{|\det [g_{ij}]|}$.
"""
log_local_metric_density(M::MetricManifold, x) = log(abs(det_local_metric(M, x))) / 2

function mean!(
    M::MMT,
    y,
    x::AbstractVector,
    w::AbstractVector;
    kwargs...,
) where {MMT<:MetricManifold}
    return mean!(M, is_default_metric(M), y, x, w; kwargs...)
end
function mean!(
    M::MMT,
    ::Val{true},
    y,
    x::AbstractVector,
    w::AbstractVector;
    kwargs...,
) where {MMT<:MetricManifold}
    return mean!(base_manifold(M), y, x, w; kwargs...)
end
function mean!(
    M::MMT,
    ::Val{false},
    y,
    x::AbstractVector,
    w::AbstractVector;
    kwargs...,
) where {MMT<:MetricManifold}
    return mean!(M, y, x, w, GradientDescentEstimation(); kwargs...)
end

function median!(
    M::MMT,
    y,
    x::AbstractVector,
    w::AbstractVector;
    kwargs...,
) where {MMT<:MetricManifold}
    return median!(M, is_default_metric(M), y, x, w; kwargs...)
end
function median!(
    M::MMT,
    ::Val{true},
    y,
    x::AbstractVector,
    w::AbstractVector;
    kwargs...,
) where {MMT<:MetricManifold}
    return median!(base_manifold(M), y, x, w; kwargs...)
end
function median!(
    M::MMT,
    ::Val{false},
    y,
    x::AbstractVector,
    w::AbstractVector;
    kwargs...,
) where {MMT<:MetricManifold}
    return median!(M, y, x, w, CyclicProximalPointEstimation(); kwargs...)
end

@doc doc"""
    metric(M::MetricManifold)

Get the metric $g$ of the manifold `M`.
"""
metric(M::MetricManifold) = M.metric

function normal_tvector_distribution(M::MMT, x, σ) where {MMT<:MetricManifold}
    return normal_tvector_distribution(M, is_default_metric(M), x, σ)
end
function normal_tvector_distribution(M::MMT, ::Val{true}, x, σ) where {MMT<:MetricManifold}
    return normal_tvector_distribution(base_manifold(M), x, σ)
end
function normal_tvector_distribution(M::MMT, ::Val{false}, x, σ) where {MMT<:MetricManifold}
    error("normal_tvector_distribution not implemented for a $(typeof(M)) at point $(typeof(x)) with standard deviation $(typeof(σ)).")
end

function project_point!(M::MMT, y, x) where {MMT<:MetricManifold}
    return project_point!(M, is_default_metric(M), y, x)
end
function project_point!(M::MMT, ::Val{true}, y, x) where {MMT<:MetricManifold}
    return project_point!(base_manifold(M), y, x)
end
function project_point!(M::MMT, ::Val{false}, y, x) where {MMT<:MetricManifold}
    error("project_point! not implemented on $(typeof(M)) for point $(typeof(x))")
end

function project_tangent!(M::MMT, w, x, v) where {MMT<:MetricManifold}
    return project_tangent!(M, is_default_metric(M), w, x, v)
end
function project_tangent!(M::MMT, ::Val{true}, w, x, v) where {MMT<:MetricManifold}
    return project_tangent!(base_manifold(M), w, x, v)
end
function project_tangent!(M::MMT, ::Val{false}, w, x, v) where {MMT<:MetricManifold}
    error("project_tangent! not implemented for a $(typeof(M)) and tangent $(typeof(v)) at point $(typeof(x)).")
end

function projected_distribution(M::MMT, d, x) where {MMT<:MetricManifold}
    return projected_distribution(M, is_default_metric(M), d, x)
end
function projected_distribution(M::MMT, ::Val{true}, d, x) where {MMT<:MetricManifold}
    return projected_distribution(base_manifold(M), d, x)
end
function projected_distribution(M::MMT, ::Val{false}, d, x) where {MMT<:MetricManifold}
    error("projected_distribution not implemented for a $(typeof(M)) and with $(typeof(d)) at point $(typeof(x)).")
end
function projected_distribution(M::MMT, d) where {MMT<:MetricManifold}
    return projected_distribution(M, is_default_metric(M), d)
end
function projected_distribution(M::MMT, ::Val{true}, d) where {MMT<:MetricManifold}
    return projected_distribution(base_manifold(M), d)
end
function projected_distribution(M::MMT, ::Val{false}, d) where {MMT<:MetricManifold}
    error("projected_distribution not implemented for a $(typeof(M)) with $(typeof(d)).")
end

"""
    ricci_curvature(M::MetricManifold, x; backend = :default)

Compute the Ricci scalar curvature of the manifold `M` at the point `x`.
"""
function ricci_curvature(M::MetricManifold, x; backend = :default)
    ginv = inverse_local_metric(M, x)
    Ric = ricci_tensor(M, x; backend = backend)
    S = sum(ginv .* Ric)
    return S
end

"""
    ricci_tensor(M::MetricManifold, x; backend = :default)

Compute the Ricci tensor, also known as the Ricci curvature tensor,
of the manifold `M` at the point `x`.
"""
function ricci_tensor(M::MetricManifold, x; kwargs...)
    R = riemann_tensor(M, x; kwargs...)
    n = size(R, 1)
    Ric = allocate(R, Size(n, n))
    @einsum Ric[i, j] = R[l, i, l, j]
    return Ric
end

@doc doc"""
    riemann_tensor(M::MetricManifold, x)

Compute the Riemann tensor $R^l_{ijk}$, also known as the Riemann curvature
tensor, at the point `x`. The dimensions of the resulting multi-dimensional
array are ordered $(l,i,j,k)$.
"""
function riemann_tensor(M::MetricManifold, x; backend = :default)
    n = size(x, 1)
    Γ = christoffel_symbols_second(M, x; backend = backend)
    ∂Γ = christoffel_symbols_second_jacobian(M, x; backend = backend) ./ n
    R = allocate(∂Γ, Size(n, n, n, n))
    @einsum R[l, i, j, k] =
        ∂Γ[l, i, k, j] - ∂Γ[l, i, j, k] + Γ[s, i, k] * Γ[l, s, j] - Γ[s, i, j] * Γ[l, s, k]
    return R
end

@doc doc"""
    sharp(N::MetricManifold{M,G}, x, w::FVector{CotangentSpaceType})

Compute the musical isomorphism to transform the cotangent vector `w` from the
[`Manifold`](@ref) `M` equipped with [`Metric`](@ref) `G` to a tangent by
computing

````math
w^\sharp = G_x^{-1}w,
````
where $G_x$ is the local matrix representation of `G`, i.e. one employs
[`inverse_local_metric`](@ref) here to obtain $G_x^{-1}$.
"""
sharp(::MetricManifold, ::Any)

function sharp!(M::N, v::TFVector, x, w::CoTFVector) where {N<:MetricManifold}
    ginv = inverse_local_metric(M, x)
    copyto!(v.data, ginv * w.data)
    return v
end

@doc doc"""
    solve_exp_ode(
        M::MetricManifold,
        x,
        v,
        tspan;
        backend = :default,
        solver = AutoVern9(Rodas5()),
        kwargs...,
    )

Approximate the exponential map on the manifold over the provided timespan
assuming the Levi-Civita connection by solving the ordinary differential
equation

$\frac{d^2}{dt^2} x^k + \Gamma^k_{ij} \frac{d}{dt} x_i \frac{d}{dt} x_j = 0,$

where $\Gamma^k_{ij}$ are the Christoffel symbols of the second kind, and
the Einstein summation convention is assumed. The arguments `tspan` and
`solver` follow the `OrdinaryDiffEq` conventions. `kwargs...` specify keyword
arguments that will be passed to `OrdinaryDiffEq.solve`.

Currently, the numerical integration is only accurate when using a single
coordinate chart that covers the entire manifold. This excludes coordinates
in an embedded space.

!!! note
    This function only works for Julia 1.1 or greater, when
    [OrdinaryDiffEq.jl](https://github.com/JuliaDiffEq/OrdinaryDiffEq.jl) is loaded with
    ```julia
    using OrdinaryDiffEq
    ```
"""
function solve_exp_ode(M, x, v, tspan; kwargs...)
    error("solve_exp_ode not implemented on $(typeof(M)) for point $(typeof(x)), vector $(typeof(v)), and timespan $(typeof(tspan)). For a suitable default, enter `using OrdinaryDiffEq` on Julia 1.1 or greater.")
end

function vector_transport_to!(
    M::MMT,
    vto,
    x,
    v,
    y,
    m::AbstractVectorTransportMethod,
) where {MMT<:MetricManifold}
    return vector_transport_to!(M, is_default_metric(M), vto, x, v, y, m)
end
function vector_transport_to!(
    M::MMT,
    ::Val{true},
    vto,
    x,
    v,
    y,
    m,
) where {MMT<:MetricManifold}
    return vector_transport_to!(base_manifold(M), vto, x, v, y, m)
end
function vector_transport_to!(
    M::MMT,
    ::Val{false},
    vto,
    x,
    v,
    y,
    m,
) where {MMT<:MetricManifold}
    error("vector transport from a point of type $(typeof(x)) to a type $(typeof(y)) on a $(typeof(M)) for a vector of type $(v) and the $(typeof(m)) not yet implemented.")
end

function zero_tangent_vector!(M::MMT, v, x) where {MMT<:MetricManifold}
    return zero_tangent_vector!(M.manifold, v, x)
end


"""
    test_quadratic_integration(model::MOI.ModelLike, config::Config)

Test the problem:
```
Min x^2 + xy + y^2 + yz + z^2
st  x + 2y + 3z >= 4 (c1)
    x +  y      >= 1 (c2)
    x, y, z in R
```
"""
function test_quadratic_integration(model::MOI.ModelLike, config::Config)
    @requires MOI.supports_incremental_interface(model)
    @requires MOI.supports(
        model,
        MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(),
    )
    @requires MOI.supports_constraint(
        model,
        MOI.ScalarAffineFunction{Float64},
        MOI.GreaterThan{Float64},
    )
    v = MOI.add_variables(model, 3)
    @test MOI.get(model, MOI.NumberOfVariables()) == 3
    cf1 =
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, 2.0, 3.0], v), 0.0)
    c1 = MOI.add_constraint(model, cf1, MOI.GreaterThan(4.0))
    if _supports(config, MOI.NumberOfConstraints)
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{
                MOI.ScalarAffineFunction{Float64},
                MOI.GreaterThan{Float64},
            }(),
        ) == 1
    end
    c2 = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([1.0, 1.0], [v[1], v[2]]),
            0.0,
        ),
        MOI.GreaterThan(1.0),
    )
    if _supports(config, MOI.NumberOfConstraints)
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{
                MOI.ScalarAffineFunction{Float64},
                MOI.GreaterThan{Float64},
            }(),
        ) == 2
    end
    MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE
    obj = MOI.ScalarQuadraticFunction(
        MOI.ScalarQuadraticTerm.(
            [2.0, 1.0, 2.0, 1.0, 2.0],
            v[[1, 1, 2, 2, 3]],
            v[[1, 2, 2, 3, 3]],
        ),
        MOI.ScalarAffineTerm{Float64}[],
        0.0,
    )
    MOI.set(
        model,
        MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(),
        obj,
    )
    if _supports(config, MOI.ObjectiveFunction)
        @test obj ≈ MOI.get(
            model,
            MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(),
        )
        @test cf1 ≈ MOI.get(model, MOI.ConstraintFunction(), c1)
        @test MOI.GreaterThan(4.0) == MOI.get(model, MOI.ConstraintSet(), c1)
    end
    if _supports(config, MOI.optimize!)
        @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
        MOI.optimize!(model)
        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
        @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 13 / 7, config)
        @test isapprox(
            MOI.get(model, MOI.VariablePrimal(), v),
            [4 / 7, 3 / 7, 6 / 7],
            config,
        )
        if _supports(config, MOI.ConstraintDual)
            @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT
            # The dual constraint gives
            # [λ_c1 + λ_c2, 2λ_c1 + λ_c2, 3λ_c1] = [2 1 0; 1 2 1; 0 1 2] * x
            #                                    = [11, 16, 15] / 7
            # hence λ_c1 = 5/7 and λ_c2 = 6/7.
            @test isapprox(
                MOI.get(model, MOI.ConstraintDual(), c1),
                5 / 7,
                config,
            )
            @test isapprox(
                MOI.get(model, MOI.ConstraintDual(), c2),
                6 / 7,
                config,
            )
        end
    end
    return
end

function setup_test(
    ::typeof(test_quadratic_integration),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            [4 / 7, 3 / 7, 6 / 7],
            (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) =>
                [5 / 7, 6 / 7],
        ),
    )
    return
end

"""
    test_quadratic_duplicate_terms(model::MOI.ModelLike, config::Config)

Test the problem:
```
Min x^2 + xy + y^2 + yz + z^2
st  x + 2y + 3z >= 4 (c1)
    x +  y      >= 1 (c2)
    x, y, z in R
```
"""
function test_quadratic_duplicate_terms(model::MOI.ModelLike, config::Config)
    @requires MOI.supports_incremental_interface(model)
    @requires MOI.supports(
        model,
        MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(),
    )
    @requires MOI.supports_constraint(
        model,
        MOI.ScalarAffineFunction{Float64},
        MOI.GreaterThan{Float64},
    )
    v = MOI.add_variables(model, 3)
    @test MOI.get(model, MOI.NumberOfVariables()) == 3
    c1f =
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, 2.0, 3.0], v), 0.0)
    c1 = MOI.add_constraint(model, c1f, MOI.GreaterThan(4.0))
    if _supports(config, MOI.NumberOfConstraints)
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{
                MOI.ScalarAffineFunction{Float64},
                MOI.GreaterThan{Float64},
            }(),
        ) == 1
    end
    c2 = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([1.0, 1.0], [v[1], v[2]]),
            0.0,
        ),
        MOI.GreaterThan(1.0),
    )
    if _supports(config, MOI.NumberOfConstraints)
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{
                MOI.ScalarAffineFunction{Float64},
                MOI.GreaterThan{Float64},
            }(),
        ) == 2
    end
    MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE
    obj = MOI.ScalarQuadraticFunction(
        MOI.ScalarQuadraticTerm.(
            [2.0, 0.5, 0.5, 2.0, 1.0, 1.0, 1.0],
            [v[1], v[1], v[1], v[2], v[2], v[3], v[3]],
            [v[1], v[2], v[2], v[2], v[3], v[3], v[3]],
        ),
        MOI.ScalarAffineTerm.(0.0, v),
        0.0,
    )
    MOI.set(
        model,
        MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(),
        obj,
    )
    if _supports(config, MOI.ObjectiveFunction)
        @test obj ≈ MOI.get(
            model,
            MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(),
        )
        @test c1f ≈ MOI.get(model, MOI.ConstraintFunction(), c1)
        @test MOI.GreaterThan(4.0) == MOI.get(model, MOI.ConstraintSet(), c1)
    end
    if _supports(config, MOI.optimize!)
        @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
        MOI.optimize!(model)
        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
        @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 13 / 7, config)
        @test isapprox(
            MOI.get(model, MOI.VariablePrimal(), v),
            [4 / 7, 3 / 7, 6 / 7],
            config,
        )
        if _supports(config, MOI.ConstraintDual)
            @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT
            @test isapprox(
                MOI.get(model, MOI.ConstraintDual(), c1),
                5 / 7,
                config,
            )
            @test isapprox(
                MOI.get(model, MOI.ConstraintDual(), c2),
                6 / 7,
                config,
            )
        end
    end
    # change objective to Max -2(x^2 + xy + y^2 + yz + z^2)
    # First clear the objective, this is needed if a `Bridges.Objective.SlackBridge` is used.
    MOI.set(model, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE)
    MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
    @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE
    obj2 = MOI.ScalarQuadraticFunction(
        MOI.ScalarQuadraticTerm.(
            [-4.0, -1.0, -1.0, -4.0, -2.0, -2.0, -2.0],
            [v[1], v[1], v[1], v[2], v[2], v[3], v[3]],
            [v[1], v[2], v[2], v[2], v[3], v[3], v[3]],
        ),
        MOI.ScalarAffineTerm.(0.0, v),
        0.0,
    )
    MOI.set(
        model,
        MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(),
        obj2,
    )
    if _supports(config, MOI.ObjectiveFunction)
        @test obj2 ≈ MOI.get(
            model,
            MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(),
        )
    end
    if _supports(config, MOI.optimize!)
        MOI.optimize!(model)
        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
        @test isapprox(
            MOI.get(model, MOI.ObjectiveValue()),
            -2 * 13 / 7,
            config,
        )
        @test isapprox(
            MOI.get(model, MOI.VariablePrimal(), v),
            [4 / 7, 3 / 7, 6 / 7],
            config,
        )
        if _supports(config, MOI.ConstraintDual)
            @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT
            @test isapprox(
                MOI.get(model, MOI.ConstraintDual(), c1),
                10 / 7,
                config,
            )
            @test isapprox(
                MOI.get(model, MOI.ConstraintDual(), c2),
                12 / 7,
                config,
            )
        end
    end
    return
end

function setup_test(
    ::typeof(test_quadratic_duplicate_terms),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            [4 / 7, 3 / 7, 6 / 7],
            (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) =>
                [5 / 7, 6 / 7],
        ),
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            [4 / 7, 3 / 7, 6 / 7],
            (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) =>
                [10 / 7, 12 / 7],
        ),
    )
    return
end

"""
    test_quadratic_nonhomogeneous(model::MOI.ModelLike, config::Config)

Test the problem:
```
minimize 2 x^2 + y^2 + xy + x + y + 1
    s.t.  x, y >= 0
          x + y = 1
```
"""
function test_quadratic_nonhomogeneous(model::MOI.ModelLike, config::Config)
    @requires MOI.supports_incremental_interface(model)
    @requires MOI.supports(
        model,
        MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(),
    )
    @requires MOI.supports_constraint(
        model,
        MOI.VariableIndex,
        MOI.GreaterThan{Float64},
    )
    @requires MOI.supports_constraint(
        model,
        MOI.ScalarAffineFunction{Float64},
        MOI.EqualTo{Float64},
    )
    x = MOI.add_variable(model)
    y = MOI.add_variable(model)
    c1 = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([1.0, 1.0], [x, y]),
            0.0,
        ),
        MOI.EqualTo(1.0),
    )
    vc1 = MOI.add_constraint(model, x, MOI.GreaterThan(0.0))
    # We test this after the creation of every `VariableIndex` constraint
    # to ensure a good coverage of corner cases.
    @test vc1.value == x.value
    vc2 = MOI.add_constraint(model, y, MOI.GreaterThan(0.0))
    @test vc2.value == y.value
    MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    obj = MOI.ScalarQuadraticFunction(
        MOI.ScalarQuadraticTerm.([4.0, 2.0, 1.0], [x, y, x], [x, y, y]),
        MOI.ScalarAffineTerm.([1.0, 1.0], [x, y]),
        1.0,
    )
    MOI.set(
        model,
        MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(),
        obj,
    )
    if _supports(config, MOI.optimize!)
        @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
        MOI.optimize!(model)
        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
        @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 2.875, config)
        @test isapprox(
            MOI.get(model, MOI.VariablePrimal(), [x, y]),
            [0.25, 0.75],
            config,
        )
        @test isapprox(
            MOI.get(model, MOI.ConstraintPrimal(), vc1),
            0.25,
            config,
        )
        @test isapprox(
            MOI.get(model, MOI.ConstraintPrimal(), vc2),
            0.75,
            config,
        )
        if _supports(config, MOI.ConstraintDual)
            @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT
            # The dual constraint gives
            # [λ_vc1 + λ_c1, λ_vc2 + λ_c1] = [4 1; 1 2] * x + [1, 1] = [11, 11] / 4
            # since `vc1` and `vc2` are not active, `λ_vc1` and `λ_vc2` are
            # zero so `λ_c1 = 11/4`.
            @test isapprox(
                MOI.get(model, MOI.ConstraintDual(), c1),
                11 / 4,
                config,
            )
            @test isapprox(MOI.get(model, MOI.ConstraintDual(), vc1), 0, config)
            @test isapprox(MOI.get(model, MOI.ConstraintDual(), vc2), 0, config)
        end
    end
    # change back to linear
    #        max 2x + y + 1
    #       s.t.  x, y >= 0
    #             x + y = 1
    # (x,y) = (1,0), obj = 3
    # First clear the objective, this is needed if a
    # `Bridges.Objective.SlackBridge` is used.
    MOI.set(model, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE)
    MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
    objf =
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], [x, y]), 1.0)
    MOI.set(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        objf,
    )
    if _supports(config, MOI.optimize!)
        MOI.optimize!(model)
        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
        @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 3.0, config)
        @test isapprox(
            MOI.get(model, MOI.VariablePrimal(), [x, y]),
            [1.0, 0.0],
            config,
        )
        @test isapprox(MOI.get(model, MOI.ConstraintPrimal(), vc1), 1.0, config)
        @test isapprox(MOI.get(model, MOI.ConstraintPrimal(), vc2), 0.0, config)
        if _supports(config, MOI.ConstraintDual)
            @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT
            @test isapprox(MOI.get(model, MOI.ConstraintDual(), c1), -2, config)
            @test isapprox(MOI.get(model, MOI.ConstraintDual(), vc1), 0, config)
            @test isapprox(MOI.get(model, MOI.ConstraintDual(), vc2), 1, config)
        end
    end
    return
end

function setup_test(
    ::typeof(test_quadratic_nonhomogeneous),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            [1 / 4, 3 / 4],
            (MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}) =>
                [11 / 4],
        ),
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            [1.0, 0.0],
            (MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}) =>
                [-2.0],
        ),
    )
    return
end

"""
    test_quadratic_constraint_integration(model::MOI.ModelLike, config::Config)

Test the problem:
```
Max x  + y
st -x  + y >= 0 (c1[1])
    x  + y >= 0 (c1[2])
    x² + y <= 2 (c2)
Optimal solution
x = 1/2, y = 7/4
```
"""
function test_quadratic_constraint_integration(
    model::MOI.ModelLike,
    config::Config,
)
    @requires MOI.supports_incremental_interface(model)
    @requires MOI.supports(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
    )
    @requires MOI.supports_constraint(
        model,
        MOI.VectorAffineFunction{Float64},
        MOI.Nonnegatives,
    )
    @requires MOI.supports_constraint(
        model,
        MOI.ScalarQuadraticFunction{Float64},
        MOI.LessThan{Float64},
    )
    x = MOI.add_variable(model)
    y = MOI.add_variable(model)
    @test MOI.get(model, MOI.NumberOfVariables()) == 2
    c1 = MOI.add_constraint(
        model,
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [1, 1, 2, 2],
                MOI.ScalarAffineTerm.([-1.0, 1.0, 1.0, 1.0], [x, y, x, y]),
            ),
            [0.0, 0.0],
        ),
        MOI.Nonnegatives(2),
    )
    if _supports(config, MOI.NumberOfConstraints)
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{
                MOI.VectorAffineFunction{Float64},
                MOI.Nonnegatives,
            }(),
        ) == 1
    end
    c2f = MOI.ScalarQuadraticFunction(
        [MOI.ScalarQuadraticTerm(2.0, x, x)],
        [MOI.ScalarAffineTerm(1.0, y)],
        0.0,
    )
    c2 = MOI.add_constraint(model, c2f, MOI.LessThan(2.0))
    if _supports(config, MOI.NumberOfConstraints)
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{
                MOI.ScalarQuadraticFunction{Float64},
                MOI.LessThan{Float64},
            }(),
        ) == 1
    end
    MOI.set(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([1.0, 1.0], [x, y]),
            0.0,
        ),
    )
    MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
    @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE
    if _supports(config, MOI.ConstraintFunction)
        @test isapprox(
            MOI.get(model, MOI.ConstraintFunction(), c2),
            c2f,
            config,
        )
    end
    if _supports(config, MOI.optimize!)
        @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
        MOI.optimize!(model)
        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
        @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 2.25, config)
        @test isapprox(
            MOI.get(model, MOI.VariablePrimal(), [x, y]),
            [0.5, 1.75],
            config,
        )
        @test isapprox(
            MOI.get(model, MOI.ConstraintPrimal(), c1),
            [5 / 4, 9 / 4],
            config,
        )
        @test isapprox(MOI.get(model, MOI.ConstraintPrimal(), c2), 2, config)
        if _supports(config, MOI.ConstraintDual)
            @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT
            # The dual constraint gives
            # [-1, -1] = [-λ_c1[1] + λ_c1[2], λ_c1[1] + λ_c1[2]] + λ_c2 * ([2 0; 0 0] * x + [0, 1])
            #          = [-λ_c1[1] + λ_c1[2] + λ_c2, λ_c1[1] + λ_c1[2] + λ_c2]
            # By complementary slackness, we have `λ_c1 = [0, 0]` so
            # `λ_c2 = -1`.
            @test isapprox(
                MOI.get(model, MOI.ConstraintDual(), c1),
                [0, 0],
                config,
            )
            @test isapprox(MOI.get(model, MOI.ConstraintDual(), c2), -1, config)
        end
    end
    # try delete quadratic constraint and go back to linear
    # FIXME why is this commented ?
    # MOI.delete(model, c2)
    #
    # MOI.optimize!(model)
    #
    # @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
    #
    # @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
    #
    # @test MOI.get(model, MOI.ObjectiveValue()), 0.0 atol=atol rtol=rtol
    return
end

function setup_test(
    ::typeof(test_quadratic_constraint_integration),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            [1 / 2, 7 / 4],
            (MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives) =>
                [zeros(2)],
            (MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}) =>
                [-1.0],
        ),
    )
    return
end

"""
    test_quadratic_constraint_basic(model::MOI.ModelLike, config::Config)

Test the problem:
```
 Max x
s.t. x^2 <= 2 (c)
```
"""
function test_quadratic_constraint_basic(model::MOI.ModelLike, config::Config)
    @requires MOI.supports_incremental_interface(model)
    @requires MOI.supports(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
    )
    @requires MOI.supports_constraint(
        model,
        MOI.ScalarQuadraticFunction{Float64},
        MOI.LessThan{Float64},
    )
    x = MOI.add_variable(model)
    @test MOI.get(model, MOI.NumberOfVariables()) == 1
    cf = MOI.ScalarQuadraticFunction(
        [MOI.ScalarQuadraticTerm(2.0, x, x)],
        [MOI.ScalarAffineTerm(0.0, x)],
        0.0,
    )
    c = MOI.add_constraint(model, cf, MOI.LessThan(2.0))
    if _supports(config, MOI.NumberOfConstraints)
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{
                MOI.ScalarQuadraticFunction{Float64},
                MOI.LessThan{Float64},
            }(),
        ) == 1
    end
    MOI.set(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0),
    )
    MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
    @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE
    if _supports(config, MOI.ConstraintFunction)
        @test isapprox(MOI.get(model, MOI.ConstraintFunction(), c), cf, config)
    end
    if _supports(config, MOI.optimize!)
        @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
        MOI.optimize!(model)
        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
        @test isapprox(MOI.get(model, MOI.ObjectiveValue()), √2, config)
        @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), √2, config)
        @test isapprox(MOI.get(model, MOI.ConstraintPrimal(), c), 2, config)
        if _supports(config, MOI.ConstraintDual)
            @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT
            # The dual constraint gives
            # -1 = λ * (2 * x) = λ * 2√2
            # hence λ = -1/(2√2).
            @test isapprox(
                MOI.get(model, MOI.ConstraintDual(), c),
                -1 / (2 * √2),
                config,
            )
        end
    end
    return
end

function setup_test(
    ::typeof(test_quadratic_constraint_basic),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            [√2],
            (MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}) =>
                [-1 / (2 * √2)],
        ),
    )
    return
end

"""
    test_quadratic_constraint_minimize(
        model::MOI.ModelLike,
        config::Config,
    )

Test the problem:
```
 Min -x
s.t.  x^2 <= 2
```
"""
function test_quadratic_constraint_minimize(
    model::MOI.ModelLike,
    config::Config,
)
    @requires MOI.supports_incremental_interface(model)
    @requires MOI.supports(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
    )
    @requires MOI.supports_constraint(
        model,
        MOI.ScalarQuadraticFunction{Float64},
        MOI.LessThan{Float64},
    )
    x = MOI.add_variable(model)
    @test MOI.get(model, MOI.NumberOfVariables()) == 1
    cf = MOI.ScalarQuadraticFunction(
        [MOI.ScalarQuadraticTerm(2.0, x, x)],
        MOI.ScalarAffineTerm{Float64}[],
        0.0,
    )
    c = MOI.add_constraint(model, cf, MOI.LessThan(2.0))
    if _supports(config, MOI.NumberOfConstraints)
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{
                MOI.ScalarQuadraticFunction{Float64},
                MOI.LessThan{Float64},
            }(),
        ) == 1
    end
    MOI.set(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(-1.0, x)], 0.0),
    )
    MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE
    if _supports(config, MOI.ConstraintFunction)
        @test isapprox(MOI.get(model, MOI.ConstraintFunction(), c), cf, config)
    end
    if _supports(config, MOI.optimize!)
        @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
        MOI.optimize!(model)
        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
        if _supports(config, MOI.ConstraintDual)
            @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT
        end
        @test isapprox(MOI.get(model, MOI.ObjectiveValue()), -√2, config)
        @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), √2, config)
        @test isapprox(MOI.get(model, MOI.ConstraintPrimal(), c), 2, config)
        if _supports(config, MOI.ConstraintDual)
            @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT
            # The dual constraint gives
            # -1 = λ * (2 * x) = λ * 2√2
            # hence λ = -1/(2√2).
            @test isapprox(
                MOI.get(model, MOI.ConstraintDual(), c),
                -1 / (2 * √2),
                config,
            )
        end
    end
    return
end

function setup_test(
    ::typeof(test_quadratic_constraint_minimize),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            [√2],
            (MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}) =>
                [-1 / (2 * √2)],
        ),
    )
    return
end

"""
Max  x
s.t. x^2 + x * y + y^2 <= 3 (c)
     y == 1
"""
function _test_quadratic_constraint_helper(
    model::MOI.ModelLike,
    config::Config,
    less_than::Bool,
)
    @requires MOI.supports_incremental_interface(model)
    @requires MOI.supports(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
    )
    quad_set = less_than ? MOI.LessThan(3.0) : MOI.GreaterThan(-3.0)
    @requires MOI.supports_constraint(
        model,
        MOI.ScalarQuadraticFunction{Float64},
        typeof(quad_set),
    )
    x = MOI.add_variable(model)
    y = MOI.add_variable(model)
    @test MOI.get(model, MOI.NumberOfVariables()) == 2
    vc = MOI.add_constraint(model, y, MOI.EqualTo(1.0))
    @test vc.value == y.value
    cf = MOI.ScalarQuadraticFunction(
        MOI.ScalarQuadraticTerm.([2.0, 1.0, 2.0], [x, x, y], [x, y, y]),
        [MOI.ScalarAffineTerm(0.0, x)],
        0.0,
    )
    if !less_than
        MOIU.operate!(-, Float64, cf)
    end
    c = MOI.add_constraint(model, cf, quad_set)
    if _supports(config, MOI.NumberOfConstraints)
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{
                MOI.ScalarQuadraticFunction{Float64},
                typeof(quad_set),
            }(),
        ) == 1
    end
    MOI.set(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0),
    )
    MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
    @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE
    if _supports(config, MOI.ConstraintFunction)
        @test isapprox(MOI.get(model, MOI.ConstraintFunction(), c), cf, config)
        @test quad_set == MOI.get(model, MOI.ConstraintSet(), c)
    end
    if _supports(config, MOI.optimize!)
        @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
        MOI.optimize!(model)
        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
        @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 1.0, config)
        @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), 1.0, config)
        @test isapprox(MOI.get(model, MOI.VariablePrimal(), y), 1.0, config)
        @test isapprox(
            MOI.get(model, MOI.ConstraintPrimal(), c),
            MOI.constant(quad_set),
            config,
        )
        @test isapprox(MOI.get(model, MOI.ConstraintPrimal(), vc), 1.0, config)
        if _supports(config, MOI.ConstraintDual)
            @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT
            # The dual constraint gives
            # [-1, 0] = λ_c * [2 1; 1 2] * x + [0, λ_vc]
            #         = [3λ_c, 3λ_c + λ_vc]
            # hence `λ_c = -1/3` and `λ_vc = 1`.
            λ_c = less_than ? -1 / 3 : 1 / 3
            @test isapprox(MOI.get(model, MOI.ConstraintDual(), c), λ_c, config)
            @test isapprox(MOI.get(model, MOI.ConstraintDual(), vc), 1, config)
        end
    end
    return
end

function test_quadratic_constraint_LessThan(
    model::MOI.ModelLike,
    config::Config,
)
    _test_quadratic_constraint_helper(model, config, true)
    return
end

function setup_test(
    ::typeof(test_quadratic_constraint_LessThan),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            [1.0, 1.0],
            (MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}) =>
                [-1 / 3],
        ),
    )
    return
end

function test_quadratic_constraint_GreaterThan(
    model::MOI.ModelLike,
    config::Config,
)
    _test_quadratic_constraint_helper(model, config, false)
    return
end

function setup_test(
    ::typeof(test_quadratic_constraint_GreaterThan),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            [1.0, 1.0],
            (MOI.ScalarQuadraticFunction{Float64}, MOI.GreaterThan{Float64}) => [1 / 3],
        ),
    )
    return
end

"""
    test_quadratic_nonconvex_constraint_integration(
        model::MOI.ModelLike,
        config::Config,
    )

Test the problem:
```
Max 2x + y
s.t. x * y <= 4 (c)
     x, y >= 1
```
"""
function test_quadratic_nonconvex_constraint_integration(
    model::MOI.ModelLike,
    config::Config,
)
    @requires MOI.supports_incremental_interface(model)
    @requires MOI.supports(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
    )
    @requires MOI.supports_constraint(
        model,
        MOI.ScalarQuadraticFunction{Float64},
        MOI.LessThan{Float64},
    )
    x = MOI.add_variable(model)
    y = MOI.add_variable(model)
    @test MOI.get(model, MOI.NumberOfVariables()) == 2
    vc1 = MOI.add_constraint(model, x, MOI.GreaterThan(1.0))
    @test vc1.value == x.value
    vc2 = MOI.add_constraint(model, y, MOI.GreaterThan(1.0))
    @test vc2.value == y.value
    cf = MOI.ScalarQuadraticFunction(
        [MOI.ScalarQuadraticTerm(1.0, x, y)],
        [MOI.ScalarAffineTerm(0.0, x)],
        0.0,
    )
    c = MOI.add_constraint(model, cf, MOI.LessThan(4.0))
    if _supports(config, MOI.NumberOfConstraints)
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{
                MOI.ScalarQuadraticFunction{Float64},
                MOI.LessThan{Float64},
            }(),
        ) == 1
    end
    MOI.set(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([2.0, 1.0], [x, y]),
            0.0,
        ),
    )
    MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
    @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE
    if _supports(config, MOI.ConstraintFunction)
        @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c)
    end
    if _supports(config, MOI.optimize!)
        @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
        MOI.optimize!(model)
        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
        @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 9.0, config)
        @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), 4.0, config)
        @test isapprox(MOI.get(model, MOI.VariablePrimal(), y), 1.0, config)
        @test isapprox(MOI.get(model, MOI.ConstraintPrimal(), c), 4.0, config)
        @test isapprox(MOI.get(model, MOI.ConstraintPrimal(), vc1), 4.0, config)
        @test isapprox(MOI.get(model, MOI.ConstraintPrimal(), vc2), 1.0, config)
    end
    return
end

function setup_test(
    ::typeof(test_quadratic_nonconvex_constraint_integration),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) ->
            MOIU.mock_optimize!(mock, [4.0, 1.0], MOI.FEASIBLE_POINT),
    )
    return
end

"""
    test_quadratic_nonconvex_constraint_basic(
        model::MOI.ModelLike,
        config::Config,
    )

Test the problem:
```
Find x, y
s.t. x * y == 4 (c)
     x * x == 4 (c2)
     x, y >= 0
```
"""
function test_quadratic_nonconvex_constraint_basic(
    model::MOI.ModelLike,
    config::Config,
)
    @requires MOI.supports_incremental_interface(model)
    @requires MOI.supports_constraint(
        model,
        MOI.ScalarQuadraticFunction{Float64},
        MOI.EqualTo{Float64},
    )
    x = MOI.add_variable(model)
    y = MOI.add_variable(model)
    @test MOI.get(model, MOI.NumberOfVariables()) == 2
    vc1 = MOI.add_constraint(model, x, MOI.GreaterThan(0.0))
    @test vc1.value == x.value
    vc2 = MOI.add_constraint(model, y, MOI.GreaterThan(0.0))
    @test vc2.value == y.value
    cf = MOI.ScalarQuadraticFunction(
        [MOI.ScalarQuadraticTerm(1.0, x, y)],
        [MOI.ScalarAffineTerm(0.0, x)],
        0.0,
    )
    c = MOI.add_constraint(model, cf, MOI.EqualTo(4.0))
    cf2 = MOI.ScalarQuadraticFunction(
        [MOI.ScalarQuadraticTerm(2.0, x, x)],
        [MOI.ScalarAffineTerm(0.0, x)],
        0.0,
    )
    c2 = MOI.add_constraint(model, cf2, MOI.EqualTo(4.0))
    if _supports(config, MOI.NumberOfConstraints)
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{
                MOI.ScalarQuadraticFunction{Float64},
                MOI.EqualTo{Float64},
            }(),
        ) == 2
    end
    if _supports(config, MOI.ConstraintFunction)
        @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c)
        @test cf2 ≈ MOI.get(model, MOI.ConstraintFunction(), c2)
    end
    if _supports(config, MOI.optimize!)
        @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
        MOI.optimize!(model)
        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
        @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), 2.0, config)
        @test isapprox(MOI.get(model, MOI.VariablePrimal(), y), 2.0, config)
        @test isapprox(MOI.get(model, MOI.ConstraintPrimal(), c), 4.0, config)
        @test isapprox(MOI.get(model, MOI.ConstraintPrimal(), c2), 4.0, config)
        @test isapprox(MOI.get(model, MOI.ConstraintPrimal(), vc1), 2.0, config)
        @test isapprox(MOI.get(model, MOI.ConstraintPrimal(), vc2), 2.0, config)
    end
    return
end

function setup_test(
    ::typeof(test_quadratic_nonconvex_constraint_basic),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) ->
            MOIU.mock_optimize!(mock, [2.0, 2.0], MOI.FEASIBLE_POINT),
    )
    return
end

"""
    test_quadratic_SecondOrderCone_basic(model::MOI.ModelLike, config::Config)

Test the non-convex formulation of a SecondOrderCone constraint:
```
min t
s.t. x + y >= 1 (c1)
     x^2 + y^2 <= t^2 (c2)
     t >= 0 (bound)
```

This is a useful test because some solvers have support for detecting that it is
actually a convex problem due to the `t >= 0` bound.

If your solver does not have this support (and therefore fails this test), you
can safely exclude it.
"""
function test_quadratic_SecondOrderCone_basic(
    model::MOI.ModelLike,
    config::Config,
)
    @requires MOI.supports_incremental_interface(model)
    @requires MOI.supports(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
    )
    @requires MOI.supports_constraint(
        model,
        MOI.ScalarQuadraticFunction{Float64},
        MOI.LessThan{Float64},
    )
    @requires MOI.supports_constraint(
        model,
        MOI.ScalarAffineFunction{Float64},
        MOI.GreaterThan{Float64},
    )
    @requires MOI.supports_constraint(
        model,
        MOI.VariableIndex,
        MOI.GreaterThan{Float64},
    )
    x = MOI.add_variable(model)
    y = MOI.add_variable(model)
    t = MOI.add_variable(model)
    @test MOI.get(model, MOI.NumberOfVariables()) == 3
    c1f =
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, 1.0], [x, y]), 0.0)
    c1 = MOI.add_constraint(model, c1f, MOI.GreaterThan(1.0))
    if _supports(config, MOI.NumberOfConstraints)
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{
                MOI.ScalarAffineFunction{Float64},
                MOI.GreaterThan{Float64},
            }(),
        ) == 1
    end
    c2f = MOI.ScalarQuadraticFunction(
        MOI.ScalarQuadraticTerm.([2.0, 2.0, -2.0], [x, y, t], [x, y, t]),
        MOI.ScalarAffineTerm{Float64}[],
        0.0,
    )
    c2 = MOI.add_constraint(model, c2f, MOI.LessThan(0.0))
    if _supports(config, MOI.NumberOfConstraints)
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{
                MOI.ScalarQuadraticFunction{Float64},
                MOI.LessThan{Float64},
            }(),
        ) == 1
    end
    bound = MOI.add_constraint(model, t, MOI.GreaterThan(0.0))
    @test bound.value == t.value
    if _supports(config, MOI.NumberOfConstraints)
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{
                MOI.VariableIndex,
                MOI.GreaterThan{Float64},
            }(),
        ) == 1
    end
    MOI.set(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, t)], 0.0),
    )
    MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE
    if _supports(config, MOI.ConstraintFunction)
        @test c1f ≈ MOI.get(model, MOI.ConstraintFunction(), c1)
        @test c2f ≈ MOI.get(model, MOI.ConstraintFunction(), c2)
    end
    if _supports(config, MOI.optimize!)
        @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
        MOI.optimize!(model)
        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
        @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 1 / √2, config)
        @test isapprox(
            MOI.get(model, MOI.VariablePrimal(), [x, y, t]),
            [0.5, 0.5, 1 / √2],
            config,
        )
        @test isapprox(
            MOI.get(model, MOI.VariablePrimal(), [t, x, y, t]),
            [1 / √2, 0.5, 0.5, 1 / √2],
            config,
        )
        if _supports(config, MOI.ConstraintDual)
            @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT
            # The dual constraint gives
            # [1, 0, 0] = [λ_bound, λ_c1, λ_c1] + λ_c2 * [-2 0 0; 0 2 0; 0 0 2] * x
            #           = [λ_bound - √2*λ_c2, λ_c1 + λ_c2, λ_c1 + λ_c2]
            # and since `bound` is not active, `λ_bound = 0`.
            # This gives `λ_c2 = -1/√2` and `λ_c1 = 1/√2`.
            @test isapprox(
                MOI.get(model, MOI.ConstraintDual(), c1),
                1 / √2,
                config,
            )
            @test isapprox(
                MOI.get(model, MOI.ConstraintDual(), c2),
                -1 / √2,
                config,
            )
            @test isapprox(
                MOI.get(model, MOI.ConstraintDual(), bound),
                0,
                config,
            )
        end
    end
    return
end

function setup_test(
    ::typeof(test_quadratic_SecondOrderCone_basic),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            [1 / 2, 1 / 2, 1 / √2],
            (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) =>
                [1 / √2],
            (MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}) =>
                [-1 / √2],
        ),
    )
    return
end

"""
    test_quadratic_Integer_SecondOrderCone(model::MOI.ModelLike, config::Config)

Run an integration test on the problem:
```
 min    - 2y - 1z
s.t.   x           == 1
      [x,  y,   z] in SecondOrderCone()
           y,   z  in ZeroOne()
```
"""
function test_quadratic_Integer_SecondOrderCone(
    model::MOI.ModelLike,
    config::Config,
)
    @requires MOI.supports_incremental_interface(model)
    @requires MOI.supports(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
    )
    @requires MOI.supports_constraint(
        model,
        MOI.VectorAffineFunction{Float64},
        MOI.Zeros,
    )
    @requires MOI.supports_constraint(model, MOI.VariableIndex, MOI.ZeroOne)
    @requires MOI.supports_constraint(
        model,
        MOI.VectorOfVariables,
        MOI.SecondOrderCone,
    )
    x, y, z = MOI.add_variables(model, 3)
    MOI.set(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([-2.0, -1.0], [y, z]),
            0.0,
        ),
    )
    MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    MOI.add_constraint(
        model,
        MOI.VectorAffineFunction(
            [MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, x))],
            [-1.0],
        ),
        MOI.Zeros(1),
    )
    MOI.add_constraint(
        model,
        MOI.VectorOfVariables([x, y, z]),
        MOI.SecondOrderCone(3),
    )
    if _supports(config, MOI.NumberOfConstraints)
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{
                MOI.VectorAffineFunction{Float64},
                MOI.Zeros,
            }(),
        ) == 1
        @test MOI.get(
            model,
            MOI.NumberOfConstraints{MOI.VectorOfVariables,MOI.SecondOrderCone}(),
        ) == 1
    end
    loc = MOI.get(model, MOI.ListOfConstraintTypesPresent())
    @test length(loc) == 2
    @test (MOI.VectorAffineFunction{Float64}, MOI.Zeros) in loc
    @test (MOI.VectorOfVariables, MOI.SecondOrderCone) in loc
    bin1 = MOI.add_constraint(model, y, MOI.ZeroOne())
    # We test this after the creation of every `VariableIndex` constraint
    # to ensure a good coverage of corner cases.
    @test bin1.value == y.value
    bin2 = MOI.add_constraint(model, z, MOI.ZeroOne())
    @test bin2.value == z.value
    if _supports(config, MOI.optimize!)
        @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
        MOI.optimize!(model)
        @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
        @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
        @test isapprox(MOI.get(model, MOI.ObjectiveValue()), -2, config)
        @test isapprox(MOI.get(model, MOI.VariablePrimal(), x), 1, config)
        @test isapprox(MOI.get(model, MOI.VariablePrimal(), y), 1, config)
        @test isapprox(MOI.get(model, MOI.VariablePrimal(), z), 0, config)
    end
    return
end

function setup_test(
    ::typeof(test_quadratic_Integer_SecondOrderCone),
    model::MOI.Utilities.MockOptimizer,
    config::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) ->
            MOIU.mock_optimize!(mock, [1.0, 1.0, 0.0]),
    )
    return
end


include("setup.jl")

@testset "factorizing permutations" begin
    @testset "symmetric graphs" begin
        for _ = 1:100
            n = rand(3:10)
            G = rand(0:1, n, n)
            G .= G .⊻ G'
            @test G == G'
            p = symgraph_factorizing_permutation(G)
            @test is_modular_permutation(G, p)
            T = sort!(StrongModuleTree(G, p))
            @test sort!(strong_modules(T)) == sort!(all_strong_modules(G))
            for _ = 1:10
                p′ = symgraph_factorizing_permutation(G, shuffle(1:n))
                @test is_modular_permutation(G, p′)
                T′ = sort!(StrongModuleTree(G, p′))
                @test T == T′
            end
        end
    end

    @testset "tournament graphs" begin
        for _ = 1:1000
            n = rand(3:10)
            T = Int[i != j && rand(Bool) for i=1:n, j=1:n]
            T .= T .⊻ T' .⊻ tril(ones(Int,n,n),-1)
            @test is_tournament(T)
            p = tournament_factorizing_permutation(T)
            modules = all_modules(T)
            @test is_modular_permutation(T, p, modules=modules)
        end
    end

    @testset "directed graphs" begin
        for _ = 1:1000
            n = rand(3:10)
            G = Int[i != j && rand(Bool) for i=1:n, j=1:n]
            p = digraph_factorizing_permutation(G)
            @test is_modular_permutation(G, p)
            p = graph_factorizing_permutation(G)
            @test is_modular_permutation(G, p)
        end
    end
end

function test_permutations(G::AbstractMatrix, T::StrongModuleTree, N::Integer=100)
    n = LinearAlgebra.checksquare(G)
    for _ = 1:N
        p = shuffle(1:n)
        G′ = G[p,p]
        @test sort!(p[StrongModuleTree(G′)]) == T
    end
end

@testset "symmetric graph example" begin
    # from Habib & Paul 2009 (and Wikipedia)
    G = sparse(
        [1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5,
         5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7,
         8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11],
        [2, 3, 4, 1, 4, 5, 6, 7, 1, 4, 5, 6, 7, 1, 2, 3, 5, 6, 7, 2,
         3, 4, 6, 7, 2, 3, 4, 5, 8, 9, 10, 11, 2, 3, 4, 5, 8, 9, 10,
         11, 6, 7, 9, 10, 11, 6, 7, 8, 10, 11, 6, 7, 8, 9, 6, 7, 8, 9],
        1
    )
    T = sort!(StrongModuleTree(G))
    @test repr(T) == "{1 ((2 3) 4) 5 (6 7) (8 9 (10 11))}"
    test_permutations(G, T)
end

@testset "directed graph example" begin
    # from Capelle, Habib & Montgolfier 2002
    G = sparse(
        [1, 1, 1, 2, 3, 3, 3, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 8, 8, 10, 10,
         10, 10, 11, 11, 11, 12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14],
        [3, 4, 5, 1, 2, 4, 5, 2, 4, 2, 7, 8, 9, 10, 8, 9, 10, 9, 10, 11,
         12, 13, 14, 9, 10, 14, 9, 10, 11, 13, 9, 10, 11, 12, 9, 10, 11],
        1
    )
    T = sort!(StrongModuleTree(G))
    @test repr(T) == "({1 2 [3 4 5]} {[6 7 8] 9 10 {11 (12 13) 14}})"
    test_permutations(G, T)
end


import OrdinaryDiffEq

@testset "test_timeresp" begin

A = [0 1; 0 0]
B = [0;1]
C = [1 0]
sys = ss(A,B,C,0)
Q = I # eye(2)
R = I # eye(1)
L = lqr(sys,Q,R)

u(x,i) = -L*x # Form control law
t=0:0.1:50
x0 = [1.,0]
y, t, x, uout = lsim(sys,u,t,x0=x0) # Continuous time

th = 1e-6
@test sum(abs.(x[end,:])) < th

y, t, x, uout = lsim(c2d(sys,0.1)[1],u,t,x0=x0) # Discrete time
@test sum(abs.(x[end,:])) < th

#Do a manual simulation with uout
ym, tm, xm = lsim(sys, uout, t, x0=x0)
@test y ≈ ym
@test x ≈ xm

# Now compare to closed loop
# Discretization is needed before feedback
sysd = c2d(sys, 0.1)[1]
# Create the closed loop system
sysdfb = ss(sysd.A-sysd.B*L, sysd.B, sysd.C, sysd.D, 0.1)
#Simulate without input
yd, td, xd = lsim(sysdfb, zeros(501), t, x0=x0)
@test y ≈ yd
@test x ≈ xd

# Error for nonuniformly spaced vector
@test_throws ErrorException lsim(sys, [1, 2, 3, 4], [0, 1, 1, 2])

# Error for mismatch of sample rates of discrete-system and signal
@test_throws ErrorException lsim(sysd, [1, 2, 3, 4], 0:0.2:0.6)

# Test for problem with broadcast dimensions
@test lsim(sys, zeros(5), 0:0.2:0.8)[1][:] == zeros(5)

# lsim for Int system with Float64 input (regression test for #264)
@test lsim(ss(1,1,1,1,1), ones(5), 0:4)[1][:] == 1:5

# Various combinations of BigFloat
@test lsim(ss(big.(1),1,1,1,1), ones(5), 0:4)[1][:] == 1:5
@test lsim(ss(big.(1),1,1,1,1), big.(ones(5)), 0:4)[1][:] == 1:5
@test lsim(ss(1,1,1,1,1), big.(ones(5)), 0:4)[1][:] == 1:5

# Tests for HeteroStateSpace
@test lsim(HeteroStateSpace(big.(1.0),1,1,1,1), ones(5), 0:4)[1][:] == 1:5

# lsim for discrete-time complex-coefficient systems

# Complex system, real input signal
G1 = ss(1.0im, 1)
@test lsim(G1, [1, 2], 0:1)[1][:] == 1.0im*[1, 2]
@test impulse(G1, 4)[1][:] == [1.0im; zeros(4)]
@test step(G1, 4)[1][:] == fill(1.0im, 5)

G2 = ss(1.0im, 1, 1, 0, 1)
@test lsim(G2, ones(3), 0:2)[1][:] == [0.0, 1, 1 + im]
@test impulse(G2, 4)[1][:] == [0.0, 1, im, -1, -im]
@test step(G2, 4)[1][:] == [0.0, 1, 1+im, 1+im-1, 0]

# Real system, complex input signal
@test lsim(ss(1, 1), [1.0, 1.0im], 0:1)[1][:] == [1.0, 1.0im]
@test lsim(ss(1.0, 1), [1.0, 1.0im], 0:1)[1][:] == [1.0, 1.0im]
@test lsim(ss(1, 1, 1, 0, 1), [1.0, 1im, 1], 0:2)[1][:] == [0.0, 1, 1 + im]

# Complex system, complex input signal
@test lsim(ss(1.0im, 1), [1im, 2], 0:1)[1][:] == [-1, 2im]
@test lsim(ss(1.0im, 1, 1, 0, 1), [1.0, 1im, 1], 0:2)[1][:] == [0.0, 1, 2im]

# Test that the discrete lsim accepts u function that returns scalar
L = lqr(sysd,Q,R)
u(x,i) = -L*x
yd, td, xd = lsim(sysd, u, t, x0=x0)
@test norm(y - yd)/norm(y) < 0.05 # Since the cost matrices are not discretized, these will differ a bit
@test norm(x - xd)/norm(x) < 0.05

# Test lsim with default time vector
uv = randn(length(t))
y,t = lsim(c2d(sys,0.1)[1],uv,t,x0=x0)
yd,td = lsim(c2d(sys,0.1)[1],uv,x0=x0)
@test yd == y
@test td == t

#Test step and impulse Continuous
t0 = 0:0.05:2
systf = [tf(1,[1,1]) 0; 0 tf([1,-1],[1,2,1])]
sysss = ss([-1 0 0; 0 -2 -1; 0 1 0], [1 0; 0 1; 0 0], [1 0 0; 0 1 -1], 0)
yreal = zeros(length(t0), 2, 2)
xreal = zeros(length(t0), 3, 2)

#Step tf
y, t, x = step(systf, t0)
yreal[:,1,1] = 1 .- exp.(-t)
yreal[:,2,2] = -1 .+ exp.(-t)+2*exp.(-t).*t
@test y ≈ yreal atol=1e-4
#Step ss
y, t, x = step(sysss, t)
@test y ≈ yreal atol=1e-4
xreal[:,1,1] = yreal[:,1,1]
xreal[:,2,2] = exp.(-t).*t
xreal[:,3,2] = exp.(-t).*(-t .- 1) .+ 1
@test x ≈ xreal atol=1e-5

#Impulse tf
y, t, x = impulse(systf, t)
yreal[:,1,1] = exp.(-t)
yreal[:,2,2] = exp.(-t).*(1 .- 2*t)
@test y ≈ yreal atol=1e-2
#Impulse ss
y, t, x = impulse(sysss, t)
@test y ≈ yreal atol=1e-2
xreal[:,1,1] = yreal[:,1,1]
xreal[:,2,2] = -exp.(-t).*t + exp.(-t)
xreal[:,3,2] = exp.(-t).*t
@test x ≈ xreal atol=1e-1

#Test step and impulse Discrete
t0 = 0:0.05:2
systf = [tf(1,[1,1]) 0; 0 tf([1,-1],[1,2,1])]
sysss = ss([-1 0 0; 0 -2 -1; 0 1 0], [1 0; 0 1; 0 0], [1 0 0; 0 1 -1], 0)
yreal = zeros(length(t0), 2, 2)
xreal = zeros(length(t0), 3, 2)

#Step tf
y, t, x = step(systf, t0, method=:zoh)
yreal[:,1,1] = 1 .- exp.(-t)
yreal[:,2,2] = -1 .+ exp.(-t) + 2*exp.(-t).*t
@test y ≈ yreal atol=1e-14
#Step ss
y, t, x = step(sysss, t, method=:zoh)
@test y ≈ yreal atol=1e-13
xreal[:,1,1] = yreal[:,1,1]
xreal[:,2,2] = exp.(-t).*t
xreal[:,3,2] = exp.(-t).*(-t .- 1) .+ 1
@test x ≈ xreal atol=1e-14

#Impulse tf
y, t, x = impulse(systf, t, method=:zoh)
yreal[:,1,1] = exp.(-t)
yreal[:,2,2] = exp.(-t).*(1 .- 2*t)
@test y ≈ yreal atol=1e-14
#Impulse ss
y, t, x = impulse(1.0sysss, t, method=:zoh)
@test y ≈ yreal atol=1e-13
xreal[:,1,1] = yreal[:,1,1]
xreal[:,2,2] = -exp.(-t).*t + exp.(-t)
xreal[:,3,2] = exp.(-t).*t
@test x ≈ xreal atol=1e-13

#Step response of discrete system with specified final time
G = tf([1], [1; zeros(3)], 1)
y, t2, x = step(G, 10)
@test y ≈ [zeros(3); ones(8)] atol=1e-5
@test t2 == 0:1:10 # isapprox is broken for ranges (julia 1.3.1)

#Impulse response of discrete system to final time that is not mulitple of the sample time
G = tf([1], [1; zeros(3)], 0.3)
y, t2, x = step(G, 2)
@test y ≈ [zeros(3); ones(4)] atol=1e-5
@test t2 ≈ 0:0.3:1.8 atol=1e-5

#Make sure t was never changed
@test t0 == t

@testset "Simulators" begin

h              = 0.1
Tf             = 20
t              = 0:h:Tf
P              = ss(tf(1,[2,1])^2)
reference(x,t) = [1.]
s              = Simulator(P, reference)
x0             = [0.,0]
tspan          = (0.0,Tf)
sol            = solve(s, x0, tspan, OrdinaryDiffEq.Tsit5())
@test step(P,t)[3] ≈ reduce(hcat,sol.(t))'
end

end


# Testing:
#
# - MC estimates of mean, variance
#
# TODO: add variance testing for asymmetric bounds

using Statistics, DiffModels
using Test

nanmean(x) = mean(x[.!isnan.(x)])
nanvar(x) = var(x[.!isnan.(x)])

function estimatemoments(d::AbstractDrift, b::AbstractBounds, n)
    t = fill(NaN, n)
    c = fill(NaN, n)
    s = sampler(d, b)
    for i = 1:n
        t[i], ci = rand(s)
        c[i] = ci ? 1.0 : 0.0
    end
    return [nanmean(t), nanvar(t), mean(c),
            nanmean(t[c .== 1.0]), nanvar(t[c .== 1.0]),
            nanmean(t[c .== 0.0]), nanvar(t[c .== 0.0])]
end

# symmetric bounds

dmsymmeant(mu::Real, bound::Real) =
    isapprox(mu, zero(mu)) ? abs2(bound) : bound / mu * tanh(bound * mu)
function dmsymvart(mu::Real, bound::Real)
    if isapprox(mu, zero(mu))
        return 2abs2(abs2(bound)) / 3
    end
    a = mu * bound
    return bound * (tanh(a) - a * abs2(sech(a))) / mu^3
end
dmsympubound(mu::Real, bound::Real) = 1 / (1 + exp(-2mu * bound))

n = 10^5
dt = 0.001

# normal/exponential sampler for small mu
bound = 0.9
mu = 0.5
m = estimatemoments(ConstDrift(mu, dt), ConstSymBounds(bound, dt), n)
@test m[1] ≈ dmsymmeant(mu, bound) atol=0.1
@test m[2] ≈ dmsymvart(mu, bound) atol=0.1
@test m[3] ≈ dmsympubound(mu, bound) atol=0.05

# inverse-normal sampler for large mu
bound = 0.9
mu = 2
m = estimatemoments(ConstDrift(mu, dt), ConstSymBounds(bound, dt), n)
@test m[1] ≈ dmsymmeant(mu, bound) atol=0.1
@test m[2] ≈ dmsymvart(mu, bound) atol=0.1
@test m[3] ≈ dmsympubound(mu, bound) atol=0.05

# generic sampler
bound = 0.9
mu = 1.1
m = estimatemoments(VarDrift([mu], dt, 15.0), ConstSymBounds(bound, dt), n)
@test m[1] ≈ dmsymmeant(mu, bound) atol=0.1
@test m[2] ≈ dmsymvart(mu, bound) atol=0.1
@test m[3] ≈ dmsympubound(mu, bound) atol=0.05

# asymmetric bounds

dmasymmeantup(mu::Real, blo::Real, bup::Real) = isapprox(mu, zero(mu)) ?
    (abs2(bup) - 2bup * blo) / 3 :
    (bup - blo) / mu * coth((bup - blo) * mu) + blo / mu * coth(- blo * mu)
dmasymmeantlo(mu::Real, blo::Real, bup::Real) = isapprox(mu, zero(mu)) ?
    (abs2(blo) - 2bup * blo) / 3 :
    (bup - blo) / mu * coth((bup - blo) * mu) - bup / mu * coth(bup * mu)
dmasympubound(mu::Real, blo::Real, bup::Real) = isapprox(mu, zero(mu)) ?
    blo / (blo + bup) : (exp(-2mu * blo) - 1) / (exp(-2mu * blo) - exp(-2mu * bup))

# fast sampler for constant drift/bounds
blo = -1.5
bup = 1.0
mu = 1.1
m = estimatemoments(ConstDrift(mu, dt), ConstAsymBounds(bup, blo, dt), n)
@test m[3] ≈ dmasympubound(mu, blo, bup) atol=0.05
@test m[4] ≈ dmasymmeantup(mu, blo, bup) atol=0.1
@test m[6] ≈ dmasymmeantlo(mu, blo, bup) atol=0.1

# generic sampler
blo = -1.5
bup = 1.0
mu = 1.1
m = estimatemoments(VarDrift([mu], dt, 15.0), ConstAsymBounds(bup, blo, dt), n)
@test m[3] ≈ dmasympubound(mu, blo, bup) atol=0.05
@test m[4] ≈ dmasymmeantup(mu, blo, bup) atol=0.1
@test m[6] ≈ dmasymmeantlo(mu, blo, bup) atol=0.1


#=
author: mingchuno
smart permutation inspired by python-high-perf
TODO: parallel & mutil core, compile?, README and output
=#

BASE = 22
PPP = sum(map(x -> BASE^x,0:2)) # generic PPP
println("PPP is:$(PPP)")
array = sort(collect(setdiff(Set(0:BASE-1), Set(1))))
sarray = Set(array)
count = 0

@time for last4_arr in combinations(array, 4), efgh_arr in permutations(last4_arr)
  @inbounds e = efgh_arr[1]
  if e == 0 continue end # fail fast

  @inbounds f = efgh_arr[2]

  @inbounds g = efgh_arr[3]
  if g == 0 continue end # fail fast

  @inbounds h = efgh_arr[4]

  ef = e*BASE + f
  gh = g*BASE + h

  if ef + gh == PPP
    remain_arr = collect(setdiff(sarray, Set(last4_arr)))
    for first4_arr in combinations(remain_arr, 4), abcd_arr in permutations(first4_arr)
      @inbounds a = abcd_arr[1]
      if a == 0 continue end # fail fast

      @inbounds b = abcd_arr[2]

      @inbounds c = abcd_arr[3]
      if c == 0 continue end # fail fast

      @inbounds d = abcd_arr[4]
      ab = a*BASE + b
      cd = c*BASE + d
      if ab - cd == ef
        println("($(base(BASE,ab)) - $(base(BASE,cd)) = $(base(BASE, ef))) + $(base(BASE,gh)) = 111")
        count += 1
      end
    end
  end
end

println("Total number of soln is:$(count)")


module TestEnsemblePTSampler

using Distributed

using Test: @test, @testset

using Ensemble

using Statistics

function test_brewer()
    nd = 5
    nw = 128
    nt = 16

    function loglike(x)
        v = 0.1
        u = 0.01
        m = 0.031

        logl1 = -nd*log(v) - nd/2*log(2*pi) - 0.5*sum(x.*x/(v*v))
        logl2 = -nd*log(u) - nd/2*log(2*pi) - 0.5*sum((x.-m).*(x.-m)/(u*u))

        return Stats.logsumexp(logl1, log(99.0) + logl2)
    end

    function logprior(x)
        if any(x .> 0.5) || any(x .< -0.5)
            return -Inf
        else
            return 0.0
        end
    end

    pts = rand(nd,nw,nt) .- 0.5

    betas = collect(range(1, stop=0, length=nt))

    chain, lnlikes, lnpriors, betas = EnsemblePTSampler.run_mcmc(pts, loglike, logprior, betas, 8192, thin=4)

    @testset "Brewer phase transition distribution" begin
        for i in 1:nd
            @test isapprox(mean(chain[i,:,1,:]), 0.99*0.031, atol=0.1*sqrt(0.99*0.01^2 + 0.01*0.1^2))
        end

        lnZ, dlnZ = EnsemblePTSampler.lnZ(lnlikes, betas)
        @test isapprox(lnZ, log(100.0), atol=3*dlnZ)
    end

    chain, lnlikes, lnpriors, betas
end

function testall()
    @testset "EnsemblePTSampler test suite" begin
        test_brewer()
    end
end

end


type ClaytonPC <: ParamPC
    θ::Float64

    function ClaytonPC(θ::Float64)
        if θ <= 0
            error("Clayton parameter must be larger than 0")
        end
        new(θ)
    end

end

function params(cop::ClaytonPC)
    return (cop.θ,)
end

#####################
## copula function ##
#####################

## pdf function
##-------------

## function pdf(cop::ClaytonPC, u1::FloatVec, u2::FloatVec)
##     θ = params(cop)[1]

##     val = (θ+1).*(u1.*u2).^(-θ-1) .*
##     				(u1.^-θ + u2.^-θ - 1).^(-(2θ+1)/θ)
##     return val
## end