# Copyright (c) 2017: Miles Lubin and contributors
# Copyright (c) 2017: Google Inc.
#
# Use of this source code is governed by an MIT-style license that can be found
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.

module TestUniversalFallback

using Test

import MathOptInterface as MOI

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

###
### Utilities for running the tests
###

struct UnknownOptimizerAttribute <: MOI.AbstractOptimizerAttribute end

# A few objective/constraint types are supported to test both the fallback and the
# delegation to the internal model
MOI.Utilities.@model(
    ModelForUniversalFallback,
    (),
    (MOI.LessThan,),
    (),
    (),
    (),
    (MOI.ScalarAffineFunction,),
    (),
    ()
)

function MOI.supports(
    ::ModelForUniversalFallback{T},
    ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}},
) where {T}
    return false
end

function MOI.supports_constraint(
    ::ModelForUniversalFallback{T},
    ::Type{MOI.VariableIndex},
    ::Type{
        <:Union{
            MOI.EqualTo{T},
            MOI.GreaterThan{T},
            MOI.Interval{T},
            MOI.Integer,
            MOI.ZeroOne,
        },
    },
) where {T}
    return false
end

###
### The tests
###

function test_MOI_Test()
    inner = ModelForUniversalFallback{Float64}()
    model = MOI.Utilities.UniversalFallback(inner)
    MOI.Test.runtests(
        model,
        MOI.Test.Config(exclude = Any[MOI.optimize!]),
        exclude = String[
            # UniversalFallback fails all these tests because it supports
            # everything
            "test_attribute_",
            "test_model_supports_constraint_",
            "test_model_copy_to_Unsupported",
            # Bugs in UniversalFallback
            "test_model_LowerBoundAlreadySet",
            "test_model_UpperBoundAlreadySet",
            "test_add_parameter",
        ],
    )
    return
end

function _test_Optimizer_Model_attributes(
    uf::MOI.Utilities.UniversalFallback,
    model::MOI.ModelLike,
    attr::Union{MOI.AbstractOptimizerAttribute,MOI.AbstractModelAttribute},
    list::Union{MOI.ListOfOptimizerAttributesSet,MOI.ListOfModelAttributesSet},
)
    @test !MOI.supports(model, attr)
    @test MOI.supports(uf, attr)
    @test isempty(MOI.get(uf, list))
    MOI.set(uf, attr, 0)
    @test MOI.get(uf, attr) == 0
    @test MOI.get(uf, list) == [attr]
    MOI.set(uf, attr, nothing)
    @test isempty(MOI.get(uf, list))
    MOI.set(uf, attr, 0)
    return
end

function _test_Variable_Constraint_attributes(
    uf::MOI.Utilities.UniversalFallback,
    model::MOI.ModelLike,
    attr::Union{MOI.AbstractVariableAttribute,MOI.AbstractConstraintAttribute},
    listattr::Union{
        MOI.ListOfVariableAttributesSet,
        MOI.ListOfConstraintAttributesSet,
    },
    I::Type{<:MOI.Index},
    addfun,
    x,
    y,
    z,
)
    @test !MOI.supports(model, attr, I)
    @test MOI.supports(uf, attr, I)
    @test isempty(MOI.get(uf, listattr))
    MOI.set(uf, attr, [x, y], [2, 0])
    @test MOI.get(uf, listattr) == [attr]
    MOI.set(uf, attr, x, nothing)
    MOI.set(uf, attr, y, nothing)
    @test isempty(MOI.get(uf, listattr))
    MOI.set(uf, attr, [x, y], [2, 0])
    @test MOI.get(uf, attr, z) === nothing
    @test !MOI.is_empty(uf)
    @test MOI.get(uf, listattr) == [attr]
    MOI.set(uf, attr, z, 5)
    @test MOI.get(uf, attr, y) == 0
    @test MOI.get(uf, attr, [z, x]) == [5, 2]
    @test MOI.get(uf, listattr) == [attr]
    u = addfun(uf)
    @test MOI.get(uf, attr, u) === nothing
    @test MOI.get(uf, listattr) == [attr]
    MOI.set(uf, attr, u, 8)
    @test MOI.get(uf, listattr) == [attr]
    w = addfun(uf)
    @test MOI.get(uf, listattr) == [attr]
    @test MOI.get(uf, attr, w) === nothing
    @test MOI.is_valid(uf, u)
    MOI.delete(uf, u)
    @test !MOI.is_valid(uf, u)
    @test_throws MOI.InvalidIndex{typeof(u)} MOI.delete(uf, u)
    @test MOI.get(uf, listattr) == [attr]
    MOI.set(uf, attr, [w, z], [9, 4])
    @test MOI.get(uf, listattr) == [attr]
    @test MOI.get(uf, attr, w) == 9
    @test MOI.get(uf, attr, x) == 2
    @test MOI.get(uf, attr, z) == 4
    @test MOI.get(uf, attr, y) == 0
    return
end

function test_optimizer_attributes()
    model = ModelForUniversalFallback{Float64}()
    uf = MOI.Utilities.UniversalFallback(model)
    _test_Optimizer_Model_attributes(
        uf,
        model,
        UnknownOptimizerAttribute(),
        MOI.ListOfOptimizerAttributesSet(),
    )
    return
end

function test_model_attributes()
    model = ModelForUniversalFallback{Float64}()
    uf = MOI.Utilities.UniversalFallback(model)
    _test_Optimizer_Model_attributes(
        uf,
        model,
        MOI.Test.UnknownModelAttribute(),
        MOI.ListOfModelAttributesSet(),
    )
    # Test that emptying the uf get's rid of the model attributes
    @test !MOI.is_empty(uf)
    MOI.empty!(uf)
    @test MOI.is_empty(uf)
    return
end

function test_variable_attributes()
    model = ModelForUniversalFallback{Float64}()
    uf = MOI.Utilities.UniversalFallback(model)
    _test_Variable_Constraint_attributes(
        uf,
        model,
        MOI.Test.UnknownVariableAttribute(),
        MOI.ListOfVariableAttributesSet(),
        MOI.VariableIndex,
        MOI.add_variable,
        MOI.add_variables(uf, 3)...,
    )
    return
end

function test_supported_objective_attributes()
    model = ModelForUniversalFallback{Float64}()
    uf = MOI.Utilities.UniversalFallback(model)
    x, y = MOI.add_variables(uf, 2)
    attr = MOI.ObjectiveFunction{MOI.VariableIndex}()
    @test MOI.supports(model, attr)
    MOI.set(uf, attr, x)
    @test MOI.get(uf, attr) ≈ x
    MOI.set(uf, attr, y)
    @test MOI.get(uf, attr) ≈ y
    return
end

function test_unsupported_objective_attributes()
    model = ModelForUniversalFallback{Float64}()
    uf = MOI.Utilities.UniversalFallback(model)
    x, y = MOI.add_variables(uf, 2)
    attr = MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()
    @test !MOI.supports(model, attr)
    @test MOI.supports(uf, attr)
    f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0)
    MOI.set(uf, attr, f)
    @test MOI.get(uf, attr) ≈ f
    MOI.modify(uf, attr, MOI.ScalarCoefficientChange(y, 1.0))
    new_obj = MOI.ScalarAffineFunction(
        [MOI.ScalarAffineTerm(1.0, x), MOI.ScalarAffineTerm(1.0, y)],
        0.0,
    )
    @test MOI.get(uf, attr) ≈ new_obj
    @test attr in MOI.get(uf, MOI.ListOfModelAttributesSet())
    @test MOI.get(uf, MOI.ObjectiveFunctionType()) ==
          MOI.ScalarAffineFunction{Float64}
    return
end

function test_supported_constraint_attributes()
    model = ModelForUniversalFallback{Float64}()
    uf = MOI.Utilities.UniversalFallback(model)
    x, y, z = MOI.add_variables(uf, 3)
    function _add_constraint(uf, x::MOI.VariableIndex, ub::Float64)
        return MOI.add_constraint(
            uf,
            MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0),
            MOI.LessThan(ub),
        )
    end
    cx = _add_constraint(uf, x, 0.0)
    cy = _add_constraint(uf, y, 1.0)
    cz = _add_constraint(uf, z, 2.0)
    F, S = MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}
    _test_Variable_Constraint_attributes(
        uf,
        model,
        MOI.Test.UnknownConstraintAttribute(),
        MOI.ListOfConstraintAttributesSet{F,S}(),
        MOI.ConstraintIndex{F,S},
        uf -> _add_constraint(uf, x, 0.0),
        cx,
        cy,
        cz,
    )
    fy = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, y)], 0.0)
    MOI.set(uf, MOI.ConstraintFunction(), cx, fy)
    @test MOI.get(uf, MOI.ConstraintFunction(), cx) ≈ fy
    @test MOI.get(uf, MOI.CanonicalConstraintFunction(), cx) ≈ fy
    @test MOI.Utilities.is_canonical(
        MOI.get(uf, MOI.CanonicalConstraintFunction(), cx),
    )
    @test MOI.supports(uf, MOI.ConstraintName(), typeof(cx))
    MOI.set(uf, MOI.ConstraintName(), cx, "LessThan")
    @test MOI.get(uf, MOI.ConstraintName(), cx) == "LessThan"
    @test MOI.get(uf, typeof(cx), "LessThan") == cx
    MOI.delete(uf, cx)
    @test MOI.get(uf, typeof(cx), "LessThan") === nothing
    return
end

function test_unsupported_constraint_attributes()
    model = ModelForUniversalFallback{Float64}()
    uf = MOI.Utilities.UniversalFallback(model)
    x, y, z = MOI.add_variables(uf, 3)
    function _add_constraint(uf, x::MOI.VariableIndex, ub::Float64)
        return MOI.add_constraint(
            uf,
            MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0),
            MOI.EqualTo(ub),
        )
    end
    cx = _add_constraint(uf, x, 0.0)
    cy = _add_constraint(uf, y, 1.0)
    cz = _add_constraint(uf, z, 2.0)
    F, S = MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}
    _test_Variable_Constraint_attributes(
        uf,
        model,
        MOI.Test.UnknownConstraintAttribute(),
        MOI.ListOfConstraintAttributesSet{F,S}(),
        MOI.ConstraintIndex{F,S},
        uf -> _add_constraint(uf, x, 0.0),
        cx,
        cy,
        cz,
    )
    fy = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, y)], 0.0)
    MOI.set(uf, MOI.ConstraintFunction(), cx, fy)
    @test MOI.get(uf, MOI.ConstraintFunction(), cx) ≈ fy
    @test MOI.get(uf, MOI.CanonicalConstraintFunction(), cx) ≈ fy
    @test MOI.Utilities.is_canonical(
        MOI.get(uf, MOI.CanonicalConstraintFunction(), cx),
    )
    @test MOI.supports(uf, MOI.ConstraintName(), typeof(cx))
    MOI.set(uf, MOI.ConstraintName(), cx, "EqualTo")
    @test MOI.get(uf, MOI.ConstraintName(), cx) == "EqualTo"
    @test MOI.get(uf, typeof(cx), "EqualTo") == cx
    MOI.delete(uf, cx)
    @test MOI.get(uf, typeof(cx), "EqualTo") === nothing
    return
end

function test_duplicate_names()
    model = ModelForUniversalFallback{Float64}()
    uf = MOI.Utilities.UniversalFallback(model)
    x = MOI.add_variable(uf)
    # These are both intercepted by the fallback.
    f = MOI.ScalarAffineFunction{Float64}([MOI.ScalarAffineTerm(1.0, x)], 0.0)
    x_eq = MOI.add_constraint(uf, f, MOI.EqualTo(1.0))
    x_gt = MOI.add_constraint(uf, f, MOI.GreaterThan(1.0))
    MOI.set(uf, MOI.ConstraintName(), x_eq, "a name")
    MOI.set(uf, MOI.ConstraintName(), x_gt, "a name")
    @test_throws Exception MOI.get(
        uf,
        MOI.ConstraintIndex{
            MOI.ScalarAffineFunction{Float64},
            MOI.EqualTo{Float64},
        },
        "a name",
    )
    @test_throws Exception MOI.get(uf, MOI.ConstraintIndex, "a name")
    return
end

function test_deterministic_constraint_ordering()
    model = ModelForUniversalFallback{Float64}()
    uf = MOI.Utilities.UniversalFallback(model)
    set1 = MOI.EqualTo(1.0)
    set2 = MOI.GreaterThan(1.0)
    F = MOI.ScalarAffineFunction{Float64}
    for sets in [[set1, set2], [set2, set1]]
        model = ModelForUniversalFallback{Float64}()
        uf = MOI.Utilities.UniversalFallback(model)
        x = MOI.add_variable(uf)
        y = MOI.add_variable(uf)
        fx = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0)
        fy = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, y)], 0.0)
        MOI.add_constraint(uf, fx, sets[1])
        MOI.add_constraint(uf, fx, sets[2])
        MOI.add_constraint(uf, fy, sets[1])
        MOI.add_constraint(uf, fy, sets[2])
        # check that the constraint types are in the order they were added in
        @test MOI.get(uf, MOI.ListOfConstraintTypesPresent()) ==
              [(F, typeof(sets[1])), (F, typeof(sets[2]))]
        # check that the constraints given the constraint type are in the order
        # they were added in
        for set in sets
            @test MOI.get(uf, MOI.ListOfConstraintIndices{F,typeof(set)}()) == [
                MOI.ConstraintIndex{F,typeof(set)}(1),
                MOI.ConstraintIndex{F,typeof(set)}(2),
            ]
        end
    end
    return
end

function test_missing_attribute()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    @test MOI.get(model, MOI.Test.UnknownModelAttribute()) === nothing
    @test MOI.get(model, MOI.TimeLimitSec()) === nothing
    @test MOI.get(model, MOI.ObjectiveLimit()) === nothing
    return
end

function test_throw_unsupported_model_attribute()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    attr = MOI.Test.UnknownModelAttribute()
    MOI.set(model, attr, 1)
    err = MOI.UnsupportedAttribute(attr)
    @test_throws err MOI.Utilities.throw_unsupported(model)
    MOI.Utilities.throw_unsupported(
        model,
        excluded_attributes = MOI.AnyAttribute[attr],
    )
    return
end

function test_throw_unsupported_variable_attribute()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variable(model)
    attr = MOI.Test.UnknownVariableAttribute()
    MOI.set(model, attr, x, 1)
    err = MOI.UnsupportedAttribute(attr)
    @test_throws err MOI.Utilities.throw_unsupported(model)
    MOI.Utilities.throw_unsupported(
        model,
        excluded_attributes = MOI.AnyAttribute[attr],
    )
    return
end

function test_throw_unsupported_constraint_attribute()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variable(model)
    c = MOI.add_constraint(model, x, MOI.EqualTo(1.0))
    attr = MOI.Test.UnknownConstraintAttribute()
    MOI.set(model, attr, c, 1)
    err = MOI.UnsupportedAttribute(attr)
    @test_throws err MOI.Utilities.throw_unsupported(model)
    MOI.Utilities.throw_unsupported(
        model,
        excluded_attributes = MOI.AnyAttribute[attr],
    )
    return
end

function test_throw_unsupported_variable_constraint()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variable(model)
    set = MOI.Test.UnknownScalarSet(1.0)
    c = MOI.add_constraint(model, x, set)
    @test_throws(
        MOI.UnsupportedConstraint{typeof(x),typeof(set)}(),
        MOI.Utilities.throw_unsupported(model),
    )
    MOI.delete(model, c)
    @test MOI.Utilities.throw_unsupported(model) === nothing
    return
end

function test_throw_unsupported_affine_constraint()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variable(model)
    func = 2.0x
    set = MOI.Test.UnknownScalarSet(1.0)
    c = MOI.add_constraint(model, func, set)
    @test_throws(
        MOI.UnsupportedConstraint{typeof(func),typeof(set)}(),
        MOI.Utilities.throw_unsupported(model),
    )
    MOI.delete(model, c)
    @test MOI.Utilities.throw_unsupported(model) === nothing
    return
end

function test_ListOfVariablesWithAttributeSet()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variables(model, 2)
    MOI.set(model, MOI.VariableName(), x[1], "x")
    # Passed through to Model with no special support
    attr = MOI.ListOfVariablesWithAttributeSet(MOI.VariableName())
    @test MOI.get(model, attr) == x
    # Handled by UniversalFallback
    attr = MOI.ListOfVariablesWithAttributeSet(MOI.VariablePrimalStart())
    # no attributes set
    @test MOI.get(model, attr) == MOI.VariableIndex[]
    # one attribute set
    MOI.set(model, MOI.VariablePrimalStart(), x[2], 1.0)
    @test MOI.get(model, attr) == [x[2]]
    return
end

function test_ListOfConstraintsWithAttributeSet()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variables(model, 2)
    c = MOI.add_constraint.(model, 1.0 * x, MOI.EqualTo(1.0))
    F, S = MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}
    MOI.set(model, MOI.ConstraintName(), c[1], "c")
    # Passed through to Model with no special support
    attr = MOI.ListOfConstraintsWithAttributeSet{F,S}(MOI.ConstraintName())
    @test MOI.get(model, attr) == c
    # Handled by UniversalFallback
    attr =
        MOI.ListOfConstraintsWithAttributeSet{F,S}(MOI.ConstraintPrimalStart())
    # no attributes set
    @test MOI.get(model, attr) == MOI.ConstraintIndex{F,S}[]
    # one attribute set
    MOI.set(model, MOI.ConstraintPrimalStart(), c[2], 1.0)
    @test MOI.get(model, attr) == [c[2]]
    return
end

function test_delete_ci_attribute()
    model =
        MOI.Utilities.UniversalFallback(ModelForUniversalFallback{Float64}())
    x = MOI.add_variable(model)
    c = MOI.add_constraint(
        model,
        MOI.VectorOfVariables([x]),
        MOI.Nonnegatives(1),
    )
    MOI.set(model, MOI.ConstraintPrimalStart(), c, [1.0])
    @test model.conattr[MOI.ConstraintPrimalStart()][c] == [1.0]
    MOI.delete(model, x)
    @test !haskey(model.conattr[MOI.ConstraintPrimalStart()], c)
    # Vector
    model =
        MOI.Utilities.UniversalFallback(ModelForUniversalFallback{Float64}())
    x = MOI.add_variable(model)
    c = MOI.add_constraint(
        model,
        MOI.VectorOfVariables([x]),
        MOI.Nonnegatives(1),
    )
    MOI.set(model, MOI.ConstraintPrimalStart(), c, [1.0])
    @test model.conattr[MOI.ConstraintPrimalStart()][c] == [1.0]
    MOI.delete(model, [x])
    @test !haskey(model.conattr[MOI.ConstraintPrimalStart()], c)
    return
end

function test_set_inner_constraint_attribute()
    inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    model = MOI.Utilities.UniversalFallback(inner)
    x = MOI.add_variable(model)
    c = MOI.add_constraint(model, x, MOI.GreaterThan(1.0))
    MOI.set(model, MOI.ConstraintPrimalStart(), c, 1.0)
    @test MOI.get(model, MOI.ConstraintPrimalStart(), c) == 1.0
    return
end

end  # module

TestUniversalFallback.runtests()
