###
### Define data structures used by later tests.
###

struct UnknownScalarSet{T} <: MOI.AbstractScalarSet
    constant::T
end

MOI.constant(set::UnknownScalarSet) = set.constant

Base.copy(set::UnknownScalarSet) = UnknownScalarSet(copy(MOI.constant(set)))

MOIU.supports_shift_constant(::Type{<:UnknownScalarSet}) = true
function MOIU.shift_constant(set::UnknownScalarSet, offset)
    return UnknownScalarSet(MOI.constant(set) + offset)
end

struct UnknownVectorSet <: MOI.AbstractVectorSet end

abstract type BadModel{T} <: MOI.ModelLike end

function MOI.get(::BadModel, ::MOI.ListOfModelAttributesSet)
    return MOI.AbstractModelAttribute[]
end

MOI.get(::BadModel, ::MOI.ListOfVariableIndices) = [MOI.VariableIndex(1)]

function MOI.get(::BadModel, ::MOI.ListOfVariableAttributesSet)
    return MOI.AbstractVariableAttribute[]
end

function MOI.get(::BadModel, ::MOI.ListOfConstraintAttributesSet)
    return MOI.AbstractConstraintAttribute[]
end

function MOI.get(::BadModel{T}, ::MOI.ListOfConstraintTypesPresent) where {T}
    return [(MOI.VariableIndex, MOI.EqualTo{T})]
end

function MOI.get(::BadModel, ::MOI.ListOfConstraintIndices{F,S}) where {F,S}
    return [MOI.ConstraintIndex{F,S}(1)]
end

function MOI.get(
    ::BadModel{T},
    ::MOI.ConstraintFunction,
    ::MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{T}},
) where {T}
    return MOI.VariableIndex(1)
end

function MOI.get(
    ::BadModel{T},
    ::MOI.ConstraintSet,
    ::MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{T}},
) where {T}
    return MOI.EqualTo(T(0))
end

struct BadConstraintModel{T} <: BadModel{T}
    BadConstraintModel(T = Float64) = new{T}()
end

function MOI.get(
    ::BadConstraintModel{T},
    ::MOI.ListOfConstraintTypesPresent,
) where {T}
    return [
        (MOI.VariableIndex, MOI.EqualTo{T}),
        (MOI.VariableIndex, UnknownScalarSet{T}),
    ]
end

function MOI.get(
    ::BadModel,
    ::MOI.ConstraintFunction,
    ::MOI.ConstraintIndex{MOI.VariableIndex,<:UnknownScalarSet},
)
    return MOI.VariableIndex(1)
end

function MOI.get(
    ::BadModel,
    ::MOI.ConstraintSet,
    ::MOI.ConstraintIndex{MOI.VariableIndex,UnknownScalarSet{T}},
) where {T}
    return UnknownScalarSet(T(1))
end

struct UnknownModelAttribute <: MOI.AbstractModelAttribute end

struct BadModelAttributeModel{T} <: BadModel{T}
    BadModelAttributeModel(T = Float64) = new{T}()
end

MOI.get(src::BadModelAttributeModel, ::UnknownModelAttribute) = 0

function MOI.get(::BadModelAttributeModel, ::MOI.ListOfModelAttributesSet)
    return MOI.AbstractModelAttribute[UnknownModelAttribute()]
end

struct UnknownVariableAttribute <: MOI.AbstractVariableAttribute end
struct BadVariableAttributeModel{T} <: BadModel{T}
    BadVariableAttributeModel(T = Float64) = new{T}()
end

function MOI.get(
    ::BadVariableAttributeModel,
    ::UnknownVariableAttribute,
    ::MOI.VariableIndex,
)
    return 0
end

function MOI.get(::BadVariableAttributeModel, ::MOI.ListOfVariableAttributesSet)
    return MOI.AbstractVariableAttribute[UnknownVariableAttribute()]
end

struct UnknownConstraintAttribute <: MOI.AbstractConstraintAttribute end

struct BadConstraintAttributeModel{T} <: BadModel{T}
    BadConstraintAttributeModel(T = Float64) = new{T}()
end

function MOI.get(
    ::BadConstraintAttributeModel,
    ::UnknownConstraintAttribute,
    ::MOI.ConstraintIndex,
)
    return 0
end

function MOI.get(
    ::BadConstraintAttributeModel,
    ::MOI.ListOfConstraintAttributesSet,
)
    return MOI.AbstractConstraintAttribute[UnknownConstraintAttribute()]
end

###
### The actual tests
###

"""
    test_model_default_ObjectiveSense(model::MOI.ModelLike, ::Config)

Test that the default ObjectiveSense is FEASIBILITY_SENSE.
"""
function test_model_default_ObjectiveSense(model::MOI.ModelLike, ::Config)
    MOI.get(model, MOI.ObjectiveSense()) == MOI.FEASIBILITY_SENSE
    _test_attribute_value_type(model, MOI.ObjectiveSense())
    return
end

"""
    test_model_default_TerminationStatus(model::MOI.AbstractOptimizer, ::Config)

Test that the default TerminationStatus is OPTIMIZE_NOT_CALLED.
"""
function test_model_default_TerminationStatus(
    model::MOI.AbstractOptimizer,
    ::Config,
)
    MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
    _test_attribute_value_type(model, MOI.TerminationStatus())
    return
end

test_model_default_TerminationStatus(::MOI.ModelLike, ::Config) = nothing

"""
    test_model_default_PrimalStatus(model::MOI.AbstractOptimizer, ::Config)

Test that the default PrimalStatus is NO_SOLUTION.
"""
function test_model_default_PrimalStatus(model::MOI.AbstractOptimizer, ::Config)
    MOI.get(model, MOI.PrimalStatus()) == MOI.NO_SOLUTION
    _test_attribute_value_type(model, MOI.PrimalStatus())
    return
end

test_model_default_PrimalStatus(::MOI.ModelLike, ::Config) = nothing

"""
    test_model_default_DualStatus(model::MOI.AbstractOptimizer, ::Config)

Test that the default DualStatus is NO_SOLUTION.
"""
function test_model_default_DualStatus(model::MOI.AbstractOptimizer, ::Config)
    MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION
    _test_attribute_value_type(model, MOI.DualStatus())
    return
end

test_model_default_DualStatus(::MOI.ModelLike, ::Config) = nothing

"""
    test_model_VariableName(model::MOI.ModelLike, ::Config)

Test MOI.VariableName.
"""
function test_model_VariableName(model::MOI.ModelLike, ::Config)
    @requires MOI.supports(model, MOI.VariableName(), MOI.VariableIndex)
    x = MOI.add_variables(model, 2)
    MOI.set(model, MOI.VariableName(), x[1], "x1")
    @test MOI.get(model, MOI.VariableIndex, "x1") == x[1]
    MOI.set(model, MOI.VariableName(), x[1], "x2")
    @test MOI.get(model, MOI.VariableIndex, "x1") === nothing
    @test MOI.get(model, MOI.VariableIndex, "x2") == x[1]
    MOI.set(model, MOI.VariableName(), x[2], "x1")
    @test MOI.get(model, MOI.VariableIndex, "x1") == x[2]
    MOI.set(model, MOI.VariableName(), x[1], "x1")
    @test_throws ErrorException MOI.get(model, MOI.VariableIndex, "x1")
    _test_attribute_value_type(model, MOI.VariableName(), x[1])
    return
end

"""
    test_model_VariableIndex_ConstraintName(
        model::MOI.ModelLike,
        ::Config,
    )

Test ConstraintName for VariableIndex constraints.
"""
function test_model_VariableIndex_ConstraintName(
    model::MOI.ModelLike,
    ::Config{T},
) where {T}
    x = MOI.add_variable(model)
    c = MOI.add_constraint(model, x, MOI.GreaterThan(T(0)))
    @test_throws(
        MOI.VariableIndexConstraintNameError(),
        MOI.set(model, MOI.ConstraintName(), c, "c1"),
    )
    return
end

"""
    test_model_ScalarAffineFunction_ConstraintName(
        model::MOI.ModelLike,
        ::Config,
    )

Test ConstraintName for ScalarAffineFunction constraints.
"""
function test_model_ScalarAffineFunction_ConstraintName(
    model::MOI.ModelLike,
    ::Config{T},
) where {T}
    x = MOI.add_variable(model)
    f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(T(1), x)], T(0))
    c1 = MOI.add_constraint(model, f, MOI.GreaterThan(T(0)))
    c2 = MOI.add_constraint(model, f, MOI.LessThan(T(1)))
    MOI.set(model, MOI.ConstraintName(), c1, "c1")
    @test MOI.get(model, MOI.ConstraintIndex, "c1") == c1
    MOI.set(model, MOI.ConstraintName(), c1, "c2")
    @test MOI.get(model, MOI.ConstraintIndex, "c1") === nothing
    @test MOI.get(model, MOI.ConstraintIndex, "c2") == c1
    MOI.set(model, MOI.ConstraintName(), c2, "c1")
    @test MOI.get(model, MOI.ConstraintIndex, "c1") == c2
    MOI.set(model, MOI.ConstraintName(), c1, "c1")
    @test_throws ErrorException MOI.get(model, MOI.ConstraintIndex, "c1")
    return
end

"""
    test_model_Name(model::MOI.ModelLike, ::Config)

Test the `MOI.Name` attribute.
"""
function test_model_Name(model::MOI.ModelLike, ::Config)
    @requires MOI.supports(model, MOI.Name())
    @test !(MOI.Name() in MOI.get(model, MOI.ListOfModelAttributesSet()))
    @test MOI.get(model, MOI.Name()) == ""
    MOI.set(model, MOI.Name(), "Name1")
    @test MOI.Name() in MOI.get(model, MOI.ListOfModelAttributesSet())
    @test MOI.get(model, MOI.Name()) == "Name1"
    MOI.set(model, MOI.Name(), "Name2")
    @test MOI.Name() in MOI.get(model, MOI.ListOfModelAttributesSet())
    @test MOI.get(model, MOI.Name()) == "Name2"
    _test_attribute_value_type(model, MOI.Name())
    return
end

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

Test a variety of name attribute behavior.
"""
function test_model_Name_VariableName_ConstraintName(
    model::MOI.ModelLike,
    config::Config{T},
) where {T}
    @requires MOI.supports_incremental_interface(model)
    @requires MOI.supports(model, MOI.VariableName(), MOI.VariableIndex)
    @test MOI.get(model, MOI.NumberOfVariables()) == 0
    @test MOI.get(
        model,
        MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}(),
    ) == 0
    v = MOI.add_variables(model, 2)
    @test MOI.get(model, MOI.VariableName(), v[1]) == ""
    x, cx = MOI.add_constrained_variable(model, MOI.GreaterThan(T(0)))
    @test MOI.get(model, MOI.VariableName(), x) == ""
    y, cy = MOI.add_constrained_variables(model, MOI.Nonpositives(4))
    for yi in y
        @test MOI.get(model, MOI.VariableName(), yi) == ""
    end
    @test MOI.get(model, MOI.ConstraintName(), cy) == ""
    MOI.set(model, MOI.VariableName(), v[1], "")
    MOI.set(model, MOI.VariableName(), v[2], "") # Shouldn't error with duplicate empty name
    MOI.set(model, MOI.VariableName(), x, "")
    for yi in y
        MOI.set(model, MOI.VariableName(), yi, "")
    end
    MOI.set(model, MOI.VariableName(), v[1], "Var1")
    MOI.set(model, MOI.VariableName(), v[2], "Var1")
    # Lookup must fail when there are multiple variables with the same name.
    @test_throws Exception MOI.get(model, MOI.VariableIndex, "Var1")
    MOI.set(model, MOI.VariableName(), v[2], "Var2")
    @test MOI.get(model, MOI.VariableIndex, "Var1") == v[1]
    @test MOI.get(model, MOI.VariableIndex, "Var2") == v[2]
    @test MOI.get(model, MOI.VariableIndex, "Var3") === nothing
    MOI.set(model, MOI.VariableName(), x, "Var1")
    @test_throws Exception MOI.get(model, MOI.VariableIndex, "Var1")
    MOI.set(model, MOI.VariableName(), x, "Varx")
    @test MOI.get(model, MOI.VariableIndex, "Var1") == v[1]
    @test MOI.get(model, MOI.VariableIndex, "Var2") == v[2]
    @test MOI.get(model, MOI.VariableIndex, "Varx") == x
    @test MOI.get(model, MOI.VariableIndex, "Var3") === nothing
    vynames = ["VarX", "Var2", "Vary1", "Vary2", "Vary3", "Vary4"]
    MOI.set(model, MOI.VariableName(), [v; y], vynames)
    @test MOI.get(model, MOI.VariableName(), v) == vynames[1:2]
    @test MOI.get(model, MOI.VariableName(), y) == vynames[3:6]
    @test MOI.get(model, MOI.VariableName(), [v; y]) == vynames
    @requires MOI.supports_constraint(
        model,
        MOI.ScalarAffineFunction{T},
        MOI.LessThan{T},
    )
    c = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(T(1), v), T(0)),
        MOI.LessThan(T(1)),
    )
    @requires MOI.supports_constraint(
        model,
        MOI.ScalarAffineFunction{T},
        MOI.EqualTo{T},
    )
    c2 = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(T[-1, 1], v), T(0)),
        MOI.EqualTo(T(0)),
    )
    @test MOI.get(model, MOI.ConstraintName(), c) == ""
    @test MOI.get(model, MOI.ConstraintName(), c2) == ""
    @test MOI.get(model, MOI.ConstraintName(), cy) == ""
    @requires MOI.supports(model, MOI.ConstraintName(), typeof(c))
    MOI.set(model, MOI.ConstraintName(), c, "")
    @requires MOI.supports(model, MOI.ConstraintName(), typeof(c2))
    MOI.set(model, MOI.ConstraintName(), c2, "") # Shouldn't error with duplicate empty name
    @requires MOI.supports(model, MOI.ConstraintName(), typeof(cy))
    MOI.set(model, MOI.ConstraintName(), cy, "")
    MOI.set(model, MOI.ConstraintName(), c, "Con0")
    @test MOI.get(model, MOI.ConstraintName(), c) == "Con0"
    MOI.set(model, MOI.ConstraintName(), c2, "Con0")
    # Lookup must fail when multiple constraints have the same name.
    @test_throws Exception MOI.get(model, MOI.ConstraintIndex, "Con0")
    @test_throws Exception MOI.get(model, typeof(c), "Con0")
    MOI.set(model, MOI.ConstraintName(), [c], ["Con1"])
    @test MOI.get(model, MOI.ConstraintName(), [c]) == ["Con1"]
    @test MOI.get(
        model,
        MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.LessThan{T}},
        "Con1",
    ) == c
    @test MOI.get(
        model,
        MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}},
        "Con1",
    ) === nothing
    @test MOI.get(
        model,
        MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.GreaterThan{T}},
        "Con1",
    ) === nothing
    @test MOI.get(
        model,
        MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.LessThan{T}},
        "Con2",
    ) === nothing
    @test MOI.get(model, MOI.ConstraintIndex, "Con1") == c
    @test MOI.get(model, MOI.ConstraintIndex, "Con2") === nothing
    MOI.set(model, MOI.ConstraintName(), [c2, cy], ["Con2", "Con2"])
    @test_throws Exception MOI.get(model, MOI.ConstraintIndex, "Con2")
    @test_throws Exception MOI.get(model, typeof(c2), "Con2")
    @test_throws Exception MOI.get(model, typeof(cy), "Con2")
    MOI.set(model, MOI.ConstraintName(), cy, "Con4")
    for (i, ca) in zip([1, 2, 4], [c, c2, cy])
        namea = "Con$i"
        @test MOI.get(model, MOI.ConstraintName(), ca) == namea
        @test MOI.get(model, typeof(ca), namea) == ca
        @test MOI.get(model, MOI.ConstraintIndex, namea) == ca
        for cb in [c, c2, cy]
            if ca === cb
                continue
            end
            nameb = MOI.get(model, MOI.ConstraintName(), cb)
            @test MOI.get(model, typeof(cb), namea) === nothing
            @test MOI.get(model, typeof(ca), nameb) === nothing
        end
    end
    if _supports(config, MOI.delete)
        MOI.delete(model, v[2])
        @test MOI.get(model, MOI.VariableIndex, "Var2") === nothing
        MOI.delete(model, c)
        @test MOI.get(model, typeof(c), "Con1") === nothing
        @test MOI.get(model, MOI.ConstraintIndex, "Con1") === nothing
        MOI.delete(model, x)
        @test MOI.get(model, MOI.VariableIndex, "Varx") === nothing
        @test MOI.get(model, MOI.ConstraintIndex, "Con3") === nothing
        @test MOI.get(model, typeof(c2), "Con2") === c2
        @test MOI.get(model, MOI.ConstraintIndex, "Con2") === c2
        MOI.delete(model, y)
        @test MOI.get(model, typeof(cy), "Con4") === nothing
        @test MOI.get(model, MOI.ConstraintIndex, "Con4") === nothing
        for i in 1:4
            @test MOI.get(model, MOI.VariableIndex, "Vary$i") === nothing
        end
        MOI.set(model, MOI.ConstraintName(), c2, "Con4")
        @test MOI.get(model, typeof(c2), "Con4") === c2
        @test MOI.get(model, MOI.ConstraintIndex, "Con4") === c2
    end
    return
end

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

Test duplicate variable names.
"""
function test_model_duplicate_VariableName(model::MOI.ModelLike, config::Config)
    (x, y, z) = MOI.add_variables(model, 3)
    MOI.set(model, MOI.VariableName(), x, "x")
    MOI.set(model, MOI.VariableName(), y, "x")
    MOI.set(model, MOI.VariableName(), z, "z")
    @test MOI.get(model, MOI.VariableIndex, "z") == z
    @test_throws ErrorException MOI.get(model, MOI.VariableIndex, "x")
    MOI.set(model, MOI.VariableName(), y, "y")
    @test MOI.get(model, MOI.VariableIndex, "x") == x
    @test MOI.get(model, MOI.VariableIndex, "y") == y
    MOI.set(model, MOI.VariableName(), z, "x")
    @test_throws ErrorException MOI.get(model, MOI.VariableIndex, "x")
    if _supports(config, MOI.delete)
        MOI.delete(model, x)
        @test MOI.get(model, MOI.VariableIndex, "x") == z
    end
    return
end

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

Test duplicate names in ScalarAffineFunction constraints.
"""
function test_model_duplicate_ScalarAffineFunction_ConstraintName(
    model::MOI.ModelLike,
    config::Config{T},
) where {T}
    x = MOI.add_variables(model, 3)
    fs = [
        MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(T(1), xi)], T(0)) for
        xi in x
    ]
    c = MOI.add_constraints(model, fs, MOI.GreaterThan(T(0)))
    MOI.set(model, MOI.ConstraintName(), c[1], "x")
    MOI.set(model, MOI.ConstraintName(), c[2], "x")
    MOI.set(model, MOI.ConstraintName(), c[3], "z")
    @test MOI.get(model, MOI.ConstraintIndex, "z") == c[3]
    @test_throws ErrorException MOI.get(model, MOI.ConstraintIndex, "x")
    MOI.set(model, MOI.ConstraintName(), c[2], "y")
    @test MOI.get(model, MOI.ConstraintIndex, "x") == c[1]
    @test MOI.get(model, MOI.ConstraintIndex, "y") == c[2]
    MOI.set(model, MOI.ConstraintName(), c[3], "x")
    @test_throws ErrorException MOI.get(model, MOI.ConstraintIndex, "x")
    if _supports(config, MOI.delete)
        MOI.delete(model, c[1])
        @test MOI.get(model, MOI.ConstraintIndex, "x") == c[3]
    end
    return
end

"""
    test_model_is_valid(model::MOI.ModelLike, config::Config{T}) where {T}

Test various parts of `MOI.is_valid`.

Taken from https://github.com/jump-dev/MathOptInterfaceUtilities.jl/issues/41
"""
function test_model_is_valid(model::MOI.ModelLike, config::Config{T}) where {T}
    @requires MOI.supports_incremental_interface(model)
    v = MOI.add_variables(model, 2)
    @test MOI.is_valid(model, v[1])
    @test MOI.is_valid(model, v[2])
    x = MOI.add_variable(model)
    @test MOI.is_valid(model, x)
    if _supports(config, MOI.delete)
        MOI.delete(model, x)
        @test !MOI.is_valid(model, x)
    end
    cf = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(T(1), v), T(0))
    @requires MOI.supports_constraint(model, typeof(cf), MOI.LessThan{T})
    c = MOI.add_constraint(model, cf, MOI.LessThan(T(1)))
    @test MOI.is_valid(model, c)
    SAF = MOI.ScalarAffineFunction
    @test !MOI.is_valid(
        model,
        MOI.ConstraintIndex{SAF{Float32},MOI.LessThan{Float32}}(1),
    )
    @test !MOI.is_valid(
        model,
        MOI.ConstraintIndex{SAF{Float32},MOI.LessThan{T}}(1),
    )
    @test !MOI.is_valid(
        model,
        MOI.ConstraintIndex{SAF{T},MOI.LessThan{Float32}}(1),
    )
    VQF = MOI.VectorQuadraticFunction
    @test !MOI.is_valid(
        model,
        MOI.ConstraintIndex{VQF{T},MOI.SecondOrderCone}(1),
    )
    return
end

"""
    test_model_empty(model::MOI.ModelLike, ::Config)

Test `MOI.empty` and `MOI.is_empty!`.
"""
function test_model_empty(model::MOI.ModelLike, ::Config)
    @test MOI.is_empty(model)
    x = MOI.add_variable(model)
    @test !MOI.is_empty(model)
    MOI.empty!(model)
    @test MOI.is_empty(model)
    return
end

"""
    test_model_copy_to_UnsupportedConstraint(model::MOI.ModelLike, ::Config)

Test an error is thrown when a constraint is unsupported.

!!! note
    This function uses the two-argument `MOI.optimize!`. This ensures that we
    test three cases:
     * models implementing the two-argument method
     * models using the generic fallback and erroring on `copy_to`
     * models using the generic fallback and erroring on `optimize!`.
"""
function test_model_copy_to_UnsupportedConstraint(
    model::MOI.ModelLike,
    ::Config{T},
) where {T}
    @test !MOI.supports_constraint(
        model,
        MOI.VariableIndex,
        UnknownScalarSet{T},
    )
    @test_throws(
        MOI.UnsupportedConstraint,
        MOI.optimize!(model, BadConstraintModel()),
    )
    return
end

"""
    test_model_copy_to_UnsupportedAttribute(model::MOI.ModelLike, ::Config)

Test an error is thrown when an attribute is unsupported.

!!! note
    This function uses the two-argument `MOI.optimize!`. This ensures that we
    test three cases:
     * models implementing the two-argument method
     * models using the generic fallback and erroring on `copy_to`
     * models using the generic fallback and erroring on `optimize!`.
"""
function test_model_copy_to_UnsupportedAttribute(
    model::MOI.ModelLike,
    ::Config{T},
) where {T}
    # ModelAttribute
    @test !MOI.supports(model, UnknownModelAttribute())
    @test_throws(
        MOI.UnsupportedAttribute,
        MOI.optimize!(model, BadModelAttributeModel(T)),
    )
    # VariableAttribute
    @test !MOI.supports(model, UnknownVariableAttribute(), MOI.VariableIndex)
    @test_throws(
        MOI.UnsupportedAttribute,
        MOI.optimize!(model, BadVariableAttributeModel(T)),
    )
    # ConstraintAttribute
    @test !MOI.supports(
        model,
        UnknownConstraintAttribute(),
        MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{T}},
    )
    @test_throws(
        MOI.UnsupportedAttribute,
        MOI.optimize!(model, BadConstraintAttributeModel(T)),
    )
    return
end

function test_model_supports_constraint_VariableIndex_EqualTo(
    model::MOI.ModelLike,
    ::Config{T},
) where {T}
    @requires MOI.supports_constraint(model, MOI.VariableIndex, MOI.EqualTo{T})
    # Pick a "bad" coefficient type that should fail tests.
    @test !MOI.supports_constraint(model, MOI.VariableIndex, MOI.EqualTo{UInt8})
    # Scalar-in-vector
    @test !MOI.supports_constraint(model, MOI.VariableIndex, MOI.Zeros)
    return
end

function test_model_supports_constraint_ScalarAffineFunction_EqualTo(
    model::MOI.ModelLike,
    ::Config{T},
) where {T}
    @requires MOI.supports_constraint(
        model,
        MOI.ScalarAffineFunction{T},
        MOI.EqualTo{T},
    )
    # Pick a "bad" coefficient type that should fail tests.
    @test !MOI.supports_constraint(
        model,
        MOI.ScalarAffineFunction{UInt8},
        MOI.EqualTo{UInt8},
    )
    return
end

function test_model_supports_constraint_VectorOfVariables_Nonnegatives(
    model::MOI.ModelLike,
    ::Config{T},
) where {T}
    @requires MOI.supports_constraint(
        model,
        MOI.VectorOfVariables,
        MOI.Nonnegatives,
    )
    # Pick a "bad" coefficient type that should fail tests.
    @test !MOI.supports_constraint(
        model,
        MOI.VectorOfVariables,
        UnknownVectorSet,
    )
    @test !MOI.supports_constraint(model, MOI.VectorOfVariables, MOI.EqualTo{T})
    return
end

"""
    test_model_ordered_indices(model::MOI.ModelLike, ::Config{T}) where {T}

Test whether the model returns ListOfVariableIndices and ListOfConstraintIndices
sorted by creation time.
"""
function test_model_ordered_indices(model::MOI.ModelLike, ::Config{T}) where {T}
    @requires MOI.supports_incremental_interface(model)
    v1 = MOI.add_variable(model)
    @test MOI.get(model, MOI.ListOfVariableIndices()) == [v1]
    v2 = MOI.add_variable(model)
    @test MOI.get(model, MOI.ListOfVariableIndices()) == [v1, v2]
    MOI.delete(model, v1)
    @test MOI.get(model, MOI.ListOfVariableIndices()) == [v2]
    v3 = MOI.add_variable(model)
    @test MOI.get(model, MOI.ListOfVariableIndices()) == [v2, v3]
    v4 = MOI.add_variable(model)
    @test MOI.get(model, MOI.ListOfVariableIndices()) == [v2, v3, v4]
    # Note: there are too many combinations to test, so we're just going to
    # check VariableIndex-in-LessThan and hope it works for the rest
    c1 = MOI.add_constraint(model, v2, MOI.LessThan(T(1)))
    @test c1.value == v2.value
    @test MOI.get(
        model,
        MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.LessThan{T}}(),
    ) == [c1]
    c2 = MOI.add_constraint(model, v3, MOI.LessThan(T(2)))
    @test c2.value == v3.value
    @test MOI.get(
        model,
        MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.LessThan{T}}(),
    ) == [c1, c2]
    MOI.delete(model, c1)
    @test MOI.get(
        model,
        MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.LessThan{T}}(),
    ) == [c2]
    c3 = MOI.add_constraint(model, v4, MOI.LessThan(T(3)))
    @test c3.value == v4.value
    @test MOI.get(
        model,
        MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.LessThan{T}}(),
    ) == [c2, c3]
    return
end

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

Test adding a linear constraint with a non-zero function constant.

This should either work, or error with `MOI.ScalarFunctionConstantNotZero` if
the model does not support it.

To skip this test, pass `MOI.ScalarFunctionConstantNotZero` to the `exclude`
argument of [`Config`](@ref).
"""
function test_model_ScalarFunctionConstantNotZero(
    model::MOI.ModelLike,
    config::Config{T},
) where {T}
    @requires _supports(config, MOI.ScalarFunctionConstantNotZero)
    function _error(S, value)
        F = MOI.ScalarAffineFunction{T}
        return MOI.ScalarFunctionConstantNotZero{T,F,S}(value)
    end
    try
        f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{T}[], T(1))
        c = MOI.add_constraint(model, f, MOI.EqualTo(T(2)))
        @requires _supports(config, MOI.ConstraintFunction)
        @test MOI.get(model, MOI.ConstraintFunction(), c) ≈ f
    catch err
        @test err == _error(MOI.EqualTo{T}, T(1))
    end
    try
        f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{T}[], T(2))
        c = MOI.add_constraint(model, f, MOI.GreaterThan(T(1)))
        @requires _supports(config, MOI.ConstraintFunction)
        @test MOI.get(model, MOI.ConstraintFunction(), c) ≈ f
    catch err
        @test err == _error(MOI.GreaterThan{T}, T(2))
    end
    return
end

"""
    test_model_LowerBoundAlreadySet(
        model::MOI.ModelLike,
        ::Config{T},
    ) where {T}

Test that setting a lower-bound twice throws `LowerBoundAlreadySet`.
"""
function test_model_LowerBoundAlreadySet(
    model::MOI.ModelLike,
    config::Config{T},
) where {T}
    @requires MOI.supports_constraint(
        model,
        MOI.VariableIndex,
        MOI.GreaterThan{T},
    )
    @requires _supports(config, MOI.delete)
    x = MOI.add_variable(model)
    lb = T(0)
    sets = [MOI.EqualTo(lb), MOI.Interval(lb, lb)]
    set2 = MOI.GreaterThan(lb)
    for set1 in sets
        if !MOI.supports_constraint(model, MOI.VariableIndex, typeof(set1))
            continue
        end
        ci = MOI.add_constraint(model, x, set1)
        err = MOI.LowerBoundAlreadySet{typeof(set1),typeof(set2)}(x)
        @test_throws err MOI.add_constraint(model, x, set2)
        MOI.delete(model, ci)
        ci = MOI.add_constraint(model, x, set2)
        err = MOI.LowerBoundAlreadySet{typeof(set2),typeof(set1)}(x)
        @test_throws err MOI.add_constraint(model, x, set1)
        MOI.delete(model, ci)
    end
    return
end

"""
    test_model_UpperBoundAlreadySet(
        model::MOI.ModelLike,
        ::Config{T},
    ) where {T}

Test that setting a lower-bound twice throws `UpperBoundAlreadySet`.
"""
function test_model_UpperBoundAlreadySet(
    model::MOI.ModelLike,
    ::Config{T},
) where {T}
    x = MOI.add_variable(model)
    ub = T(0)
    @requires MOI.supports_constraint(model, MOI.VariableIndex, MOI.LessThan{T})
    sets = [MOI.EqualTo(ub), MOI.Interval(ub, ub)]
    set2 = MOI.LessThan(ub)
    for set1 in sets
        if !MOI.supports_constraint(model, MOI.VariableIndex, typeof(set1))
            continue
        end
        ci = MOI.add_constraint(model, x, set1)
        err = MOI.UpperBoundAlreadySet{typeof(set1),typeof(set2)}(x)
        @test_throws err MOI.add_constraint(model, x, set2)
        MOI.delete(model, ci)
        ci = MOI.add_constraint(model, x, set2)
        err = MOI.UpperBoundAlreadySet{typeof(set2),typeof(set1)}(x)
        @test_throws err MOI.add_constraint(model, x, set1)
        MOI.delete(model, ci)
    end
    return
end

"""
    test_model_delete(model::MOI.ModelLike, config::Config{T}) where {T}

Test various operations to do with deleting variables and constraints.
"""
function test_model_delete(model::MOI.ModelLike, config::Config{T}) where {T}
    @requires _supports(config, MOI.delete)
    x = MOI.add_variable(model)
    cx = MOI.add_constraint(model, x, MOI.GreaterThan(T(0)))
    y = MOI.add_variables(model, 4)
    cy = MOI.add_constraint(model, y, MOI.Nonpositives(4))
    @test MOI.is_valid(model, x)
    @test MOI.is_valid(model, y[1])
    @test MOI.is_valid(model, y[2])
    @test MOI.is_valid(model, y[3])
    @test MOI.is_valid(model, y[4])
    @test MOI.is_valid(model, cx)
    @test MOI.is_valid(model, cy)
    @test MOI.get(model, MOI.ConstraintFunction(), cx) == x
    @test MOI.get(model, MOI.ConstraintSet(), cx) == MOI.GreaterThan(T(0))
    @test MOI.get(model, MOI.ConstraintFunction(), cy) ==
          MOI.VectorOfVariables(y)
    @test MOI.get(model, MOI.ConstraintSet(), cy) == MOI.Nonpositives(4)
    @test Set(MOI.get(model, MOI.ListOfConstraintTypesPresent())) == Set([
        (MOI.VariableIndex, MOI.GreaterThan{T}),
        (MOI.VectorOfVariables, MOI.Nonpositives),
    ])
    @test MOI.get(
        model,
        MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{T}}(),
    ) == [cx]
    @test MOI.get(
        model,
        MOI.ListOfConstraintIndices{MOI.VectorOfVariables,MOI.Nonpositives}(),
    ) == [cy]
    MOI.delete(model, y[3])
    @test MOI.is_valid(model, x)
    @test MOI.is_valid(model, y[1])
    @test MOI.is_valid(model, y[2])
    @test !MOI.is_valid(model, y[3])
    @test MOI.is_valid(model, y[4])
    @test MOI.is_valid(model, cx)
    @test MOI.is_valid(model, cy)
    @test MOI.get(model, MOI.ConstraintFunction(), cx) == x
    @test MOI.get(model, MOI.ConstraintSet(), cx) == MOI.GreaterThan(T(0))
    @test MOI.get(model, MOI.ConstraintFunction(), cy) ==
          MOI.VectorOfVariables(y[[1, 2, 4]])
    @test MOI.get(model, MOI.ConstraintSet(), cy) == MOI.Nonpositives(3)
    @test Set(MOI.get(model, MOI.ListOfConstraintTypesPresent())) == Set([
        (MOI.VariableIndex, MOI.GreaterThan{T}),
        (MOI.VectorOfVariables, MOI.Nonpositives),
    ])
    @test MOI.get(
        model,
        MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{T}}(),
    ) == [cx]
    @test MOI.get(
        model,
        MOI.ListOfConstraintIndices{MOI.VectorOfVariables,MOI.Nonpositives}(),
    ) == [cy]
    MOI.delete(model, y[1])
    @test MOI.is_valid(model, x)
    @test !MOI.is_valid(model, y[1])
    @test MOI.is_valid(model, y[2])
    @test !MOI.is_valid(model, y[3])
    @test MOI.is_valid(model, y[4])
    @test MOI.is_valid(model, cx)
    @test MOI.is_valid(model, cy)
    @test MOI.get(model, MOI.ConstraintFunction(), cx) == x
    @test MOI.get(model, MOI.ConstraintSet(), cx) == MOI.GreaterThan(T(0))
    @test MOI.get(model, MOI.ConstraintFunction(), cy) ==
          MOI.VectorOfVariables(y[[2, 4]])
    @test MOI.get(model, MOI.ConstraintSet(), cy) == MOI.Nonpositives(2)
    @test Set(MOI.get(model, MOI.ListOfConstraintTypesPresent())) == Set([
        (MOI.VariableIndex, MOI.GreaterThan{T}),
        (MOI.VectorOfVariables, MOI.Nonpositives),
    ])
    @test MOI.get(
        model,
        MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{T}}(),
    ) == [cx]
    @test MOI.get(
        model,
        MOI.ListOfConstraintIndices{MOI.VectorOfVariables,MOI.Nonpositives}(),
    ) == [cy]
    MOI.delete(model, x)
    @test !MOI.is_valid(model, x)
    @test !MOI.is_valid(model, y[1])
    @test MOI.is_valid(model, y[2])
    @test !MOI.is_valid(model, y[3])
    @test MOI.is_valid(model, y[4])
    @test !MOI.is_valid(model, cx)
    @test MOI.is_valid(model, cy)
    @test MOI.get(model, MOI.ConstraintFunction(), cy) ==
          MOI.VectorOfVariables(y[[2, 4]])
    @test MOI.get(model, MOI.ConstraintSet(), cy) == MOI.Nonpositives(2)
    @test MOI.get(model, MOI.ListOfConstraintTypesPresent()) ==
          [(MOI.VectorOfVariables, MOI.Nonpositives)]
    @test isempty(
        MOI.get(
            model,
            MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{T}}(),
        ),
    )
    @test MOI.get(
        model,
        MOI.ListOfConstraintIndices{MOI.VectorOfVariables,MOI.Nonpositives}(),
    ) == [cy]
    MOI.delete(model, y[[2, 4]])
    @test !MOI.is_valid(model, x)
    @test !MOI.is_valid(model, y[1])
    @test !MOI.is_valid(model, y[2])
    @test !MOI.is_valid(model, y[3])
    @test !MOI.is_valid(model, y[4])
    @test !MOI.is_valid(model, cx)
    @test !MOI.is_valid(model, cy)
    @test isempty(MOI.get(model, MOI.ListOfConstraintTypesPresent()))
    @test isempty(
        MOI.get(
            model,
            MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{T}}(),
        ),
    )
    @test isempty(
        MOI.get(
            model,
            MOI.ListOfConstraintIndices{
                MOI.VectorOfVariables,
                MOI.Nonpositives,
            }(),
        ),
    )
end

"""
    test_model_ListOfConstraintAttributesSet(
        model::MOI.ModelLike,
        config::Config{T},
    ) where {T}

Test issue #1429: ConstraintName should only be included in
`ListOfConstraintAttributesSet` if it actually is set.
"""
function test_model_ListOfConstraintAttributesSet(
    model::MOI.ModelLike,
    ::Config{T},
) where {T}
    @requires MOI.supports(
        model,
        MOI.ConstraintName(),
        MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}},
    )
    x = MOI.add_variable(model)
    MOI.add_constraint(model, x, MOI.GreaterThan(T(0)))
    c = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(T(1), x)], T(0)),
        MOI.EqualTo(T(1)),
    )
    MOI.set(model, MOI.ConstraintName(), c, "c")
    @test MOI.get(
        model,
        MOI.ListOfConstraintAttributesSet{
            MOI.ScalarAffineFunction{T},
            MOI.EqualTo{T},
        }(),
    ) == [MOI.ConstraintName()]
    @test MOI.get(
        model,
        MOI.ListOfConstraintAttributesSet{
            MOI.VariableIndex,
            MOI.GreaterThan{T},
        }(),
    ) == []
    return
end

"""
    test_model_ModelFilter_AbstractModelAttribute(
        src::MOI.ModelLike,
        ::Config{T},
    ) where {T}

Tests `Utilties.ModelFilter` of `AbstractModelAttribute`.
"""
function test_model_ModelFilter_AbstractModelAttribute(
    src::MOI.ModelLike,
    ::Config{T},
) where {T}
    @requires MOI.supports(src, MOI.Name())
    MOI.set(src, MOI.Name(), "src")
    dest = MOI.Utilities.Model{T}()
    MOI.copy_to(dest, MOI.Utilities.ModelFilter(src) do item
        return item != MOI.Name()
    end)
    @test MOI.get(dest, MOI.Name()) == ""
    return
end

"""
    test_model_ModelFilter_AbstractVariableAttribute(
        src::MOI.ModelLike,
        ::Config{T},
    ) where {T}

Tests `Utilties.ModelFilter` of `AbstractVariableAttribute`.
"""
function test_model_ModelFilter_AbstractVariableAttribute(
    src::MOI.ModelLike,
    ::Config{T},
) where {T}
    @requires MOI.supports(src, MOI.VariableName(), MOI.VariableIndex)
    @requires MOI.supports(src, MOI.VariablePrimalStart(), MOI.VariableIndex)
    x = MOI.add_variable(src)
    MOI.set(src, MOI.VariableName(), x, "x")
    MOI.set(src, MOI.VariablePrimalStart(), x, T(1))
    dest = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}())
    index_map = MOI.copy_to(dest, MOI.Utilities.ModelFilter(src) do item
        return item != MOI.VariableName()
    end)
    @test MOI.get(dest, MOI.VariableName(), index_map[x]) == ""
    @test MOI.get(dest, MOI.VariablePrimalStart(), index_map[x]) == T(1)
    return
end

"""
    test_model_ModelFilter_AbstractConstraintAttribute(
        src::MOI.ModelLike,
        ::Config{T},
    ) where {T}

Tests `Utilties.ModelFilter` of `AbstractConstraintAttribute`.
"""
function test_model_ModelFilter_AbstractConstraintAttribute(
    src::MOI.ModelLike,
    ::Config{T},
) where {T}
    @requires(
        MOI.supports(
            src,
            MOI.ConstraintName(),
            MOI.ConstraintIndex{MOI.VectorOfVariables,MOI.Nonnegatives},
        ),
    )
    @requires(
        MOI.supports(
            src,
            MOI.ConstraintDualStart(),
            MOI.ConstraintIndex{MOI.VectorOfVariables,MOI.Nonnegatives},
        ),
    )
    @requires(
        MOI.supports_constraint(src, MOI.VectorOfVariables, MOI.Nonnegatives),
    )
    x = MOI.add_variable(src)
    c = MOI.add_constraint(src, MOI.VectorOfVariables([x]), MOI.Nonnegatives(1))
    MOI.set(src, MOI.ConstraintName(), c, "c")
    MOI.set(src, MOI.ConstraintDualStart(), c, T[1])
    dest = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}())
    index_map = MOI.copy_to(dest, MOI.Utilities.ModelFilter(src) do item
        if item isa MOI.AbstractConstraintAttribute
            return item != MOI.ConstraintName()
        end
        return true
    end)
    @test MOI.get(dest, MOI.ConstraintName(), index_map[c]) == ""
    @test MOI.get(dest, MOI.ConstraintDualStart(), index_map[c]) == T[1]
    return
end

"""
    test_model_ModelFilter_ListOfConstraintIndices(
        src::MOI.ModelLike,
        ::Config{T},
    ) where {T}

Tests `Utilties.ModelFilter` of `ListOfConstraintIndices`.
"""
function test_model_ModelFilter_ListOfConstraintIndices(
    src::MOI.ModelLike,
    ::Config{T},
) where {T}
    @requires(
        MOI.supports_constraint(src, MOI.VariableIndex, MOI.GreaterThan{T}),
    )
    x = MOI.add_variables(src, 4)
    MOI.add_constraint.(src, x, MOI.GreaterThan(T(0)))
    dest = MOI.Utilities.Model{T}()
    index_map = MOI.copy_to(dest, MOI.Utilities.ModelFilter(src) do item
        if item isa MOI.ConstraintIndex
            return isodd(item.value)
        end
        return true
    end)
    @test MOI.get(dest, MOI.NumberOfVariables()) == 4
    @test MOI.get(
        dest,
        MOI.NumberOfConstraints{MOI.VariableIndex,MOI.GreaterThan{T}}(),
    ) == 2
    for xi in x
        @test haskey(index_map, xi)
        ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{T}}(xi.value)
        @test haskey(index_map, ci) == isodd(xi.value)
    end
    return
end

"""
    test_model_ModelFilter_ListOfConstraintTypesPresent(
        src::MOI.ModelLike,
        ::Config{T},
    ) where {T}

Tests `Utilties.ModelFilter` of `ListOfConstraintTypesPresent`.
"""
function test_model_ModelFilter_ListOfConstraintTypesPresent(
    src::MOI.ModelLike,
    ::Config{T},
) where {T}
    @requires(
        MOI.supports_constraint(src, MOI.VariableIndex, MOI.GreaterThan{T}),
    )
    @requires(MOI.supports_constraint(src, MOI.VariableIndex, MOI.LessThan{T}))
    x = MOI.add_variables(src, 4)
    MOI.add_constraint.(src, x, MOI.GreaterThan(T(0)))
    MOI.add_constraint.(src, x, MOI.LessThan(T(1)))
    dest = MOI.Utilities.Model{T}()
    _ = MOI.copy_to(
        dest,
        MOI.Utilities.ModelFilter(src) do item
            if item isa Tuple{Type,Type}
                return item == (MOI.VariableIndex, MOI.LessThan{T})
            end
            return true
        end,
    )
    @test MOI.get(dest, MOI.NumberOfVariables()) == 4
    attr = MOI.NumberOfConstraints{MOI.VariableIndex,MOI.GreaterThan{T}}()
    @test MOI.get(dest, attr) == 0
    attr = MOI.NumberOfConstraints{MOI.VariableIndex,MOI.LessThan{T}}()
    @test MOI.get(dest, attr) == 4
    return
end


using JSON
function dump(net::Network,file_str::String)::Nothing
    dict = Dict("layers" => net.layers,
                "neurons" => net.neurons,
                "weights" => net.weights,
                "biases" => net.biases,
                "z" => net.z,
                "batch" => net.batch,
                "epochs" => net.epochs,
                "η" => net.η)

    open(file_str, "w") do file
        write(file, JSON.json(dict))
    end

    println("Saved to $(file_str)")
    return nothing
end


using Tables

abstract type AbstractConnector end

Base.length(conn::AbstractConnector) = Inf
hasnext(conn::AbstractConnector) = true
reset!(conn::AbstractConnector) = nothing

mutable struct TablesConnector <: AbstractConnector
    rows
    state::Int
    args::Dict{Symbol, Any}
end

function TablesConnector(data; shuffle::Bool = false)
    if !Tables.istable(data)
        throw(ArgumentError("data must have the Tables.jl interface"))
    end

    if shuffle
        data = data[Random.shuffle(1:size(data,1)), :]
    end

    return TablesConnector(Tables.rows(data), 0, Dict{Symbol, Any}())
end

function TablesConnector(data, orderBy::Symbol; rev::Bool = false)
    if !(orderBy in propertynames(data))
        throw(ArgumentError("data doesn't have the column $orderBy"))
    end

    data = sort(data, orderBy, rev = rev)

    return TablesConnector(data)
end

TablesConnector(filename::String) = TablesConnector(CSV.read(filename; header = false))

Base.length(conn::TablesConnector) = length(conn.rows)
hasnext(conn::TablesConnector) = conn.state < length(conn)

function next(conn::TablesConnector)
    if conn.state >= length(conn)
        return nothing
    end

    conn.state += 1

    return DataFrame([conn.rows[conn.state]])
end

reset!(conn::TablesConnector) = conn.state = 0

mutable struct GeneratorConnector <: AbstractConnector
    generator::Function
    args::Dict{Symbol, Any}
end

function GeneratorConnector(generator::Function;
                            args...)
    return GeneratorConnector(generator, args)
end

function next(conn::GeneratorConnector)
    total = 100
    data = conn.generator(;n_samples = total, conn.args...)

    return DataFrame(data[1 + Int(floor(rand(1,1)[1] .* size(data)[1])), :])
end


module TestFunctions

using MathOptInterface: VectorOfVariables
using Test
using MathOptInterface
import MutableArithmetics

const MOI = MathOptInterface
const MA = MutableArithmetics

function runtests()
    for name in names(@__MODULE__; all = true)
        if startswith("$(name)", "test_")
            @testset "$(name)" begin
                getfield(@__MODULE__, name)()
            end
        end
    end
    return
end

const w = MOI.VariableIndex(0)
const x = MOI.VariableIndex(1)
const y = MOI.VariableIndex(2)
const z = MOI.VariableIndex(3)

# Number-like but not subtype of `Number`
struct NonNumber
    value::Int
end
Base.:*(a::NonNumber, b::NonNumber) = NonNumber(a.value * b.value)
Base.:+(a::NonNumber, b::NonNumber) = NonNumber(a.value + b.value)
Base.zero(::Type{NonNumber}) = NonNumber(0)
function Base.isapprox(a::NonNumber, b::NonNumber; kws...)
    return isapprox(a.value, b.value; kws...)
end

function test_NonNumber()
    two = NonNumber(2)
    three = NonNumber(3)
    six = NonNumber(6)
    three_x = MOI.Utilities.operate(*, NonNumber, three, x)
    six_x = MOI.Utilities.operate(*, NonNumber, six, x)
    @test six_x ≈ two * three_x
    @test six_x ≈ three_x * two
    return
end

function test_Vectorization_vectorize()
    g = MOI.VectorAffineFunction(
        MOI.VectorAffineTerm.([3, 1], MOI.ScalarAffineTerm.([5, 2], [y, x])),
        [3, 1, 4],
    )
    g1 = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(2, x)], 3)
    g2 = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{Int}[], 1)
    g3 = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(5, y)], 4)
    @test g ≈ MOI.Utilities.vectorize([g1, g2, g3])
    vov = MOI.Utilities.vectorize(MOI.VariableIndex[])
    @test MOI.output_dimension(vov) == 0
    @test vov isa MOI.VectorOfVariables
    aff = MOI.Utilities.vectorize(MOI.ScalarAffineFunction{Int}[])
    @test MOI.output_dimension(aff) == 0
    @test aff isa MOI.VectorAffineFunction{Int}
    quad = MOI.Utilities.vectorize(MOI.ScalarQuadraticFunction{Int}[])
    @test MOI.output_dimension(quad) == 0
    @test quad isa MOI.VectorQuadraticFunction{Int}
    return
end

function test_Vectorization_operate_vcat()
    g = MOI.VectorAffineFunction(
        MOI.VectorAffineTerm.([3, 1], MOI.ScalarAffineTerm.([5, 2], [y, x])),
        [3, 1, 4],
    )
    v = MOI.VectorOfVariables([y, w])
    for T in [Int, Float64]
        @test MOI.VectorOfVariables == MOI.Utilities.promote_operation(
            vcat,
            T,
            typeof(w),
            typeof(v),
            typeof(x),
        )
        vov = MOI.Utilities.operate(vcat, T, w, v, x)
        @test vov.variables == [w, y, w, x]
        @test MOI.VectorOfVariables == MOI.Utilities.promote_operation(
            vcat,
            T,
            typeof(v),
            typeof(w),
            typeof(x),
        )
        vov = MOI.Utilities.operate(vcat, T, v, w, x)
        @test vov.variables == [y, w, w, x]
        @test MOI.VectorOfVariables == MOI.Utilities.promote_operation(
            vcat,
            T,
            typeof(w),
            typeof(x),
            typeof(v),
        )
        vov = MOI.Utilities.operate(vcat, T, w, x, v)
        @test vov.variables == [w, x, y, w]
    end
    f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2, 4], [x, z]), 5)
    g = MOI.VectorAffineFunction(
        MOI.VectorAffineTerm.([3, 1], MOI.ScalarAffineTerm.([5, 2], [y, x])),
        [3, 1, 4],
    )
    @test MOI.Utilities.promote_operation(
        vcat,
        Int,
        typeof(w),
        typeof(f),
        typeof(v),
        Int,
        typeof(g),
        typeof(x),
        Int,
    ) == MOI.VectorAffineFunction{Int}
    F = MOI.Utilities.operate(vcat, Int, w, f, v, 3, g, x, -4)
    expected_terms =
        MOI.VectorAffineTerm.(
            [1, 2, 2, 3, 4, 8, 6, 9],
            MOI.ScalarAffineTerm.(
                [1, 2, 4, 1, 1, 5, 2, 1],
                [w, x, z, y, w, y, x, x],
            ),
        )
    expected_constants = [0, 5, 0, 0, 3, 3, 1, 4, 0, -4]
    F = MOI.Utilities.operate(vcat, Int, w, f, v, 3, g, x, -4)
    @test F.terms == expected_terms
    @test F.constants == expected_constants
    return
end

function test_promote_operation_Quadratic()
    @test MOI.Utilities.promote_operation(
        vcat,
        Int,
        MOI.VectorQuadraticFunction{Int},
        MOI.VariableIndex,
        MOI.ScalarQuadraticFunction{Int},
        MOI.VectorOfVariables,
        Int,
        MOI.ScalarQuadraticFunction{Int},
        MOI.VectorQuadraticFunction{Int},
        MOI.VariableIndex,
        Int,
    ) == MOI.VectorQuadraticFunction{Int}
    return
end

function test_MultirowChange_construction()
    chg1 = MOI.MultirowChange(w, [(Int32(2), 2.0), (Int32(1), 3.0)])
    chg2 = MOI.MultirowChange(w, [(Int64(2), 2.0), (Int64(1), 3.0)])
    @test chg1.variable == chg2.variable
    @test chg1.new_coefficients == chg2.new_coefficients
    return
end

function test_VectorAffineTerm_VectorQuadraticTerm_construction()
    scalaraffine = MOI.ScalarAffineTerm(2.0, z)
    @test MOI.VectorAffineTerm(Int32(3), scalaraffine) ===
          MOI.VectorAffineTerm(Int64(3), scalaraffine)
    @test MOI.VectorAffineTerm{Float64}(Int32(3), scalaraffine) ===
          MOI.VectorAffineTerm(Int64(3), scalaraffine)
    scalarquad = MOI.ScalarQuadraticTerm(2.0, y, z)
    @test MOI.VectorQuadraticTerm(Int32(3), scalarquad) ===
          MOI.VectorQuadraticTerm(Int64(3), scalarquad)
    @test MOI.VectorQuadraticTerm{Float64}(Int32(3), scalarquad) ===
          MOI.VectorQuadraticTerm(Int64(3), scalarquad)
    return
end

function test_eval_variables()
    # We do tests twice to make sure the function is not modified
    vals = Dict(w => 0, x => 3, y => 1, z => 5)
    @test MOI.output_dimension(z) == 1
    @test MOI.Utilities.eval_variables(vi -> vals[vi], z) ≈ 5
    @test MOI.Utilities.eval_variables(vi -> vals[vi], z) ≈ 5
    fvv = MOI.VectorOfVariables([x, z, y])
    @test MOI.output_dimension(fvv) == 3
    @test MOI.Utilities.eval_variables(vi -> vals[vi], fvv) ≈ [3, 5, 1]
    @test MOI.Utilities.eval_variables(vi -> vals[vi], fvv) ≈ [3, 5, 1]
    fsa = MOI.ScalarAffineFunction(
        [
            MOI.ScalarAffineTerm(1.0, x),
            MOI.ScalarAffineTerm(3.0, z),
            MOI.ScalarAffineTerm(2.0, y),
        ],
        2.0,
    )
    @test MOI.output_dimension(fsa) == 1
    @test MOI.Utilities.eval_variables(vi -> vals[vi], fsa) ≈ 22
    @test MOI.Utilities.eval_variables(vi -> vals[vi], fsa) ≈ 22
    fva = MOI.VectorAffineFunction(
        MOI.VectorAffineTerm.(
            [2, 1, 2],
            MOI.ScalarAffineTerm.([1.0, 3.0, 2.0], [x, z, y]),
        ),
        [-3.0, 2.0],
    )
    @test MOI.output_dimension(fva) == 2
    @test MOI.Utilities.eval_variables(vi -> vals[vi], fva) ≈ [12, 7]
    @test MOI.Utilities.eval_variables(vi -> vals[vi], fva) ≈ [12, 7]
    fsq = MOI.ScalarQuadraticFunction(
        MOI.ScalarQuadraticTerm.(1.0, [x, w, w], [z, z, y]),
        MOI.ScalarAffineTerm.(1.0, [x, y]),
        -3.0,
    )
    @test MOI.output_dimension(fsq) == 1
    @test MOI.Utilities.eval_variables(vi -> vals[vi], fsq) ≈ 16
    @test MOI.Utilities.eval_variables(vi -> vals[vi], fsq) ≈ 16
    fvq = MOI.VectorQuadraticFunction(
        MOI.VectorQuadraticTerm.(
            [1, 2, 2],
            MOI.ScalarQuadraticTerm.(1.0, [x, w, w], [z, z, y]),
        ),
        MOI.VectorAffineTerm.([2, 1], MOI.ScalarAffineTerm.(1.0, [x, y])),
        [-3.0, -2.0],
    )
    @test MOI.output_dimension(fvq) == 2
    @test MOI.Utilities.eval_variables(vi -> vals[vi], fvq) ≈ [13, 1]
    @test MOI.Utilities.eval_variables(vi -> vals[vi], fvq) ≈ [13, 1]
    return
end

function test_substitute_variables()
    # We do tests twice to make sure the function is not modified
    subs = Dict(w => 1.0y + 1.0z, x => 2.0y + 1.0, y => 1.0y, z => -1.0w)
    vals = Dict(w => 0.0, x => 3.0, y => 1.0, z => 5.0)
    subs_vals = Dict(w => 6.0, x => 3.0, y => 1.0, z => 0.0)
    fsa = x + 3.0z + 2.0y + 2.0
    subs_sa = -3.0w + 4.0y + 3.0
    @test MOI.Utilities.eval_variables(vi -> subs_vals[vi], fsa) ==
          MOI.Utilities.eval_variables(vi -> vals[vi], subs_sa)
    @test MOI.Utilities.substitute_variables(vi -> subs[vi], fsa) ≈ subs_sa
    @test MOI.Utilities.substitute_variables(vi -> subs[vi], fsa) ≈ subs_sa
    fva = MOI.Utilities.operate(vcat, Float64, 3.0z - 3.0, x + 2.0y + 2.0)
    subs_va = MOI.Utilities.operate(vcat, Float64, -3.0w - 3.0, 4.0y + 3.0)
    @test MOI.Utilities.eval_variables(vi -> subs_vals[vi], fva) ==
          MOI.Utilities.eval_variables(vi -> vals[vi], subs_va)
    @test MOI.Utilities.substitute_variables(vi -> subs[vi], fva) ≈ subs_va
    @test MOI.Utilities.substitute_variables(vi -> subs[vi], fva) ≈ subs_va
    fsq = 1.0x + 1.0y + 1.0x * z + 1.0w * z + 1.0w * y + 2.0w * w - 3.0
    #     2y + 1 +     y - 2yw - w    + (y + z) * (-w) + (y + z) * y  + 2 (y + z) * (y + z) - 3
    #     2y     +     y - 2yw - w    - yw - zw        + y^2 + zy     + 2y^2 + 2z^2 + 4yz   - 2
    #     3y - w + 3y^2 + 2z^2 - 3yw - zw + 5yz - 2
    subs_sq =
        3.0y - 1.0w + 3.0y * y + 2.0z * z - 3.0y * w - 1.0z * w + 5.0y * z - 2.0
    @test MOI.Utilities.eval_variables(vi -> subs_vals[vi], fsq) ==
          MOI.Utilities.eval_variables(vi -> vals[vi], subs_sq)
    @test MOI.Utilities.substitute_variables(vi -> subs[vi], fsq) ≈ subs_sq
    @test MOI.Utilities.substitute_variables(vi -> subs[vi], fsq) ≈ subs_sq
    fvq = MOI.Utilities.operate(
        vcat,
        Float64,
        1.0y + 1.0x * z - 3.0,
        1.0x + 1.0w * z + 1.0w * y - 2.0,
    )
    subs_vq = MOI.Utilities.operate(
        vcat,
        Float64,
        1.0y - w - 2.0y * w - 3.0,
        2.0y + 1.0y * y - 1.0w * y - 1.0w * z + 1.0y * z - 1.0,
    )
    @test MOI.Utilities.eval_variables(vi -> subs_vals[vi], fvq) ==
          MOI.Utilities.eval_variables(vi -> vals[vi], subs_vq)
    @test MOI.Utilities.substitute_variables(vi -> subs[vi], fvq) ≈ subs_vq
    @test MOI.Utilities.substitute_variables(vi -> subs[vi], fvq) ≈ subs_vq

    complex_aff = 2.0im * y
    # Test that variables can be substituted for `MOI.ScalarAffineFunction{S}`
    # in a `MOI.ScalarAffineFunction{T}` where `S != T`.
    @test MOI.Utilities.substitute_variables(vi -> 1.5x, complex_aff) ≈
          3.0im * x
    float_aff = 2.0 * y
    @test MOI.Utilities.substitute_variables(vi -> 3x, float_aff) ≈ 6.0 * x
    complex_quad = 1.5im * y * y
    @test MOI.Utilities.substitute_variables(vi -> 2x, complex_quad) ≈
          6.0im * x * x
    int_quad = 3 * y * x
    @test MOI.Utilities.substitute_variables(vi -> true * y, int_quad) ≈
          3 * y * y
    return
end

function test_map_indices()
    fsq = MOI.ScalarQuadraticFunction(
        MOI.ScalarQuadraticTerm.(1.0, [x, w, w], [z, z, y]),
        MOI.ScalarAffineTerm.(1.0, [x, y]),
        -3.0,
    )
    index_map = Dict(x => y, y => z, w => w, z => x)
    gsq = MOI.Utilities.map_indices(index_map, fsq)
    sats = MOI.ScalarAffineTerm.(1.0, [y, z])
    sqts = MOI.ScalarQuadraticTerm.(1.0, [y, w, w], [x, x, z])
    @test gsq.affine_terms == sats
    @test gsq.quadratic_terms == sqts
    @test gsq.constant == -3.0
    fvq = MOI.VectorQuadraticFunction(
        MOI.VectorQuadraticTerm.(
            [1, 2, 2],
            MOI.ScalarQuadraticTerm.(1.0, [x, w, w], [z, z, y]),
        ),
        MOI.VectorAffineTerm.([2, 1], MOI.ScalarAffineTerm.(1.0, [x, y])),
        [-3.0, -2.0],
    )
    gvq = MOI.Utilities.map_indices(index_map, fvq)
    @test gvq.affine_terms == MOI.VectorAffineTerm.([2, 1], sats)
    @test gvq.quadratic_terms == MOI.VectorQuadraticTerm.([1, 2, 2], sqts)
    @test MOI.Utilities.constant_vector(gvq) == [-3.0, -2.0]
    @test MOI.Utilities.map_indices(index_map, :s) == :s
    @test MOI.Utilities.map_indices(index_map, [:a, :b]) == [:a, :b]
    @test MOI.Utilities.map_indices(index_map, "s") == "s"
    @test MOI.Utilities.map_indices(index_map, ["a", "b"]) == ["a", "b"]
    return
end

function test_Conversion_VectorOfVariables_VectorAffineFunction()
    f = MOI.VectorAffineFunction{Int}(MOI.VectorOfVariables([z, x, y]))
    @test f isa MOI.VectorAffineFunction{Int}
    @test f.terms ==
          MOI.VectorAffineTerm.(
        1:3,
        MOI.ScalarAffineTerm.(ones(Int, 3), [z, x, y]),
    )
    @test all(iszero.(MOI.Utilities.constant_vector(f)))
    f = MOI.VectorAffineFunction{Float64}(MOI.VectorOfVariables([x, w]))
    @test f isa MOI.VectorAffineFunction{Float64}
    @test f.terms ==
          MOI.VectorAffineTerm.(1:2, MOI.ScalarAffineTerm.(1.0, [x, w]))
    @test all(iszero.(MOI.Utilities.constant_vector(f)))
    return
end

function test_iteration_and_indexing_on_VectorOfVariables()
    f = MOI.VectorOfVariables([z, w, x, y])
    it = MOI.Utilities.eachscalar(f)
    @test length(it) == 4
    @test eltype(it) == MOI.VariableIndex
    @test collect(it) == [z, w, x, y]
    @test it[2] == w
    @test it[end] == y
    return
end

function test_indexing_on_VectorAffineFunction()
    f = MOI.VectorAffineFunction(
        MOI.VectorAffineTerm.(
            [2, 1, 3, 2, 2, 1, 3, 1, 2],
            MOI.ScalarAffineTerm.(
                [1, 7, 2, 9, 3, 1, 6, 4, 1],
                [x, y, z, z, y, z, x, x, y],
            ),
        ),
        [2, 7, 5],
    )
    it = MOI.Utilities.eachscalar(f)
    @test length(it) == 3
    @test eltype(it) == MOI.ScalarAffineFunction{Int}
    g = it[2]
    @test g isa MOI.ScalarAffineFunction
    @test g.terms == MOI.ScalarAffineTerm.([1, 9, 3, 1], [x, z, y, y])
    @test g.constant == 7
    g = it[1]
    @test g isa MOI.ScalarAffineFunction
    @test g.terms == MOI.ScalarAffineTerm.([7, 1, 4], [y, z, x])
    @test g.constant == 2
    g = it[end]
    @test g isa MOI.ScalarAffineFunction
    @test g.terms == MOI.ScalarAffineTerm.([2, 6], [z, x])
    @test g.constant == 5
    h = it[[3, 1]]
    @test h isa MOI.VectorAffineFunction
    @test sort(h.terms, by = t -> t.output_index) ==
          MOI.VectorAffineTerm.(
        [1, 1, 2, 2, 2],
        MOI.ScalarAffineTerm.([2, 6, 7, 1, 4], [z, x, y, z, x]),
    )
    @test MOI.Utilities.constant_vector(h) == [5, 2]
    F = MOI.Utilities.operate(vcat, Int, it[[1, 2]], it[3])
    @test F isa MOI.VectorAffineFunction{Int}
    @test sort(F.terms, by = t -> t.output_index) ==
          MOI.VectorAffineTerm.(
        [1, 1, 1, 2, 2, 2, 2, 3, 3],
        MOI.ScalarAffineTerm.(
            [7, 1, 4, 1, 9, 3, 1, 2, 6],
            [y, z, x, x, z, y, y, z, x],
        ),
    )
    @test MOI.Utilities.constant_vector(F) == MOI.Utilities.constant_vector(f)
    return
end

function test_indexing_on_VectorQuadraticFunction()
    f = MOI.VectorQuadraticFunction(
        MOI.VectorQuadraticTerm.(
            [2, 3, 1, 2],
            MOI.ScalarQuadraticTerm.([1, 6, 4, 3], [z, x, x, y], [y, z, z, y]),
        ),
        MOI.VectorAffineTerm.(
            [2, 1, 3, 2, 2],
            MOI.ScalarAffineTerm.([1, 7, 2, 9, 3], [x, y, z, z, y]),
        ),
        [2, 7, 5],
    )
    it = MOI.Utilities.eachscalar(f)
    @test length(it) == 3
    @test eltype(it) == MOI.ScalarQuadraticFunction{Int}
    g = it[2]
    @test g isa MOI.ScalarQuadraticFunction
    @test g.affine_terms == MOI.ScalarAffineTerm.([1, 9, 3], [x, z, y])
    @test g.quadratic_terms == MOI.ScalarQuadraticTerm.([1, 3], [z, y], [y, y])
    @test g.constant == 7
    g = it[end]
    @test g isa MOI.ScalarQuadraticFunction
    @test g.affine_terms == MOI.ScalarAffineTerm.([2], [z])
    @test g.quadratic_terms == MOI.ScalarQuadraticTerm.([6], [x], [z])
    @test g.constant == 5
    return
end

function test_Scalar_Variable_isone()
    f = MOI.VariableIndex(0)
    g = MOI.VariableIndex(1)
    @test !isone(f)
    @test !isone(g)
    return
end

function test_Scalar_Variable_iszero()
    f = MOI.VariableIndex(0)
    g = MOI.VariableIndex(1)
    @test !iszero(f)
    @test !iszero(g)
    @test f + 1 ≈ 1 + f
    @test (f + 1.0) - 1.0 ≈ (2.0f) / 2.0
    @test (f - 1.0) + 1.0 ≈ (2.0f) / 2.0
    @test (1.0 + f) - 1.0 ≈ (f * 2.0) / 2.0
    @test 1.0 - (1.0 - f) ≈ (f / 2.0) * 2.0
    return
end

function test_Scalar_Variable_complex()
    f = MOI.VariableIndex(0)
    @test real(f) === f
    @test MA.promote_operation(real, typeof(f)) == typeof(f)
    for T in [Int, Int32, Float64]
        z = MOI.Utilities.operate(imag, T, f)
        @test z isa MOI.ScalarAffineFunction{T}
        @test iszero(z)
        @test MOI.Utilities.promote_operation(imag, T, typeof(f)) == typeof(z)
    end
    @test conj(f) === f
    @test MA.promote_operation(conj, typeof(f)) == typeof(f)
    return
end

function test_Scalar_Affine_zero()
    f = @inferred MOI.Utilities.zero(MOI.ScalarAffineFunction{Float64})
    @test iszero(f)
    @test MOI.Utilities.isapprox_zero(f, 1e-16)
    return
end

function test_Scalar_Affine_complex()
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    r = 3x + 4y
    c = 2x + 5y
    f = (1 + 0im) * r + c * im
    @test real(f) ≈ r
    @test MOI.Utilities.operate(imag, Int, f) ≈ c
    @test imag(f) ≈ c
    @test conj(f) ≈ (1 + 0im) * r - c * im
    @test MA.promote_operation(real, typeof(f)) == typeof(r)
    @test MA.promote_operation(imag, typeof(f)) == typeof(c)
    @test MA.promote_operation(conj, typeof(f)) == typeof(f)
    return
end

function test_Scalar_Affine_promote_operation()
    @test MOI.Utilities.promote_operation(-, Int, MOI.VariableIndex) ==
          MOI.ScalarAffineFunction{Int}
    @test MOI.Utilities.promote_operation(
        -,
        Int,
        MOI.ScalarAffineFunction{Int},
    ) == MOI.ScalarAffineFunction{Int}
    @test MOI.Utilities.promote_operation(
        +,
        Float64,
        MOI.VariableIndex,
        MOI.VariableIndex,
    ) == MOI.ScalarAffineFunction{Float64}
    @test MOI.Utilities.promote_operation(
        +,
        Float64,
        MOI.ScalarAffineFunction{Float64},
        Float64,
    ) == MOI.ScalarAffineFunction{Float64}
    @test MOI.Utilities.promote_operation(
        +,
        Int,
        MOI.ScalarAffineFunction{Int},
        MOI.ScalarAffineFunction{Int},
    ) == MOI.ScalarAffineFunction{Int}
    return
end

function test_Scalar_Affine_comparison()
    @test MOI.Utilities.operate(+, Float64, x, z) + 1.0 ≈
          MOI.ScalarAffineFunction(
        MOI.ScalarAffineTerm.([1, 1e-7, 1], [x, y, z]),
        1.0,
    ) atol = 1e-6
    f1 = MOI.ScalarAffineFunction(
        [MOI.ScalarAffineTerm(1.0, x), MOI.ScalarAffineTerm(1e-7, y)],
        1.0,
    )
    f2 = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 1.0)
    @test f1 ≈ f2 atol = 1e-6
    fdiff = f1 - f2
    @test !iszero(fdiff)
    @test iszero(f1 - f1)
    @test iszero(f2 - f2)
    MOI.Utilities.canonicalize!(fdiff)
    @test !MOI.Utilities.isapprox_zero(fdiff, 1e-8)
    @test MOI.Utilities.isapprox_zero(fdiff, 1e-6)
    return
end

function test_Scalar_Affine_canonical()
    f = MOI.Utilities.canonical(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([2, 1, 3, -2, -3], [y, x, z, x, z]),
            5,
        ),
    )
    @test MOI.output_dimension(f) == 1
    @test f.terms == MOI.ScalarAffineTerm.([-1, 2], [x, y])
    @test f.constant == 5
    f = MOI.Utilities.canonical(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([1, 3, 1, 2, -3, 2], [w, y, w, x, x, z]),
            2,
        ) + MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([-1, -2, -2, 3, 2], [y, z, w, x, y]),
            3,
        ),
    )
    @test f.terms == MOI.ScalarAffineTerm.([2, 4], [x, y])
    @test f.constant == 5
end

function test_Scalar_Affine_convert()
    f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, 0.5], [x, y]), 0.5)
    @test_throws InexactError convert(MOI.VariableIndex, f)
    @test_throws InexactError MOI.Utilities.convert_approx(MOI.VariableIndex, f)
    @test MOI.Utilities.convert_approx(MOI.VariableIndex, f, tol = 0.5) == x
    @test convert(typeof(f), f) === f
    quad_f = MOI.ScalarQuadraticFunction(
        MOI.ScalarQuadraticTerm{Float64}[],
        f.terms,
        f.constant,
    )
    @test convert(MOI.ScalarQuadraticFunction{Float64}, f) ≈ quad_f
    for g in [
        convert(MOI.ScalarAffineFunction{Float64}, x),
        convert(MOI.ScalarAffineFunction{Float64}, 1x),
    ]
        @test g isa MOI.ScalarAffineFunction{Float64}
        @test convert(MOI.VariableIndex, g) == x
        @test MOI.Utilities.convert_approx(MOI.VariableIndex, g) == x
    end
    return
end

function test_Scalar_Affine_operate_with_Float_coefficient()
    f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, 4.0], [x, y]), 5.0)
    @test f ≈ 2.0f / 2.0
    return
end

function test_Scalar_Affine_operate_with_Int_coefficient()
    f = MOI.Utilities.canonical(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([1, 3, 1, 2, -3, 2], [w, y, w, x, x, z]),
            2,
        ) + MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([-1, -2, -2, 3, 2], [y, z, w, x, y]),
            3,
        ),
    )
    @test f === +f
    @test f ≈
          x + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1, 4], [x, y]), 5)
    @test f ≈ f * 1
    @test f ≈
          MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1, 2], [x, y]), 2) *
          2 + 1
    @test f ≈
          x -
          MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([-1, -4], [x, y]), -5)
    @test f ≈
          MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([3, 4], [x, y]), 5) - x
    return
end

function test_Scalar_Affine_modification()
    f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2, 4], [x, y]), 0)
    f = MOI.Utilities.modify_function(f, MOI.ScalarConstantChange(6))
    @test f.constant == 6
    g = deepcopy(f)
    @test g ≈ f
    f = MOI.Utilities.modify_function(f, MOI.ScalarCoefficientChange(y, 3))
    @test !(g ≈ f)
    @test g.terms == MOI.ScalarAffineTerm.([2, 4], [x, y])
    @test f.terms == MOI.ScalarAffineTerm.([2, 3], [x, y])
    f = MOI.Utilities.modify_function(f, MOI.ScalarCoefficientChange(x, 0))
    @test f.terms == MOI.ScalarAffineTerm.([3], [y])
    return
end

function test_Scalar_Quadratic_zero()
    f = @inferred MOI.Utilities.zero(MOI.ScalarQuadraticFunction{Float64})
    @test MOI.Utilities.isapprox_zero(f, 1e-16)
    return
end

function test_Scalar_Quadratic_promote_operation()
    @test MOI.Utilities.promote_operation(
        -,
        Int,
        MOI.ScalarQuadraticFunction{Int},
    ) == MOI.ScalarQuadraticFunction{Int}
    @test MOI.Utilities.promote_operation(
        +,
        Int,
        MOI.ScalarQuadraticFunction{Int},
        MOI.ScalarQuadraticFunction{Int},
    ) == MOI.ScalarQuadraticFunction{Int}
    @test MOI.Utilities.promote_operation(
        +,
        Int,
        MOI.ScalarQuadraticFunction{Int},
        MOI.ScalarAffineFunction{Int},
    ) == MOI.ScalarQuadraticFunction{Int}
    @test MOI.Utilities.promote_operation(
        +,
        Int,
        MOI.ScalarAffineFunction{Int},
        MOI.ScalarQuadraticFunction{Int},
    ) == MOI.ScalarQuadraticFunction{Int}
    @test MOI.Utilities.promote_operation(
        *,
        Int,
        MOI.VariableIndex,
        MOI.VariableIndex,
    ) == MOI.ScalarQuadraticFunction{Int}
    @test MOI.Utilities.promote_operation(
        *,
        Float64,
        MOI.VariableIndex,
        MOI.ScalarAffineFunction{Float64},
    ) == MOI.ScalarQuadraticFunction{Float64}
    @test MOI.Utilities.promote_operation(
        *,
        Int,
        MOI.ScalarAffineFunction{Int},
        MOI.VariableIndex,
    ) == MOI.ScalarQuadraticFunction{Int}
    @test MOI.Utilities.promote_operation(
        *,
        Float64,
        MOI.ScalarAffineFunction{Float64},
        MOI.ScalarAffineFunction{Float64},
    ) == MOI.ScalarQuadraticFunction{Float64}
    @test MOI.Utilities.promote_operation(
        /,
        Float64,
        MOI.ScalarQuadraticFunction{Float64},
        Float64,
    ) == MOI.ScalarQuadraticFunction{Float64}
    return
end

function test_Scalar_Quadratic_Comparison_With_iszero()
    f = 7 + 3x + 1x * x + 2y * y + 3x * y
    MOI.Utilities.canonicalize!(f)
    @test !iszero(f)
    @test iszero(0 * f)
    @test iszero(f - f)
    return
end

function test_Scalar_Quadratic_Comparison_With_tolerance()
    f = 7 + 3x + 1x * x + 2y * y + 3x * y
    MOI.Utilities.canonicalize!(f)
    @test !MOI.Utilities.isapprox_zero(f, 1e-8)
    # Test isapprox_zero with zero terms
    @test MOI.Utilities.isapprox_zero(0 * f, 1e-8)
    g = 1.0x * y - (1 + 1e-6) * y * x
    MOI.Utilities.canonicalize!(g)
    @test MOI.Utilities.isapprox_zero(g, 1e-5)
    @test !MOI.Utilities.isapprox_zero(g, 1e-7)
    return
end

function test_Scalar_Quadratic_convert()
    f = 7 + 3x + 1x * x + 2y * y + 3x * y
    MOI.Utilities.canonicalize!(f)
    @test_throws InexactError convert(MOI.VariableIndex, f)
    @test_throws InexactError convert(MOI.ScalarAffineFunction{Int}, f)
    g = convert(MOI.ScalarQuadraticFunction{Float64}, x)
    @test convert(MOI.VariableIndex, g) == x
    return
end

function test_Scalar_Quadratic_Power_Affine()
    f = 7 + 3x + 1x * x + 2y * y + 3x * y
    aff = 1x + 2 + y
    @test isone(@inferred aff^0)
    @test convert(typeof(f), aff) ≈ @inferred aff^1
    @test aff * aff ≈ @inferred aff^2
    err = ArgumentError("Cannot take $(typeof(aff)) to the power 3.")
    @test_throws err aff^3
    return
end

function test_Scalar_Quadratic_Power_Quadratic()
    f = 7 + 3x + 1x * x + 2y * y + 3x * y
    @test isone(@inferred f^0)
    @test f ≈ @inferred f^1
    err = ArgumentError("Cannot take $(typeof(f)) to the power 2.")
    @test_throws err f^2
    return
end

function test_Scalar_Quadratic_no_zero_affine_term()
    # Test that no affine 0y term is created when multiplying 1x by y
    for xy in [1x * y, x * 1y]
        @test isempty(xy.affine_terms)
        @test length(xy.quadratic_terms) == 1
        @test xy.quadratic_terms[1] == MOI.ScalarQuadraticTerm(1, x, y) ||
              xy.quadratic_terms[1] == MOI.ScalarQuadraticTerm(1, y, x)
    end
    for xx in [1x * x, x * 1x]
        @test isempty(xx.affine_terms)
        @test length(xx.quadratic_terms) == 1
        @test xx.quadratic_terms[1] == MOI.ScalarQuadraticTerm(2, x, x)
    end
    return
end

function test_Scalar_Quadratic_operate!()
    q = 1.0x + 1.0y + (1.0x) * z + (1.0w) * z
    @test q ≈ 1.0x + 1.0y + (1.0w) * z + (1.0x) * z
    # This calls
    aff = 1.0x + 1.0y
    # which tries to mutate `aff`, gets a quadratic expression
    # and mutate it with the remaining term
    @test MOI.Utilities.operate!(+, Float64, aff, (1.0x) * z, (1.0w) * z) ≈ q
    return
end

function test_Scalar_Quadratic_operate()
    f = 7 + 3x + 1x * x + 2y * y + 3x * y
    @test f ≈ 7 + (x + 2y) * (1x + y) + 3x
    @test f ≈ -(-7 - 3x) + (x + 2y) * (1x + y)
    @test f ≈ -((x + 2y) * (MOI.Utilities.operate(-, Int, x) - y)) + 3x + 7
    @test f ≈ 7 + MOI.Utilities.operate(*, Int, x, x) + 3x * (y + 1) + 2y * y
    @test f ≈ (x + 2) * (x + 1) + (y + 1) * (2y + 3x) + (5 - 3x - 2y)
    @test f ≈ begin
        MOI.ScalarQuadraticFunction(
            MOI.ScalarQuadraticTerm.([2], [x], [x]),
            [MOI.ScalarAffineTerm(3, x)],
            4,
        ) + MOI.ScalarQuadraticFunction(
            MOI.ScalarQuadraticTerm.([4, 3], [y, x], [y, y]),
            MOI.ScalarAffineTerm{Int}[],
            3,
        )
    end
    @test f ≈ begin
        MOI.ScalarQuadraticFunction(
            MOI.ScalarQuadraticTerm.([2], [x], [x]),
            [MOI.ScalarAffineTerm(3, x)],
            10,
        ) - MOI.ScalarQuadraticFunction(
            MOI.ScalarQuadraticTerm.([-4, -3], [y, x], [y, y]),
            MOI.ScalarAffineTerm{Int}[],
            3,
        )
    end
    @test f ≈ begin
        MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(3, x)], 5) +
        MOI.ScalarQuadraticFunction(
            MOI.ScalarQuadraticTerm.([2, 4, 3], [x, y, x], [x, y, y]),
            MOI.ScalarAffineTerm{Int}[],
            2,
        )
    end
    @test f ≈ begin
        MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(3, x)], 5) -
        MOI.ScalarQuadraticFunction(
            MOI.ScalarQuadraticTerm.([-2, -4, -3], [x, y, x], [x, y, y]),
            MOI.ScalarAffineTerm{Int}[],
            -2,
        )
    end
    @test f ≈ begin
        MOI.ScalarQuadraticFunction(
            MOI.ScalarQuadraticTerm.([2, 4, 3], [x, y, x], [x, y, y]),
            MOI.ScalarAffineTerm{Int}[],
            2,
        ) + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(3, x)], 5)
    end
    @test f ≈ begin
        MOI.ScalarQuadraticFunction(
            MOI.ScalarQuadraticTerm.([2, 4, 3], [x, y, x], [x, y, y]),
            MOI.ScalarAffineTerm{Int}[],
            12,
        ) - MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(-3, x)], 5)
    end
    @test f ≈ begin
        MOI.ScalarQuadraticFunction(
            MOI.ScalarQuadraticTerm.([2, 4, 3], [x, y, x], [x, y, y]),
            MOI.ScalarAffineTerm.([2], [x]),
            7,
        ) + x
    end
    @test f ≈
          MOI.ScalarQuadraticFunction(
        MOI.ScalarQuadraticTerm.([2, 4, 3], [x, y, x], [x, y, y]),
        MOI.ScalarAffineTerm.([3], [x]),
        10,
    ) - 3
    @test f ≈
          2.0 * MOI.ScalarQuadraticFunction(
        MOI.ScalarQuadraticTerm.([2.0, 4.0, 3.0], [x, y, x], [x, y, y]),
        MOI.ScalarAffineTerm.([3.0], [x]),
        7.0,
    ) / 2.0
    return
end

function test_Scalar_Quadratic_modification()
    f = 7 + 3x + 1x * x + 2y * y + 3x * y
    f = MOI.Utilities.modify_function(f, MOI.ScalarConstantChange(9))
    @test f.constant == 9
    f = MOI.Utilities.modify_function(f, MOI.ScalarCoefficientChange(y, 0))
    @test f.affine_terms == MOI.ScalarAffineTerm.([3], [x])
    g = deepcopy(f)
    @test f ≈ g
    f = MOI.Utilities.modify_function(f, MOI.ScalarCoefficientChange(y, 2))
    @test !(f ≈ g)
    @test g.affine_terms == MOI.ScalarAffineTerm.([3], [x])
    @test f.affine_terms == MOI.ScalarAffineTerm.([3, 2], [x, y])
    return
end

function test_Vector_Variable()
    f = MOI.VectorOfVariables([MOI.VariableIndex(1), MOI.VariableIndex(2)])
    @test real(f) === f
    @test MA.promote_operation(real, typeof(f)) == typeof(f)
    for T in [Int, Int32, Float64]
        z = MOI.Utilities.operate(imag, T, f)
        @test z isa MOI.VectorAffineFunction{T}
        @test isempty(z.terms)
        @test all(iszero, z.constants)
        @test MOI.output_dimension(z) == MOI.output_dimension(f)
        @test MOI.Utilities.promote_operation(imag, T, typeof(f)) == typeof(z)
    end
    @test conj(f) === f
    @test MA.promote_operation(conj, typeof(f)) == typeof(f)
end

function test_Vector_Affine_complex()
    x = MOI.VectorOfVariables([MOI.VariableIndex(1)])
    y = MOI.VectorOfVariables([MOI.VariableIndex(2)])
    r = 3x + 4y
    c = 2x + 5y
    f = (1 + 0im) * r + c * im
    @test real(f) ≈ r
    @test MOI.Utilities.operate(imag, Int, f) ≈ c
    @test imag(f) ≈ c
    @test conj(f) ≈ (1 + 0im) * r - c * im
    @test MA.promote_operation(real, typeof(f)) == typeof(r)
    @test MA.promote_operation(imag, typeof(f)) == typeof(c)
    @test MA.promote_operation(conj, typeof(f)) == typeof(f)
    return
end

function test_Vector_Affine()
    f = MOI.Utilities.canonical(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [2, 1, 2, 1, 1, 2, 2, 2, 2, 1, 1, 2, 1, 2],
                MOI.ScalarAffineTerm.(
                    [3, 2, 3, -3, -1, -2, 3, -2, 1, 3, 5, -2, 0, -1],
                    [x, x, z, y, y, x, y, z, x, y, y, x, x, z],
                ),
            ),
            [5, 7],
        ),
    )
    @test MOI.output_dimension(f) == 2
    @test f.terms ==
          MOI.VectorAffineTerm.(
        [1, 1, 2],
        MOI.ScalarAffineTerm.([2, 4, 3], [x, y, y]),
    )
    @test MOI.Utilities.constant_vector(f) == [5, 7]
    f = MOI.Utilities.modify_function(f, MOI.VectorConstantChange([6, 8]))
    @test MOI.Utilities.constant_vector(f) == [6, 8]
    g = deepcopy(f)
    @test f ≈ g
    f = MOI.Utilities.modify_function(f, MOI.MultirowChange(y, [(2, 9)]))
    @test !(f ≈ g)
    @test f.terms ==
          MOI.VectorAffineTerm.(
        [1, 1, 2],
        MOI.ScalarAffineTerm.([2, 4, 9], [x, y, y]),
    )
    @test g.terms ==
          MOI.VectorAffineTerm.(
        [1, 1, 2],
        MOI.ScalarAffineTerm.([2, 4, 3], [x, y, y]),
    )
    f = MOI.Utilities.modify_function(f, MOI.MultirowChange(y, [(1, 0)]))
    @test f.terms ==
          MOI.VectorAffineTerm.([1, 2], MOI.ScalarAffineTerm.([2, 9], [x, y]))
    return
end

function test_Vector_Quadratic()
    f = MOI.VectorQuadraticFunction(
        MOI.VectorQuadraticTerm.(
            [1, 1, 2],
            MOI.ScalarQuadraticTerm.([1, 2, 3], [x, y, x], [x, y, y]),
        ),
        MOI.VectorAffineTerm.(
            [1, 2, 2],
            MOI.ScalarAffineTerm.([3, 1, 2], [x, x, y]),
        ),
        [7, 3, 4],
    )
    @test MOI.output_dimension(f) == 3
    f = MOI.Utilities.modify_function(f, MOI.VectorConstantChange([10, 11, 12]))
    @test MOI.Utilities.constant_vector(f) == [10, 11, 12]
    f = MOI.Utilities.modify_function(
        f,
        MOI.MultirowChange(y, [(2, 0), (1, 1)]),
    )
    @test f.affine_terms ==
          MOI.VectorAffineTerm.(
        [1, 2, 1],
        MOI.ScalarAffineTerm.([3, 1, 1], [x, x, y]),
    )
    g = deepcopy(f)
    f = MOI.Utilities.modify_function(
        f,
        MOI.MultirowChange(x, [(1, 0), (3, 4)]),
    )
    @test f.affine_terms ==
          MOI.VectorAffineTerm.(
        [2, 1, 3],
        MOI.ScalarAffineTerm.([1, 1, 4], [x, y, x]),
    )
    @test g.affine_terms ==
          MOI.VectorAffineTerm.(
        [1, 2, 1],
        MOI.ScalarAffineTerm.([3, 1, 1], [x, x, y]),
    )
    return
end

function _isapprox_ordered(
    f1::T,
    f2::T,
) where {T<:Union{MOI.ScalarAffineFunction,MOI.VectorAffineFunction}}
    return (
        (MOI.term_indices.(f1.terms) == MOI.term_indices.(f2.terms)) &&
        (MOI.constant(f1) ≈ MOI.constant(f2)) &&
        (MOI.coefficient.(f1.terms) ≈ MOI.coefficient.(f2.terms))
    )
end

function _isapprox_ordered(
    f1::T,
    f2::T,
) where {T<:Union{MOI.ScalarQuadraticFunction,MOI.VectorQuadraticFunction}}
    return (
        (
            MOI.term_indices.(f1.affine_terms) ==
            MOI.term_indices.(f2.affine_terms)
        ) &&
        (
            MOI.term_indices.(f1.quadratic_terms) ==
            MOI.term_indices.(f2.quadratic_terms)
        ) &&
        (MOI.constant(f1) ≈ MOI.constant(f2)) &&
        (
            MOI.coefficient.(f1.affine_terms) ≈
            MOI.coefficient.(f2.affine_terms)
        ) &&
        (
            MOI.coefficient.(f1.quadratic_terms) ≈
            MOI.coefficient.(f2.quadratic_terms)
        )
    )
end

function _test_canonicalization(
    f::T,
    expected::T,
) where {
    T<:Union{
        MOI.ScalarAffineFunction,
        MOI.VectorAffineFunction,
        MOI.ScalarQuadraticFunction,
        MOI.VectorQuadraticFunction,
    },
}
    @test MOI.Utilities.is_canonical(expected)
    g = @inferred(MOI.Utilities.canonical(f))
    @test _isapprox_ordered(g, expected)
    @test MOI.Utilities.is_canonical(g)
    @test MOI.Utilities.is_canonical(expected)
    @test _isapprox_ordered(MOI.Utilities.canonical(g), g)
    @test MOI.Utilities.canonical(g) !== g
    @test @allocated(MOI.Utilities.canonicalize!(f)) == 0
    return
end

function test_canonical_ScalarAffine()
    @test MOI.Utilities.is_canonical(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex.(Int[])),
            1.0,
        ),
    )
    @test !MOI.Utilities.is_canonical(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([0.0], MOI.VariableIndex.([1])),
            1.0,
        ),
    )
    @test !MOI.Utilities.is_canonical(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([1.0, 2.0], MOI.VariableIndex.([2, 1])),
            2.0,
        ),
    )
    @test !MOI.Utilities.is_canonical(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([1.0, 0.0], MOI.VariableIndex.([1, 2])),
            2.0,
        ),
    )

    _test_canonicalization(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex[]),
            1.5,
        ),
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex[]),
            1.5,
        ),
    )
    _test_canonicalization(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([0.0], [MOI.VariableIndex(1)]),
            -2.0,
        ),
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex[]),
            -2.0,
        ),
    )
    _test_canonicalization(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([0.0], [MOI.VariableIndex(2)]),
            -2.0,
        ),
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex[]),
            -2.0,
        ),
    )
    _test_canonicalization(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([3.0], [MOI.VariableIndex(2)]),
            1.0,
        ),
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([3.0], [MOI.VariableIndex(2)]),
            1.0,
        ),
    )
    _test_canonicalization(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([3.0, 4.0], MOI.VariableIndex.([2, 1])),
            0.0,
        ),
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([4.0, 3.0], MOI.VariableIndex.([1, 2])),
            0.0,
        ),
    )
    _test_canonicalization(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([1.0, 0.1], MOI.VariableIndex.([1, 1])),
            5.0,
        ),
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([1.1], MOI.VariableIndex.([1])),
            5.0,
        ),
    )
    _test_canonicalization(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.(
                [1.0, 0.5, 2.0],
                MOI.VariableIndex.([1, 3, 1]),
            ),
            5.0,
        ),
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([3.0, 0.5], MOI.VariableIndex.([1, 3])),
            5.0,
        ),
    )
    _test_canonicalization(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.(
                [1.0, 0.5, 2.0],
                MOI.VariableIndex.([1, 3, 1]),
            ),
            5.0,
        ),
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([3.0, 0.5], MOI.VariableIndex.([1, 3])),
            5.0,
        ),
    )
    _test_canonicalization(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.(
                [1.0, 2.0, 3.0, 4.0, 5.0],
                MOI.VariableIndex.([1, 3, 1, 3, 2]),
            ),
            5.0,
        ),
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.(
                [4.0, 5.0, 6.0],
                MOI.VariableIndex.([1, 2, 3]),
            ),
            5.0,
        ),
    )
    _test_canonicalization(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([1.0, -1.0], MOI.VariableIndex.([7, 7])),
            5.0,
        ),
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex.(Int[])),
            5.0,
        ),
    )
    _test_canonicalization(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.(
                [1.0, -1.0, 0.5],
                MOI.VariableIndex.([7, 7, 2]),
            ),
            5.0,
        ),
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([0.5], MOI.VariableIndex.([2])),
            5.0,
        ),
    )
    _test_canonicalization(
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.(
                [1.0, -0.5, 0.5, -0.5],
                MOI.VariableIndex.([7, 7, 2, 7]),
            ),
            5.0,
        ),
        MOI.ScalarAffineFunction(
            MOI.ScalarAffineTerm.([0.5], MOI.VariableIndex.([2])),
            5.0,
        ),
    )
    return
end

function test_canonical_VectorAffine()
    @test MOI.Utilities.is_canonical(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                Int[],
                MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex.(Int[])),
            ),
            Float64[],
        ),
    )
    @test !MOI.Utilities.is_canonical(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [1],
                MOI.ScalarAffineTerm.([0.0], MOI.VariableIndex.([1])),
            ),
            [1.0],
        ),
    )
    @test !MOI.Utilities.is_canonical(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [1, 2],
                MOI.ScalarAffineTerm.([0.0, 1.0], MOI.VariableIndex.([1, 2])),
            ),
            [1.0],
        ),
    )
    @test !MOI.Utilities.is_canonical(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [2, 1],
                MOI.ScalarAffineTerm.([1.0, 1.0], MOI.VariableIndex.([1, 2])),
            ),
            [1.0],
        ),
    )
    @test !MOI.Utilities.is_canonical(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [1, 1],
                MOI.ScalarAffineTerm.([1.0, -1.0], MOI.VariableIndex.([1, 1])),
            ),
            [1.0],
        ),
    )
    @test !MOI.Utilities.is_canonical(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [1, 1],
                MOI.ScalarAffineTerm.([1.0, 1.0], MOI.VariableIndex.([1, 1])),
            ),
            [1.0],
        ),
    )

    _test_canonicalization(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                Int[],
                MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex.(Int[])),
            ),
            Float64[],
        ),
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                Int[],
                MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex.(Int[])),
            ),
            Float64[],
        ),
    )
    _test_canonicalization(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [3],
                MOI.ScalarAffineTerm.([0.0], MOI.VariableIndex.([5])),
            ),
            [1.0],
        ),
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                Int[],
                MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex.(Int[])),
            ),
            [1.0],
        ),
    )
    _test_canonicalization(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [3, 2],
                MOI.ScalarAffineTerm.([1.0, 2.0], MOI.VariableIndex.([5, 6])),
            ),
            [1.0, 2.0, 3.0],
        ),
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [2, 3],
                MOI.ScalarAffineTerm.([2.0, 1.0], MOI.VariableIndex.([6, 5])),
            ),
            [1.0, 2.0, 3.0],
        ),
    )
    _test_canonicalization(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [3, 3],
                MOI.ScalarAffineTerm.([1.0, 2.0], MOI.VariableIndex.([6, 5])),
            ),
            [1.0, 2.0, 3.0],
        ),
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [3, 3],
                MOI.ScalarAffineTerm.([2.0, 1.0], MOI.VariableIndex.([5, 6])),
            ),
            [1.0, 2.0, 3.0],
        ),
    )
    _test_canonicalization(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [3, 3],
                MOI.ScalarAffineTerm.([1.0, 2.0], MOI.VariableIndex.([1, 1])),
            ),
            [1.0, 2.0, 3.0],
        ),
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [3],
                MOI.ScalarAffineTerm.([3.0], MOI.VariableIndex.([1])),
            ),
            [1.0, 2.0, 3.0],
        ),
    )
    _test_canonicalization(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [1, 1, 1],
                MOI.ScalarAffineTerm.(
                    [1.0, 2.0, 3.0],
                    MOI.VariableIndex.([1, 2, 1]),
                ),
            ),
            [4.0, 5.0, 6.0],
        ),
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [1, 1],
                MOI.ScalarAffineTerm.([4.0, 2.0], MOI.VariableIndex.([1, 2])),
            ),
            [4.0, 5.0, 6.0],
        ),
    )
    _test_canonicalization(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [1, 1, 1],
                MOI.ScalarAffineTerm.(
                    [1.0, 2.0, 3.0],
                    MOI.VariableIndex.([2, 1, 1]),
                ),
            ),
            [4.0, 5.0, 6.0],
        ),
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [1, 1],
                MOI.ScalarAffineTerm.([5.0, 1.0], MOI.VariableIndex.([1, 2])),
            ),
            [4.0, 5.0, 6.0],
        ),
    )
    _test_canonicalization(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [2, 3, 3],
                MOI.ScalarAffineTerm.(
                    [1.0, 2.0, 3.0],
                    MOI.VariableIndex.([1, 1, 1]),
                ),
            ),
            [4.0, 5.0, 6.0],
        ),
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [2, 3],
                MOI.ScalarAffineTerm.([1.0, 5.0], MOI.VariableIndex.([1, 1])),
            ),
            [4.0, 5.0, 6.0],
        ),
    )
    _test_canonicalization(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [2, 3, 3],
                MOI.ScalarAffineTerm.(
                    [1.0, -3.0, 3.0],
                    MOI.VariableIndex.([1, 1, 1]),
                ),
            ),
            [4.0, 5.0, 6.0],
        ),
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [2],
                MOI.ScalarAffineTerm.([1.0], MOI.VariableIndex.([1])),
            ),
            [4.0, 5.0, 6.0],
        ),
    )
    _test_canonicalization(
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [2, 3, 3, 3],
                MOI.ScalarAffineTerm.(
                    [1.0, 3.0, -1.0, -2.0],
                    MOI.VariableIndex.([1, 1, 1, 1]),
                ),
            ),
            [4.0, 5.0, 6.0],
        ),
        MOI.VectorAffineFunction(
            MOI.VectorAffineTerm.(
                [2],
                MOI.ScalarAffineTerm.([1.0], MOI.VariableIndex.([1])),
            ),
            [4.0, 5.0, 6.0],
        ),
    )
    return
end

function test_canonical_ScalarQuadratic()
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    @test MOI.Utilities.is_canonical(
        convert(MOI.ScalarQuadraticFunction{Float64}, 1.0),
    )
    @test !MOI.Utilities.is_canonical(1.0x * y + 2.0x * x + 2.0)
    @test !MOI.Utilities.is_canonical(1.0x * x + 0.0x * y + 2.0)

    _test_canonicalization(
        convert(MOI.ScalarQuadraticFunction{Float64}, 1.5),
        convert(MOI.ScalarQuadraticFunction{Float64}, 1.5),
    )
    _test_canonicalization(
        0.0x * y - 2.0,
        convert(MOI.ScalarQuadraticFunction{Float64}, -2.0),
    )
    _test_canonicalization(3.0x * y + 4.0x * x + 0.0, 4.0x * x + 3.0x * y + 0.0)
    _test_canonicalization(1.0x * y + 0.1x * y + 5.0, 1.1x * y + 5.0)
    return
end

function test_canonical_VectorQuadratic()
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    @test MOI.Utilities.is_canonical(
        MOI.Utilities.operate(
            vcat,
            Float64,
            convert(MOI.ScalarQuadraticFunction{Float64}, 1.0),
        ),
    )
    @test !MOI.Utilities.is_canonical(
        MOI.Utilities.operate(vcat, Float64, 1.0x * y + 2.0x * x + 2.0),
    )
    @test !MOI.Utilities.is_canonical(
        MOI.Utilities.operate(vcat, Float64, 1.0x * x + 0.0x * y + 2.0),
    )

    _test_canonicalization(
        MOI.Utilities.operate(
            vcat,
            Float64,
            1.5,
            0.0x * y - 2.0,
            3.0x * y + 4.0x * x + 0.0,
            1.0x * y + 0.1x * y + 5.0,
        ),
        MOI.Utilities.operate(
            vcat,
            Float64,
            1.5,
            -2.0,
            4.0x * x + 3.0x * y + 0.0,
            1.1x * y + 5.0,
        ),
    )
    return
end

function test_vector_terms()
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    at = MOI.VectorAffineTerm(3, MOI.ScalarAffineTerm(10.0, x))
    qt = MOI.VectorQuadraticTerm(3, MOI.ScalarQuadraticTerm(6.0, x, y))
    @test MOI.Utilities.operate_term(*, 3.0, at) ==
          MOI.VectorAffineTerm(3, MOI.ScalarAffineTerm(30.0, x))
    @test MOI.Utilities.operate_term(*, at, at) ==
          MOI.VectorQuadraticTerm(3, MOI.ScalarQuadraticTerm(200.0, x, x))
    @test MOI.Utilities.operate_term(*, 3.0, qt) ==
          MOI.VectorQuadraticTerm(3, MOI.ScalarQuadraticTerm(18.0, x, y))
    @test MOI.Utilities.operate_term(/, at, 2.0) ==
          MOI.VectorAffineTerm(3, MOI.ScalarAffineTerm(5.0, x))
    @test MOI.Utilities.operate_term(/, qt, 3.0) ==
          MOI.VectorQuadraticTerm(3, MOI.ScalarQuadraticTerm(2.0, x, y))
end

function test_vector_promote()
    T = Float64
    for t1 in [MOI.VectorAffineFunction{T}, MOI.VectorOfVariables, Vector{T}]
        for t2 in [MOI.VectorAffineFunction{T}, MOI.VectorOfVariables]
            @test MOI.Utilities.promote_operation(+, T, t1, t2) ==
                  MOI.VectorAffineFunction{T}
            @test MOI.Utilities.promote_operation(-, T, t1, t2) ==
                  MOI.VectorAffineFunction{T}
        end
    end
    for t1 in [
        MOI.VectorQuadraticFunction{T},
        MOI.VectorAffineFunction{T},
        MOI.VectorOfVariables,
        Vector{T},
    ]
        for t2 in [MOI.VectorQuadraticFunction{T}]
            @test MOI.Utilities.promote_operation(+, T, t1, t2) ==
                  MOI.VectorQuadraticFunction{T}
            @test MOI.Utilities.promote_operation(-, T, t1, t2) ==
                  MOI.VectorQuadraticFunction{T}
        end
    end
    for t in [MOI.VectorOfVariables, MOI.VectorAffineFunction{T}]
        @test MOI.Utilities.promote_operation(-, T, t) ==
              MOI.VectorAffineFunction{T}
    end
    t = MOI.VectorQuadraticFunction{T}
    @test MOI.Utilities.promote_operation(-, T, t) ==
          MOI.VectorQuadraticFunction{T}
end

function test_vector_plus_and_neg()
    w = MOI.VariableIndex(0)
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    α = [1, 2, 3]
    v = MOI.VectorOfVariables([y, w, y])
    g = MOI.VectorAffineFunction(
        MOI.VectorAffineTerm.([3, 1], MOI.ScalarAffineTerm.([5, 2], [y, x])),
        [3, 1, 4],
    )
    f = MOI.VectorQuadraticFunction(
        MOI.VectorQuadraticTerm.(
            [1, 1, 2],
            MOI.ScalarQuadraticTerm.([1, 2, 3], [x, y, x], [x, y, y]),
        ),
        MOI.VectorAffineTerm.(
            [1, 2, 2],
            MOI.ScalarAffineTerm.([3, 1, 2], [x, x, y]),
        ),
        [7, 3, 4],
    )
    v_plus_g = MOI.VectorAffineFunction(
        MOI.VectorAffineTerm.(
            [3, 1, 1, 2, 3],
            MOI.ScalarAffineTerm.([5, 2, 1, 1, 1], [y, x, y, w, y]),
        ),
        [3, 1, 4],
    )
    g_plus_α = MOI.VectorAffineFunction(
        MOI.VectorAffineTerm.([3, 1], MOI.ScalarAffineTerm.([5, 2], [y, x])),
        [4, 3, 7],
    )
    α_minus_v = MOI.VectorAffineFunction(
        MOI.VectorAffineTerm.(
            [1, 2, 3],
            MOI.ScalarAffineTerm.([-1, -1, -1], [y, w, y]),
        ),
        [1, 2, 3],
    )
    v_minus_v_plus_v = MOI.VectorAffineFunction(
        MOI.VectorAffineTerm.(
            [1, 2, 3, 1, 2, 3, 1, 2, 3],
            MOI.ScalarAffineTerm.(
                [1, 1, 1, -1, -1, -1, 1, 1, 1],
                [y, w, y, y, w, y, y, w, y],
            ),
        ),
        [0, 0, 0],
    )
    f_plus_α = MOI.VectorQuadraticFunction(
        MOI.VectorQuadraticTerm.(
            [1, 1, 2],
            MOI.ScalarQuadraticTerm.([1, 2, 3], [x, y, x], [x, y, y]),
        ),
        MOI.VectorAffineTerm.(
            [1, 2, 2],
            MOI.ScalarAffineTerm.([3, 1, 2], [x, x, y]),
        ),
        [8, 5, 7],
    )
    f_minus_g = MOI.VectorQuadraticFunction(
        MOI.VectorQuadraticTerm.(
            [1, 1, 2],
            MOI.ScalarQuadraticTerm.([1, 2, 3], [x, y, x], [x, y, y]),
        ),
        MOI.VectorAffineTerm.(
            [1, 2, 2, 3, 1],
            MOI.ScalarAffineTerm.([3, 1, 2, -5, -2], [x, x, y, y, x]),
        ),
        [4, 2, 0],
    )
    @test v + g ≈ v_plus_g
    @test g + α ≈ g_plus_α
    @test α + g ≈ g_plus_α
    @test α - v ≈ α_minus_v
    @test MOI.Utilities.operate(
        +,
        Int,
        MOI.Utilities.operate(-, Int, v, v),
        v,
    ) ≈ v_minus_v_plus_v
    @test f + α ≈ f_plus_α
    @test f - g ≈ f_minus_g
    @test f - f + f - g ≈ f_minus_g
    @test v + f + α - v ≈ f_plus_α
    @test v - f - α - v ≈ -f_plus_α
    @test MOI.Utilities.operate!(-, Int, v, f) - v ≈ -f
    @test (g + v + g + v + f) - (v + g + v + g) ≈ f
    @test v - α ≈ -α_minus_v
    @test g - f ≈ -f_minus_g
    return
end

function test_vector_mult_and_div()
    w = MOI.VariableIndex(0)
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    v = MOI.VectorOfVariables([y, w, y])
    v2 = MOI.Utilities.operate(vcat, Float64, 2.0y, 2.0w, 2.0y)
    f = MOI.Utilities.operate(
        vcat,
        Float64,
        2.0x + 3.0y + 1.0,
        3.0x + 2.0y + 3.0,
    )
    f2 = MOI.Utilities.operate(
        vcat,
        Float64,
        4.0x + 6.0y + 2.0,
        6.0x + 4.0y + 6.0,
    )
    g = MOI.Utilities.operate(
        vcat,
        Float64,
        7.0x * y + 2.0x + 3.0y + 1.0,
        6.0x * x + 5.0y * y + 3.0x + 2.0y + 3.0,
    )
    g2 = MOI.Utilities.operate(
        vcat,
        Float64,
        14.0x * y + 4.0x + 6.0y + 2.0,
        12.0x * x + 10.0y * y + 6.0x + 4.0y + 6.0,
    )
    for (a, a2) in [(v, v2), (f, f2), (g, g2)]
        @test a * 2.0 ≈ a2
        @test 2.0 * a ≈ a2
        @test a / 0.5 ≈ a2
    end
    return
end

function test_modifycoefficient_duplicates()
    model = MOI.Utilities.Model{Float64}()
    x = MOI.add_variable(model)
    f = MOI.ScalarAffineFunction(
        [MOI.ScalarAffineTerm(1.0, x), MOI.ScalarAffineTerm(1.0, x)],
        0.0,
    )
    new_f =
        MOI.Utilities.modify_function(f, MOI.ScalarCoefficientChange(x, 3.0))
    @test new_f ≈ MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(3.0, x)], 0.0)
    return
end

function test_modifycoefficients_duplicates()
    model = MOI.Utilities.Model{Float64}()
    x = MOI.add_variable(model)
    f = MOI.VectorAffineFunction(
        [
            MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, x)),
            MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, x)),
            MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(1.0, x)),
            MOI.VectorAffineTerm(3, MOI.ScalarAffineTerm(1.0, x)),
        ],
        [0.0, 0.0, 0.0],
    )
    new_f = MOI.Utilities.modify_function(
        f,
        MOI.MultirowChange(x, [(1, 3.0), (2, 0.5)]),
    )
    @test new_f ≈ MOI.VectorAffineFunction(
        [
            MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(3.0, x)),
            MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(0.5, x)),
            MOI.VectorAffineTerm(3, MOI.ScalarAffineTerm(1.0, x)),
        ],
        [0.0, 0.0, 0.0],
    )
    return
end

function test_zero_with_output_dimension()
    f = MOI.Utilities.zero_with_output_dimension(Vector{Int}, 2)
    @test f == [0, 0]
    @test f isa Vector{Int}

    g = MOI.Utilities.zero_with_output_dimension(Vector{Float64}, 2)
    @test g == [0.0, 0.0]
    @test g isa Vector{Float64}

    h = MOI.Utilities.zero_with_output_dimension(
        MOI.VectorAffineFunction{Int},
        2,
    )
    @test MOI.output_dimension(h) == 2
    @test isapprox(
        h,
        MOI.VectorAffineFunction{Int}(MOI.VectorAffineTerm{Int}[], [0, 0]),
    )
    @test h isa MOI.VectorAffineFunction{Int}

    i = MOI.Utilities.zero_with_output_dimension(
        MOI.VectorQuadraticFunction{Int},
        2,
    )
    @test MOI.output_dimension(i) == 2
    @test isapprox(
        i,
        MOI.VectorQuadraticFunction{Int}(
            MOI.VectorQuadraticTerm{Int}[],
            MOI.VectorAffineTerm{Int}[],
            [0, 0],
        ),
    )
    @test i isa MOI.VectorQuadraticFunction{Int}
    return
end

"""
    test_SingleVariable_operators()

Test the three Base.:(op) fallbacks for pure SingleVariable operations.
"""
function test_SingleVariable_operators()
    x = MOI.VariableIndex(1)
    @test_throws ErrorException x + x
    @test_throws ErrorException x - x
    @test_throws ErrorException x * x
    return
end

end  # module

TestFunctions.runtests()