text:
"""
    test_solve_ObjectiveBound_MIN_SENSE_IP(model::MOI.ModelLike, config::Config)

Test a variety of edge cases related to the ObjectiveBound attribute.
"""
function test_solve_ObjectiveBound_MIN_SENSE_IP(
    model::MOI.ModelLike,
    config::Config,
)
    @requires _supports(config, MOI.optimize!)
    @requires _supports(config, MOI.ObjectiveBound)
    MOIU.loadfromstring!(
        model,
        """
variables: x
minobjective: 2.0x + -1.0
x >= 1.5
x in Integer()
""",
    )
    x = MOI.get(model, MOI.VariableIndex, "x")
    _test_model_solution(
        model,
        config;
        objective_value = 3.0,
        variable_primal = [(x, 2.0)],
    )
    @test MOI.get(model, MOI.ObjectiveBound()) <= 3.0
    return
end

function setup_test(
    ::typeof(test_solve_ObjectiveBound_MIN_SENSE_IP),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> begin
            MOI.set(mock, MOI.ObjectiveBound(), 3.0)
            MOIU.mock_optimize!(mock, MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [2.0]))
        end,
    )
    return
end

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

Test a variety of edge cases related to the ObjectiveBound attribute.
"""
function test_solve_ObjectiveBound_MAX_SENSE_IP(
    model::MOI.ModelLike,
    config::Config,
)
    @requires _supports(config, MOI.optimize!)
    @requires _supports(config, MOI.ObjectiveBound)
    MOIU.loadfromstring!(
        model,
        """
variables: x
maxobjective: 2.0x + 1.0
x <= 1.5
x in Integer()
""",
    )
    x = MOI.get(model, MOI.VariableIndex, "x")
    _test_model_solution(
        model,
        config;
        objective_value = 3.0,
        variable_primal = [(x, 1.0)],
    )
    @test MOI.get(model, MOI.ObjectiveBound()) >= 3.0
    return
end

function setup_test(
    ::typeof(test_solve_ObjectiveBound_MAX_SENSE_IP),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> begin
            MOI.set(mock, MOI.ObjectiveBound(), 3.0)
            MOIU.mock_optimize!(mock, MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [1.0]))
        end,
    )
    return
end

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

Test a variety of edge cases related to the ObjectiveBound attribute.
"""
function test_solve_ObjectiveBound_MIN_SENSE_LP(
    model::MOI.ModelLike,
    config::Config,
)
    @requires _supports(config, MOI.optimize!)
    @requires _supports(config, MOI.ObjectiveBound)
    MOIU.loadfromstring!(
        model,
        """
variables: x
minobjective: 2.0x + -1.0
x >= 1.5
""",
    )
    x = MOI.get(model, MOI.VariableIndex, "x")
    _test_model_solution(
        model,
        config;
        objective_value = 2.0,
        variable_primal = [(x, 1.5)],
    )
    @test MOI.get(model, MOI.ObjectiveBound()) <= 2.0
end

function setup_test(
    ::typeof(test_solve_ObjectiveBound_MIN_SENSE_LP),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> begin
            MOI.set(mock, MOI.ObjectiveBound(), 2.0)
            MOIU.mock_optimize!(mock, MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [1.5]))
        end,
    )
    return
end

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

Test a variety of edge cases related to the ObjectiveBound attribute.
"""
function test_solve_ObjectiveBound_MAX_SENSE_LP(
    model::MOI.ModelLike,
    config::Config,
)
    @requires _supports(config, MOI.optimize!)
    @requires _supports(config, MOI.ObjectiveBound)
    MOIU.loadfromstring!(
        model,
        """
variables: x
maxobjective: 2.0x + 1.0
x <= 1.5
""",
    )
    x = MOI.get(model, MOI.VariableIndex, "x")
    _test_model_solution(
        model,
        config;
        objective_value = 4.0,
        variable_primal = [(x, 1.5)],
    )
    @test MOI.get(model, MOI.ObjectiveBound()) >= 4.0
    return
end

function setup_test(
    ::typeof(test_solve_ObjectiveBound_MAX_SENSE_LP),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> begin
            MOI.set(mock, MOI.ObjectiveBound(), 4.0)
            MOIU.mock_optimize!(mock, MOI.OPTIMAL, (MOI.FEASIBLE_POINT, [1.5]))
        end,
    )
    return
end

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

Test an unbounded linear program.
"""
function test_solve_TerminationStatus_DUAL_INFEASIBLE(
    model::MOI.ModelLike,
    config::Config,
)
    @requires _supports(config, MOI.optimize!)
    x = MOI.add_variables(model, 5)
    MOI.set(
        model,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, x), 0.0),
    )
    MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE
    return
end

function setup_test(
    ::typeof(test_solve_TerminationStatus_DUAL_INFEASIBLE),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) ->
            MOIU.mock_optimize!(mock, MOI.DUAL_INFEASIBLE),
    )
    return
end

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

Test `ConstraintDual` of a `VariableIndex` constraint when minimizing.
"""
function test_solve_VariableIndex_ConstraintDual_MIN_SENSE(
    model::MOI.ModelLike,
    config::Config,
)
    @requires _supports(config, MOI.optimize!)
    @requires _supports(config, MOI.ConstraintDual)
    x = MOI.add_variable(model)
    xl = MOI.add_constraint(model, x, MOI.GreaterThan(1.0))
    xu = MOI.add_constraint(model, x, MOI.LessThan(1.0))
    MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    MOI.set(model, MOI.ObjectiveFunction{MOI.VariableIndex}(), x)
    MOI.optimize!(model)
    @test isapprox(
        MOI.get(model, MOI.VariablePrimal(), x),
        1.0,
        atol = config.atol,
    )
    sl = MOI.get(model, MOI.ConstraintDual(), xl)
    su = MOI.get(model, MOI.ConstraintDual(), xu)
    @test isapprox(sl + su, 1.0, atol = config.atol)
    @test sl >= -config.atol
    @test su <= config.atol
    return
end

function setup_test(
    ::typeof(test_solve_VariableIndex_ConstraintDual_MIN_SENSE),
    model::MOIU.MockOptimizer,
    ::Config,
)
    flag = model.eval_variable_constraint_dual
    model.eval_variable_constraint_dual = false
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            MOI.OPTIMAL,
            (MOI.FEASIBLE_POINT, [1.0]),
            MOI.FEASIBLE_POINT,
            (MOI.VariableIndex, MOI.GreaterThan{Float64}) => [1.0],
            (MOI.VariableIndex, MOI.LessThan{Float64}) => [0.0],
        ),
    )
    return () -> model.eval_variable_constraint_dual = flag
end

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

Test `ConstraintDual` of a `VariableIndex` constraint when maximizing.
"""
function test_solve_VariableIndex_ConstraintDual_MAX_SENSE(
    model::MOI.ModelLike,
    config::Config,
)
    @requires _supports(config, MOI.optimize!)
    @requires _supports(config, MOI.ConstraintDual)
    x = MOI.add_variable(model)
    xl = MOI.add_constraint(model, x, MOI.GreaterThan(1.0))
    xu = MOI.add_constraint(model, x, MOI.LessThan(1.0))
    MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
    MOI.set(model, MOI.ObjectiveFunction{MOI.VariableIndex}(), x)
    MOI.optimize!(model)
    @test isapprox(
        MOI.get(model, MOI.VariablePrimal(), x),
        1.0,
        atol = config.atol,
    )
    sl = MOI.get(model, MOI.ConstraintDual(), xl)
    su = MOI.get(model, MOI.ConstraintDual(), xu)
    @test isapprox(sl + su, -1.0, atol = config.atol)
    @test sl >= -config.atol
    @test su <= config.atol
    return
end

function setup_test(
    ::typeof(test_solve_VariableIndex_ConstraintDual_MAX_SENSE),
    model::MOIU.MockOptimizer,
    ::Config,
)
    flag = model.eval_variable_constraint_dual
    model.eval_variable_constraint_dual = false
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            MOI.OPTIMAL,
            (MOI.FEASIBLE_POINT, [1.0]),
            MOI.FEASIBLE_POINT,
            (MOI.VariableIndex, MOI.GreaterThan{Float64}) => [0.0],
            (MOI.VariableIndex, MOI.LessThan{Float64}) => [-1.0],
        ),
    )
    return () -> model.eval_variable_constraint_dual = flag
end

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

Test that various attributes implement `.result_index` correctly.
"""
function test_solve_result_index(model::MOI.ModelLike, config::Config)
    @requires _supports(config, MOI.optimize!)
    atol = config.atol
    rtol = config.rtol
    x = MOI.add_variable(model)
    c = MOI.add_constraint(model, x, MOI.GreaterThan(1.0))
    MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    MOI.set(model, MOI.ObjectiveFunction{MOI.VariableIndex}(), x)
    MOI.optimize!(model)
    result_count = MOI.get(model, MOI.ResultCount())
    _test_attribute_value_type(model, MOI.ResultCount())
    function result_err(attr)
        return MOI.ResultIndexBoundsError{typeof(attr)}(attr, result_count)
    end
    result_index = result_count + 1
    @test isapprox(MOI.get(model, MOI.ObjectiveValue(1)), 1.0, config)
    @test_throws result_err(MOI.ObjectiveValue(result_index)) MOI.get(
        model,
        MOI.ObjectiveValue(result_index),
    )
    if _supports(config, MOI.DualObjectiveValue)
        @test isapprox(MOI.get(model, MOI.DualObjectiveValue(1)), 1.0, config)
        @test_throws(
            result_err(MOI.DualObjectiveValue(result_index)),
            MOI.get(model, MOI.DualObjectiveValue(result_index)),
        )
    end
    @test MOI.get(model, MOI.PrimalStatus(1)) == MOI.FEASIBLE_POINT
    @test MOI.get(model, MOI.PrimalStatus(result_index)) == MOI.NO_SOLUTION
    @test MOI.get(model, MOI.VariablePrimal(1), x) ≈ 1.0 atol = atol rtol = rtol
    @test_throws result_err(MOI.VariablePrimal(result_index)) MOI.get(
        model,
        MOI.VariablePrimal(result_index),
        x,
    )
    @test MOI.get(model, MOI.ConstraintPrimal(1), c) ≈ 1.0 atol = atol rtol =
        rtol
    @test_throws result_err(MOI.ConstraintPrimal(result_index)) MOI.get(
        model,
        MOI.ConstraintPrimal(result_index),
        c,
    )
    if _supports(config, MOI.ConstraintDual)
        @test MOI.get(model, MOI.DualStatus(1)) == MOI.FEASIBLE_POINT
        @test MOI.get(model, MOI.DualStatus(result_index)) == MOI.NO_SOLUTION
        @test MOI.get(model, MOI.ConstraintDual(1), c) ≈ 1.0 atol = atol rtol =
            rtol
        @test_throws result_err(MOI.ConstraintDual(result_index)) MOI.get(
            model,
            MOI.ConstraintDual(result_index),
            c,
        )
    end
    return
end

function setup_test(
    ::typeof(test_solve_result_index),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            MOI.OPTIMAL,
            (MOI.FEASIBLE_POINT, [1.0]),
            MOI.FEASIBLE_POINT,
            (MOI.VariableIndex, MOI.GreaterThan{Float64}) => [1.0],
        ),
    )
    return
end

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

Test the Farkas dual of an equality constraint violated above.
"""
function test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_EqualTo_upper(
    model::MOI.ModelLike,
    config::Config,
)
    @requires _supports(config, MOI.optimize!)
    @requires _supports(config, MOI.ConstraintDual)
    x = MOI.add_variables(model, 2)
    clb = MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
    c = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], x), 0.0),
        MOI.EqualTo(-1.0),
    )
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
    if MOI.get(model, MOI.DualStatus()) != MOI.INFEASIBILITY_CERTIFICATE
        return
    end
    clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb)
    c_dual = MOI.get(model, MOI.ConstraintDual(), c)
    @test clb_dual[1] > config.atol
    @test clb_dual[2] > config.atol
    @test c_dual[1] < -config.atol
    @test clb_dual ≈ [2, 1] .* -c_dual atol = config.atol rtol = config.rtol
    return
end

function setup_test(
    ::typeof(test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_EqualTo_upper),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            MOI.INFEASIBLE,
            (MOI.NO_SOLUTION, [NaN, NaN]),
            MOI.INFEASIBILITY_CERTIFICATE,
            (MOI.VariableIndex, MOI.GreaterThan{Float64}) => [2.0, 1.0],
            (MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}) =>
                [-1.0],
        ),
    )
    return
end

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

Test the Farkas dual of an equality constraint violated below.
"""
function test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_EqualTo_lower(
    model::MOI.ModelLike,
    config::Config,
)
    @requires _supports(config, MOI.optimize!)
    @requires _supports(config, MOI.ConstraintDual)
    x = MOI.add_variables(model, 2)
    clb = MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
    c = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([-2.0, -1.0], x), 0.0),
        MOI.EqualTo(1.0),
    )
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
    if MOI.get(model, MOI.DualStatus()) != MOI.INFEASIBILITY_CERTIFICATE
        return
    end
    clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb)
    c_dual = MOI.get(model, MOI.ConstraintDual(), c)
    @test clb_dual[1] > config.atol
    @test clb_dual[2] > config.atol
    @test c_dual[1] > config.atol
    @test clb_dual ≈ [2, 1] .* c_dual atol = config.atol rtol = config.rtol
end

function setup_test(
    ::typeof(test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_EqualTo_lower),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            MOI.INFEASIBLE,
            (MOI.NO_SOLUTION, [NaN, NaN]),
            MOI.INFEASIBILITY_CERTIFICATE,
            (MOI.VariableIndex, MOI.GreaterThan{Float64}) => [2.0, 1.0],
            (MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}) =>
                [1.0],
        ),
    )
    return
end

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

Test the Farkas dual of a less-than constraint.
"""
function test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_LessThan(
    model::MOI.ModelLike,
    config::Config,
)
    @requires _supports(config, MOI.optimize!)
    @requires _supports(config, MOI.ConstraintDual)
    x = MOI.add_variables(model, 2)
    clb = MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
    c = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], x), 0.0),
        MOI.LessThan(-1.0),
    )
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
    if MOI.get(model, MOI.DualStatus()) != MOI.INFEASIBILITY_CERTIFICATE
        return
    end
    clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb)
    c_dual = MOI.get(model, MOI.ConstraintDual(), c)
    @test clb_dual[1] > config.atol
    @test clb_dual[2] > config.atol
    @test c_dual[1] < -config.atol
    @test clb_dual ≈ [2, 1] .* -c_dual atol = config.atol rtol = config.rtol
    return
end

function setup_test(
    ::typeof(test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_LessThan),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            MOI.INFEASIBLE,
            (MOI.NO_SOLUTION, [NaN, NaN]),
            MOI.INFEASIBILITY_CERTIFICATE,
            (MOI.VariableIndex, MOI.GreaterThan{Float64}) => [2.0, 1.0],
            (MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}) =>
                [-1.0],
        ),
    )
    return
end

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

Test the Farkas dual of a greater-than constraint.
"""
function test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_GreaterThan(
    model::MOI.ModelLike,
    config::Config,
)
    @requires _supports(config, MOI.optimize!)
    @requires _supports(config, MOI.ConstraintDual)
    x = MOI.add_variables(model, 2)
    clb = MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
    c = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([-2.0, -1.0], x), 0.0),
        MOI.GreaterThan(1.0),
    )
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
    if MOI.get(model, MOI.DualStatus()) != MOI.INFEASIBILITY_CERTIFICATE
        return
    end
    clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb)
    c_dual = MOI.get(model, MOI.ConstraintDual(), c)
    @test clb_dual[1] > config.atol
    @test clb_dual[2] > config.atol
    @test c_dual[1] > config.atol
    @test clb_dual ≈ [2, 1] .* c_dual atol = config.atol rtol = config.rtol
    return
end

function setup_test(
    ::typeof(test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_GreaterThan),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            MOI.INFEASIBLE,
            (MOI.NO_SOLUTION, [NaN, NaN]),
            MOI.INFEASIBILITY_CERTIFICATE,
            (MOI.VariableIndex, MOI.GreaterThan{Float64}) => [2.0, 1.0],
            (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) =>
                [1.0],
        ),
    )
    return
end

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

Test the Farkas dual of an interval constraint violated above.
"""
function test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_Interval_upper(
    model::MOI.ModelLike,
    config::Config,
)
    @requires _supports(config, MOI.optimize!)
    @requires _supports(config, MOI.ConstraintDual)
    x = MOI.add_variables(model, 2)
    clb = MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
    c = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], x), 0.0),
        MOI.Interval(-2.0, -1.0),
    )
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
    if MOI.get(model, MOI.DualStatus()) != MOI.INFEASIBILITY_CERTIFICATE
        return
    end
    clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb)
    c_dual = MOI.get(model, MOI.ConstraintDual(), c)
    @test clb_dual[1] > config.atol
    @test clb_dual[2] > config.atol
    @test c_dual[1] < -config.atol
    @test clb_dual ≈ [2, 1] .* -c_dual atol = config.atol rtol = config.rtol
    return
end

function setup_test(
    ::typeof(test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_Interval_upper),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            MOI.INFEASIBLE,
            (MOI.NO_SOLUTION, [NaN, NaN]),
            MOI.INFEASIBILITY_CERTIFICATE,
            (MOI.VariableIndex, MOI.GreaterThan{Float64}) => [2.0, 1.0],
            (MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64}) =>
                [-1.0],
        ),
    )
    return
end

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

Test the Farkas dual of an interval constraint violated below.
"""
function test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_Interval_lower(
    model::MOI.ModelLike,
    config::Config,
)
    @requires _supports(config, MOI.optimize!)
    @requires _supports(config, MOI.ConstraintDual)
    x = MOI.add_variables(model, 2)
    clb = MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
    c = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([-2.0, -1.0], x), 0.0),
        MOI.Interval(1.0, 2.0),
    )
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
    if MOI.get(model, MOI.DualStatus()) != MOI.INFEASIBILITY_CERTIFICATE
        return
    end
    clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb)
    c_dual = MOI.get(model, MOI.ConstraintDual(), c)
    @test clb_dual[1] > config.atol
    @test clb_dual[2] > config.atol
    @test c_dual[1] > config.atol
    @test clb_dual ≈ [2, 1] .* c_dual atol = config.atol rtol = config.rtol
    return
end

function setup_test(
    ::typeof(test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_Interval_lower),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            MOI.INFEASIBLE,
            (MOI.NO_SOLUTION, [NaN, NaN]),
            MOI.INFEASIBILITY_CERTIFICATE,
            (MOI.VariableIndex, MOI.GreaterThan{Float64}) => [2.0, 1.0],
            (MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64}) =>
                [1.0],
        ),
    )
    return
end

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

Test the Farkas dual of a variable upper bound violated above when minimizing.
"""
function test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_VariableIndex_LessThan(
    model::MOI.ModelLike,
    config::Config,
)
    @requires _supports(config, MOI.optimize!)
    @requires _supports(config, MOI.ConstraintDual)
    x = MOI.add_variables(model, 2)
    clb = MOI.add_constraint.(model, x, MOI.LessThan(0.0))
    c = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], x), 0.0),
        MOI.GreaterThan(1.0),
    )
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
    if MOI.get(model, MOI.DualStatus()) != MOI.INFEASIBILITY_CERTIFICATE
        return
    end
    clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb)
    c_dual = MOI.get(model, MOI.ConstraintDual(), c)
    @test clb_dual[1] < -config.atol
    @test clb_dual[2] < -config.atol
    @test c_dual[1] > config.atol
    @test clb_dual ≈ [2, 1] .* -c_dual atol = config.atol rtol = config.rtol
    return
end

function setup_test(
    ::typeof(
        test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_VariableIndex_LessThan,
    ),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            MOI.INFEASIBLE,
            (MOI.NO_SOLUTION, [NaN, NaN]),
            MOI.INFEASIBILITY_CERTIFICATE,
            (MOI.VariableIndex, MOI.LessThan{Float64}) => [-2.0, -1.0],
            (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) =>
                [1.0],
        ),
    )
    return
end

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

Test the Farkas dual of a variable upper bound violated above when maximizing.
"""
function test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_VariableIndex_LessThan_max(
    model::MOI.ModelLike,
    config::Config,
)
    @requires _supports(config, MOI.optimize!)
    @requires _supports(config, MOI.ConstraintDual)
    x = MOI.add_variables(model, 2)
    clb = MOI.add_constraint.(model, x, MOI.LessThan(0.0))
    c = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], x), 0.0),
        MOI.GreaterThan(1.0),
    )
    MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
    MOI.set(model, MOI.ObjectiveFunction{MOI.VariableIndex}(), x[1])
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
    if MOI.get(model, MOI.DualStatus()) != MOI.INFEASIBILITY_CERTIFICATE
        return
    end
    clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb)
    c_dual = MOI.get(model, MOI.ConstraintDual(), c)
    @test clb_dual[1] < -config.atol
    @test clb_dual[2] < -config.atol
    @test c_dual[1] > config.atol
    @test clb_dual ≈ [2, 1] .* -c_dual atol = config.atol rtol = config.rtol
    return
end

function setup_test(
    ::typeof(
        test_solve_DualStatus_INFEASIBILITY_CERTIFICATE_VariableIndex_LessThan_max,
    ),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            MOI.INFEASIBLE,
            (MOI.NO_SOLUTION, [NaN, NaN]),
            MOI.INFEASIBILITY_CERTIFICATE,
            (MOI.VariableIndex, MOI.LessThan{Float64}) => [-2.0, -1.0],
            (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) =>
                [1.0],
        ),
    )
    return
end

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

Test that calling `optimize!` twice in succession does not error.
"""
function test_solve_optimize_twice(
    model::MOI.ModelLike,
    config::Config{T},
) where {T}
    @requires _supports(config, MOI.optimize!)
    x = MOI.add_variable(model)
    MOI.add_constraint(model, x, MOI.GreaterThan(one(T)))
    MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    MOI.set(model, MOI.ObjectiveFunction{MOI.VariableIndex}(), x)
    MOI.optimize!(model)
    MOI.optimize!(model)
    MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
    MOI.get(model, MOI.VariablePrimal(), x) == one(T)
    return
end

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

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

Test the ConflictStatus API when two variable bounds are in the conflict.
"""
function test_solve_conflict_bound_bound(
    model::MOI.ModelLike,
    config::Config{T},
) where {T}
    @requires _supports(config, MOI.compute_conflict!)
    @requires _supports(config, MOI.optimize!)
    try
        MOI.get(model, MOI.ConflictStatus())
    catch
        return  # If this fails, skip the test.
    end
    x = MOI.add_variable(model)
    c1 = MOI.add_constraint(model, x, MOI.GreaterThan(T(2)))
    c2 = MOI.add_constraint(model, x, MOI.LessThan(one(T)))
    @test MOI.get(model, MOI.ConflictStatus()) ==
          MOI.COMPUTE_CONFLICT_NOT_CALLED
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
    MOI.compute_conflict!(model)
    @test MOI.get(model, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND
    @test MOI.get(model, MOI.ConstraintConflictStatus(), c1) == MOI.IN_CONFLICT
    @test MOI.get(model, MOI.ConstraintConflictStatus(), c2) == MOI.IN_CONFLICT
    return
end

function setup_test(
    ::typeof(test_solve_conflict_bound_bound),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        mock::MOIU.MockOptimizer -> begin
            MOIU.mock_optimize!(
                mock,
                MOI.INFEASIBLE,
                MOI.NO_SOLUTION,
                MOI.NO_SOLUTION;
                constraint_conflict_status = [
                    (MOI.VariableIndex, MOI.LessThan{Float64}) =>
                        [MOI.IN_CONFLICT],
                    (MOI.VariableIndex, MOI.GreaterThan{Float64}) =>
                        [MOI.IN_CONFLICT],
                ],
            )
            MOI.set(mock, MOI.ConflictStatus(), MOI.CONFLICT_FOUND)
        end,
    )
    return
end

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

Test the ConflictStatus API when two affine constraints are in the conflict.
"""
function test_solve_conflict_two_affine(
    model::MOI.ModelLike,
    config::Config{T},
) where {T}
    @requires _supports(config, MOI.compute_conflict!)
    @requires _supports(config, MOI.optimize!)
    try
        MOI.get(model, MOI.ConflictStatus())
    catch
        return  # If this fails, skip the test.
    end
    x = MOI.add_variable(model)
    c1 = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([one(T)], [x]), zero(T)),
        MOI.GreaterThan(T(2)),
    )
    c2 = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([one(T)], [x]), zero(T)),
        MOI.LessThan(T(1)),
    )
    @test MOI.get(model, MOI.ConflictStatus()) ==
          MOI.COMPUTE_CONFLICT_NOT_CALLED
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
    MOI.compute_conflict!(model)
    @test MOI.get(model, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND
    @test MOI.get(model, MOI.ConstraintConflictStatus(), c1) == MOI.IN_CONFLICT
    @test MOI.get(model, MOI.ConstraintConflictStatus(), c2) == MOI.IN_CONFLICT
    return
end

function setup_test(
    ::typeof(test_solve_conflict_two_affine),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        mock::MOIU.MockOptimizer -> begin
            MOIU.mock_optimize!(
                mock,
                MOI.INFEASIBLE,
                MOI.NO_SOLUTION,
                MOI.NO_SOLUTION;
                constraint_conflict_status = [
                    (
                        MOI.ScalarAffineFunction{Float64},
                        MOI.LessThan{Float64},
                    ) => [MOI.IN_CONFLICT],
                    (
                        MOI.ScalarAffineFunction{Float64},
                        MOI.GreaterThan{Float64},
                    ) => [MOI.IN_CONFLICT],
                ],
            )
            MOI.set(mock, MOI.ConflictStatus(), MOI.CONFLICT_FOUND)
        end,
    )
    return
end

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

Test the ConflictStatus API when an interval bound has upper < lower.
"""
function test_solve_conflict_invalid_interval(
    model::MOI.ModelLike,
    config::Config{T},
) where {T}
    @requires _supports(config, MOI.compute_conflict!)
    @requires _supports(config, MOI.optimize!)
    try
        MOI.get(model, MOI.ConflictStatus())
    catch
        return  # If this fails, skip the test.
    end
    x = MOI.add_variable(model)
    c1 = MOI.add_constraint(model, x, MOI.Interval(one(T), zero(T)))
    @test MOI.get(model, MOI.ConflictStatus()) ==
          MOI.COMPUTE_CONFLICT_NOT_CALLED
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
    MOI.compute_conflict!(model)
    @test MOI.get(model, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND
    @test MOI.get(model, MOI.ConstraintConflictStatus(), c1) == MOI.IN_CONFLICT
    return
end

function setup_test(
    ::typeof(test_solve_conflict_invalid_interval),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        mock::MOIU.MockOptimizer -> begin
            MOIU.mock_optimize!(
                mock,
                MOI.INFEASIBLE,
                MOI.NO_SOLUTION,
                MOI.NO_SOLUTION;
                constraint_conflict_status = [
                    (MOI.VariableIndex, MOI.Interval{Float64}) =>
                        [MOI.IN_CONFLICT],
                ],
            )
            MOI.set(mock, MOI.ConflictStatus(), MOI.CONFLICT_FOUND)
        end,
    )
    return
end

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

Test the ConflictStatus API when two constraints are in the conflict.
"""
function test_solve_conflict_affine_affine(
    model::MOI.ModelLike,
    config::Config{T},
) where {T}
    @requires _supports(config, MOI.compute_conflict!)
    @requires _supports(config, MOI.optimize!)
    try
        MOI.get(model, MOI.ConflictStatus())
    catch
        return  # If this fails, skip the test.
    end
    x = MOI.add_variable(model)
    y = MOI.add_variable(model)
    b1 = MOI.add_constraint(model, x, MOI.GreaterThan(zero(T)))
    b2 = MOI.add_constraint(model, y, MOI.GreaterThan(zero(T)))
    cf1 =
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(one(T), [x, y]), zero(T))
    c1 = MOI.add_constraint(model, cf1, MOI.LessThan(-one(T)))
    cf2 = MOI.ScalarAffineFunction(
        MOI.ScalarAffineTerm.([one(T), -one(T)], [x, y]),
        zero(T),
    )
    c2 = MOI.add_constraint(model, cf2, MOI.GreaterThan(one(T)))
    @test MOI.get(model, MOI.ConflictStatus()) ==
          MOI.COMPUTE_CONFLICT_NOT_CALLED
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
    MOI.compute_conflict!(model)
    @test MOI.get(model, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND
    @test MOI.get(model, MOI.ConstraintConflictStatus(), b1) == MOI.IN_CONFLICT
    @test MOI.get(model, MOI.ConstraintConflictStatus(), b2) == MOI.IN_CONFLICT
    @test MOI.get(model, MOI.ConstraintConflictStatus(), c1) == MOI.IN_CONFLICT
    @test MOI.get(model, MOI.ConstraintConflictStatus(), c2) ==
          MOI.NOT_IN_CONFLICT
    return
end

function setup_test(
    ::typeof(test_solve_conflict_affine_affine),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        mock::MOIU.MockOptimizer -> begin
            MOIU.mock_optimize!(
                mock,
                MOI.INFEASIBLE,
                MOI.NO_SOLUTION,
                MOI.NO_SOLUTION;
                constraint_conflict_status = [
                    (MOI.VariableIndex, MOI.GreaterThan{Float64}) =>
                        [MOI.IN_CONFLICT, MOI.IN_CONFLICT],
                    (
                        MOI.ScalarAffineFunction{Float64},
                        MOI.LessThan{Float64},
                    ) => [MOI.IN_CONFLICT],
                    (
                        MOI.ScalarAffineFunction{Float64},
                        MOI.GreaterThan{Float64},
                    ) => [MOI.NOT_IN_CONFLICT],
                ],
            )
            MOI.set(mock, MOI.ConflictStatus(), MOI.CONFLICT_FOUND)
        end,
    )
    return
end

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

Test the ConflictStatus API when some constraints are EqualTo.
"""
function test_solve_conflict_EqualTo(
    model::MOI.ModelLike,
    config::Config{T},
) where {T}
    @requires _supports(config, MOI.compute_conflict!)
    @requires _supports(config, MOI.optimize!)
    try
        MOI.get(model, MOI.ConflictStatus())
    catch
        return  # If this fails, skip the test.
    end
    x = MOI.add_variable(model)
    y = MOI.add_variable(model)
    b1 = MOI.add_constraint(model, x, MOI.GreaterThan(zero(T)))
    b2 = MOI.add_constraint(model, y, MOI.GreaterThan(zero(T)))
    cf1 =
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(one(T), [x, y]), zero(T))
    c1 = MOI.add_constraint(model, cf1, MOI.EqualTo(-one(T)))
    cf2 = MOI.ScalarAffineFunction(
        MOI.ScalarAffineTerm.([one(T), -one(T)], [x, y]),
        zero(T),
    )
    c2 = MOI.add_constraint(model, cf2, MOI.GreaterThan(one(T)))
    @test MOI.get(model, MOI.ConflictStatus()) ==
          MOI.COMPUTE_CONFLICT_NOT_CALLED
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
    MOI.compute_conflict!(model)
    @test MOI.get(model, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND
    @test MOI.get(model, MOI.ConstraintConflictStatus(), b1) == MOI.IN_CONFLICT
    @test MOI.get(model, MOI.ConstraintConflictStatus(), b2) == MOI.IN_CONFLICT
    @test MOI.get(model, MOI.ConstraintConflictStatus(), c1) == MOI.IN_CONFLICT
    @test MOI.get(model, MOI.ConstraintConflictStatus(), c2) ==
          MOI.NOT_IN_CONFLICT
    return
end

function setup_test(
    ::typeof(test_solve_conflict_EqualTo),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        mock::MOIU.MockOptimizer -> begin
            MOIU.mock_optimize!(
                mock,
                MOI.INFEASIBLE,
                MOI.NO_SOLUTION,
                MOI.NO_SOLUTION;
                constraint_conflict_status = [
                    (MOI.VariableIndex, MOI.GreaterThan{Float64}) =>
                        [MOI.IN_CONFLICT, MOI.IN_CONFLICT],
                    (MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}) => [MOI.IN_CONFLICT],
                    (
                        MOI.ScalarAffineFunction{Float64},
                        MOI.GreaterThan{Float64},
                    ) => [MOI.NOT_IN_CONFLICT],
                ],
            )
            MOI.set(mock, MOI.ConflictStatus(), MOI.CONFLICT_FOUND)
        end,
    )
    return
end

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

Test the ConflictStatus API when some constraints are not in the conlict.
"""
function test_solve_conflict_NOT_IN_CONFLICT(
    model::MOI.ModelLike,
    config::Config{T},
) where {T}
    @requires _supports(config, MOI.compute_conflict!)
    @requires _supports(config, MOI.optimize!)
    try
        MOI.get(model, MOI.ConflictStatus())
    catch
        return  # If this fails, skip the test.
    end
    x = MOI.add_variable(model)
    y = MOI.add_variable(model)
    z = MOI.add_variable(model)
    S = MOI.GreaterThan(zero(T))
    b1 = MOI.add_constraint(model, x, S)
    b2 = MOI.add_constraint(model, y, S)
    b3 = MOI.add_constraint(model, z, S)
    cf1 =
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(one(T), [x, y]), zero(T))
    c1 = MOI.add_constraint(model, cf1, MOI.LessThan(-one(T)))
    cf2 = MOI.ScalarAffineFunction(
        MOI.ScalarAffineTerm.([one(T), -one(T), one(T)], [x, y, z]),
        zero(T),
    )
    c2 = MOI.add_constraint(model, cf2, MOI.GreaterThan(one(T)))
    @test MOI.get(model, MOI.ConflictStatus()) ==
          MOI.COMPUTE_CONFLICT_NOT_CALLED
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
    MOI.compute_conflict!(model)
    @test MOI.get(model, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND
    @test MOI.get(model, MOI.ConstraintConflictStatus(), b1) == MOI.IN_CONFLICT
    @test MOI.get(model, MOI.ConstraintConflictStatus(), b2) == MOI.IN_CONFLICT
    @test MOI.get(model, MOI.ConstraintConflictStatus(), b3) ==
          MOI.NOT_IN_CONFLICT
    @test MOI.get(model, MOI.ConstraintConflictStatus(), c1) == MOI.IN_CONFLICT
    @test MOI.get(model, MOI.ConstraintConflictStatus(), c2) ==
          MOI.NOT_IN_CONFLICT
    return
end

function setup_test(
    ::typeof(test_solve_conflict_NOT_IN_CONFLICT),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        mock::MOIU.MockOptimizer -> begin
            MOIU.mock_optimize!(
                mock,
                MOI.INFEASIBLE,
                MOI.NO_SOLUTION,
                MOI.NO_SOLUTION;
                constraint_conflict_status = [
                    (MOI.VariableIndex, MOI.GreaterThan{Float64}) => [
                        MOI.IN_CONFLICT,
                        MOI.IN_CONFLICT,
                        MOI.NOT_IN_CONFLICT,
                    ],
                    (
                        MOI.ScalarAffineFunction{Float64},
                        MOI.LessThan{Float64},
                    ) => [MOI.IN_CONFLICT],
                    (
                        MOI.ScalarAffineFunction{Float64},
                        MOI.GreaterThan{Float64},
                    ) => [MOI.NOT_IN_CONFLICT],
                ],
            )
            MOI.set(mock, MOI.ConflictStatus(), MOI.CONFLICT_FOUND)
        end,
    )
    return
end

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

Test the ConflictStatus API when the problem is feasible.
"""
function test_solve_conflict_feasible(
    model::MOI.ModelLike,
    config::Config{T},
) where {T}
    @requires _supports(config, MOI.compute_conflict!)
    @requires _supports(config, MOI.optimize!)
    try
        MOI.get(model, MOI.ConflictStatus())
    catch
        return  # If this fails, skip the test.
    end
    x = MOI.add_variable(model)
    f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(one(T), x)], zero(T))
    _ = MOI.add_constraint(model, f, MOI.GreaterThan(one(T)))
    _ = MOI.add_constraint(model, f, MOI.LessThan(T(2)))
    @test MOI.get(model, MOI.ConflictStatus()) ==
          MOI.COMPUTE_CONFLICT_NOT_CALLED
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
    MOI.compute_conflict!(model)
    @test MOI.get(model, MOI.ConflictStatus()) == MOI.NO_CONFLICT_EXISTS
    return
end

function setup_test(
    ::typeof(test_solve_conflict_feasible),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        mock::MOIU.MockOptimizer -> begin
            MOIU.mock_optimize!(
                mock,
                MOI.OPTIMAL,
                MOI.NO_SOLUTION,
                MOI.NO_SOLUTION,
            )
            MOI.set(mock, MOI.ConflictStatus(), MOI.NO_CONFLICT_EXISTS)
        end,
    )
    return
end

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

Test the ConflictStatus API when an integrality is in the conflict.
"""
function test_solve_conflict_zeroone(
    model::MOI.ModelLike,
    config::Config{T},
) where {T}
    @requires _supports(config, MOI.compute_conflict!)
    @requires _supports(config, MOI.optimize!)
    try
        MOI.get(model, MOI.ConflictStatus())
    catch
        return  # If this fails, skip the test.
    end
    x, c1 = MOI.add_constrained_variable(model, MOI.ZeroOne())
    c2 = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(one(T), [x]), zero(T)),
        MOI.GreaterThan(T(2)),
    )
    MOI.optimize!(model)
    @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
    MOI.compute_conflict!(model)
    @test MOI.get(model, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND
    zeroone_conflict = MOI.get(model, MOI.ConstraintConflictStatus(), c1)
    @test zeroone_conflict == MOI.MAYBE_IN_CONFLICT ||
          zeroone_conflict == MOI.IN_CONFLICT
    @test MOI.get(model, MOI.ConstraintConflictStatus(), c2) == MOI.IN_CONFLICT
    return
end

function setup_test(
    ::typeof(test_solve_conflict_zeroone),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        mock::MOIU.MockOptimizer -> begin
            MOIU.mock_optimize!(
                mock,
                MOI.INFEASIBLE,
                MOI.NO_SOLUTION,
                MOI.NO_SOLUTION;
                constraint_conflict_status = [
                    (MOI.VariableIndex, MOI.ZeroOne) =>
                        [MOI.MAYBE_IN_CONFLICT],
                    (
                        MOI.ScalarAffineFunction{Float64},
                        MOI.GreaterThan{Float64},
                    ) => [MOI.IN_CONFLICT],
                ],
            )
            MOI.set(mock, MOI.ConflictStatus(), MOI.CONFLICT_FOUND)
        end,
    )
    return
end

function test_solve_SOS2_add_and_delete(model::MOI.ModelLike, config::Config)
    @requires _supports(config, MOI.optimize!)
    @requires MOI.supports_constraint(
        model,
        MOI.VectorOfVariables,
        MOI.SOS2{Float64},
    )
    function _add_SOS2(model, x, y, xp, yp)
        λ = MOI.add_variables(model, length(xp))
        # 0 <= λ <= 1
        MOI.add_constraint.(model, λ, MOI.LessThan(1.0))
        MOI.add_constraint.(model, λ, MOI.GreaterThan(0.0))
        # Σλᵢ == 1
        MOI.add_constraint(
            model,
            MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, λ), 0.0),
            MOI.EqualTo(1.0),
        )
        # Σλᵢxᵢ - x == 0
        MOI.add_constraint(
            model,
            MOI.ScalarAffineFunction(
                MOI.ScalarAffineTerm.([xp; -1.0], [λ; x]),
                0.0,
            ),
            MOI.EqualTo(0.0),
        )
        # Σλᵢyᵢ - y  == 0
        MOI.add_constraint(
            model,
            MOI.ScalarAffineFunction(
                MOI.ScalarAffineTerm.([yp; -1.0], [λ; y]),
                0.0,
            ),
            MOI.EqualTo(0.0),
        )
        # λ ∈ SOS2()
        c = MOI.add_constraint(
            model,
            MOI.VectorOfVariables(λ),
            MOI.SOS2{Float64}(collect(1.0:length(xp))),
        )
        return λ, c
    end
    x = MOI.add_variables(model, 2)
    y = MOI.add_variables(model, 2)
    # !!! warning
    #     Make sure the variable returned from `_add_SOS2` is named something
    #     other than λ to avoid Julia's `Box`ing issue with closed over
    #     variables.
    vλ, c1 = _add_SOS2(model, x[1], y[1], [1.0, 2.0, 3.0], [2.0, 2.0, 1.0])
    MOI.add_constraint(model, x[1], MOI.LessThan(2.5))
    vη, c2 =
        _add_SOS2(model, x[2], y[2], [1.0, 2.0, 3.0, 4.0], [0.5, 1.0, 0.2, 2.0])
    MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, y), 0.0)
    MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
    attr = MOI.NumberOfConstraints{MOI.VectorOfVariables,MOI.SOS2{Float64}}()
    @test MOI.get(model, attr) == 2
    MOI.optimize!(model)
    #  x[1] == 2.5
    #  y[1] == 1.5
    #  x[2] == 3.0
    #  y[2] == 0.2
    @test isapprox(MOI.get(model, MOI.VariablePrimal(), x[1]), 2.5, config)
    @test isapprox(MOI.get(model, MOI.VariablePrimal(), x[2]), 3.0, config)
    @test isapprox(MOI.get(model, MOI.VariablePrimal(), y[1]), 1.5, config)
    @test isapprox(MOI.get(model, MOI.VariablePrimal(), y[2]), 0.2, config)
    @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 1.7, config)
    MOI.delete(model, c1)
    MOI.delete(model, c2)
    @test MOI.get(model, attr) == 0
    MOI.optimize!(model)
    @test isapprox(MOI.get(model, MOI.VariablePrimal(), x[1]), 2.5, config)
    @test isapprox(MOI.get(model, MOI.VariablePrimal(), x[2]), 3.0, config)
    @test isapprox(MOI.get(model, MOI.VariablePrimal(), y[1]), 1.25, config)
    @test isapprox(MOI.get(model, MOI.VariablePrimal(), y[2]), 0.2, config)
    @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 1.45, config)
    MOI.add_constraint(
        model,
        MOI.VectorOfVariables(vλ),
        MOI.SOS2{Float64}([1.0, 2.0, 3.0]),
    )
    MOI.add_constraint(
        model,
        MOI.VectorOfVariables(vη),
        MOI.SOS2{Float64}([1.0, 2.0, 3.0, 4.0]),
    )
    @test MOI.get(model, attr) == 2
    MOI.optimize!(model)
    @test isapprox(MOI.get(model, MOI.VariablePrimal(), x[1]), 2.5, config)
    @test isapprox(MOI.get(model, MOI.VariablePrimal(), x[2]), 3.0, config)
    @test isapprox(MOI.get(model, MOI.VariablePrimal(), y[1]), 1.5, config)
    @test isapprox(MOI.get(model, MOI.VariablePrimal(), y[2]), 0.2, config)
    @test isapprox(MOI.get(model, MOI.ObjectiveValue()), 1.7, config)
    return
end

function setup_test(
    ::typeof(test_solve_SOS2_add_and_delete),
    model::MOIU.MockOptimizer,
    ::Config,
)
    MOIU.set_mock_optimize!(
        model,
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            MOI.OPTIMAL,
            [2.5, 3.0, 1.5, 0.2, 0.0, 0.5, 0.5, 0.0, 0.0, 1.0, 0.0],
        ),
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            MOI.OPTIMAL,
            [2.5, 3.0, 1.25, 0.2, 0.25, 0.0, 0.75, 0.0, 0.0, 1.0, 0.0],
        ),
        (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
            mock,
            MOI.OPTIMAL,
            [2.5, 3.0, 1.5, 0.2, 0.0, 0.5, 0.5, 0.0, 0.0, 1.0, 0.0],
        ),
    )
    return
end


text:
# Kernel properties
ismercer(::T) where T = false
ismercer(::MercerKernel) = true

isstationary(::T) where T = false
isstationary(::StationaryKernel) = true
isstationary(::IsotropicKernel) = true

isisotropic(::T) where T = false # default to false
isisotropic(::StationaryKernel) = false
isisotropic(::IsotropicKernel) = true

isdot(::T) where T = false # dot product kernel
isdot(::Union{Dot, ExponentialDot}) = true

# TODO: check if this is doing constant propagation
const ProductsAndSums{T, AT} = Union{Sum{T, AT}, Product{T, AT},
                                SeparableProduct{T, AT}, SeparableSum{T, AT}}
ismercer(S::ProductsAndSums) = all(ismercer, S.args)
ismercer(P::Power) = ismercer(P.k)
# TODO: this needs special treatement for constant kernels, which can be all input types
isstationary(S::ProductsAndSums) = all(isstationary, S.args)
isstationary(P::Power) = isstationary(P.k)

isisotropic(S::ProductsAndSums) = all(isisotropic, S.args)
isisotropic(P::Power) = isisotropic(P.k)

isdot(S::ProductsAndSums) = all(isdot, S.args) # need to special case constant kernel
isdot(P::Power) = isdot(P.k)

abstract type InputTrait end
struct GenericInput <: InputTrait end
struct IsotropicInput <: InputTrait end # dependent on |r|²
struct DotProductInput <: InputTrait end # dependent on x ⋅ y
struct StationaryInput <: InputTrait end # dependent on r
struct StationaryLinearFunctionalInput <: InputTrait end # dependent on c ⋅ r
struct PeriodicInput <: InputTrait end

input_trait(::T) where T = GenericInput()
input_trait(::StationaryKernel) = StationaryInput()
input_trait(::IsotropicKernel) = IsotropicInput()
input_trait(::Union{Dot, ExponentialDot}) = DotProductInput()
input_trait(P::Power) = input_trait(P.k)

input_trait(S::Union{Product, Sum}) = S.input_trait
# special treatment for constant kernel, since it can function for any input
function sum_and_product_input_trait(args)
    i = findfirst(x->!isa(x, Constant), args)
    if isnothing(i) # all constant kernels
        return IsotropicInput()
    else
        trait = input_trait(args[i]) # first non-constant kernel
        for j in i+1:length(args)
            k = args[j]
            if k isa Constant # ignore constants, since they can function as any input type
                continue
            elseif input_trait(k) != trait # if the non-constant kernels don't have the same input type,
                return GenericInput() # we default back to GenericInput
            end
        end
        return trait
    end
end

# traits (works on type level only)
# mercer_trait(::T) where T = Val(false)
# stationary_trait(::T) where T = Val(false)
# isotropic_trait(::T) where T = Val(false)
# dot_trait(::T) where T = Val(false)
#
# mercer_trait(::Type{<:MercerKernel}) = Val(true)
# stationary_trait(::Type{<:StationaryKernel}) = Val(true)
# isotropic_trait(::Type{<:IsotropicKernel}) = Val(true)
# dot_trait(::Type{<:Dot}) = Val(true)
#
# mercer_trait(P::Power) = mercer_trait(P.k)
# stationary_trait(P::Power) = stationary_trait(P.k)
# isotropic_trait(P::Power) = isotropic_trait(P.k)
# dot_trait(P::Power) = dot_trait(P.k)

# TODO:
# _tuple_trait(trait, ::Type{Tuple{T, Vararg}}) where T = (trait(T),
# function _product_sum_trait(trait, S::Type{<:ProductsAndSums{T, AT}}) where {T, AT}
#     trait.(AT) isa NTuple ? Val(true) : Val(false)
# end
# mercer_trait(S::Type{<:ProductsAndSums}) = _product_sum_trait(mercer_trait, S)
# stationary_trait(S::Type{<:ProductsAndSums}) = _product_sum_trait(stationary_trait, S)
# isotropic_trait(S::Type{<:ProductsAndSums}) = _product_sum_trait(isotropic_trait, S)
# dot_trait(S::Type{<:ProductsAndSums}) = _product_sum_trait(dot_trait, S)

ext: jl


text:
export iauTdbtcb
"""
Time scale transformation:  Barycentric Dynamical Time, TDB, to
Barycentric Coordinate Time, TCB.

This function is part of the International Astronomical Union's
SOFA (Standards of Fundamental Astronomy) software collection.

Status:  canonical.

Given:
   tdb1,tdb2  double    TDB as a 2-part Julian Date

Returned:
   tcb1,tcb2  double    TCB as a 2-part Julian Date

Returned (function value):
               int       status:  0 = OK

Notes:

   1. tdb1+tdb2 is Julian Date, apportioned in any convenient way
   between the two arguments, for example where tdb1 is the Julian
   Day Number and tdb2 is the fraction of a day.  The returned
   tcb1,tcb2 follow suit.

   2. The 2006 IAU General Assembly introduced a conventional linear
   transformation between TDB and TCB.  This transformation
   compensates for the drift between TCB and terrestrial time TT,
   and keeps TDB approximately centered on TT.  Because the
   relationship between TT and TCB depends on the adopted solar
   system ephemeris, the degree of alignment between TDB and TT over
   long intervals will vary according to which ephemeris is used.
   Former definitions of TDB attempted to avoid this problem by
   stipulating that TDB and TT should differ only by periodic
   effects.  This is a good description of the nature of the
   relationship but eluded precise mathematical formulation.  The
   conventional linear relationship adopted in 2006 sidestepped
   these difficulties whilst delivering a TDB that in practice was
   consistent with values before that date.

   3. TDB is essentially the same as Teph, the time argument for the
   JPL solar system ephemerides.

Reference:

   IAU 2006 Resolution B3

This revision:  2013 June 18

SOFA release 2018-01-30

Copyright (C) 2018 IAU SOFA Board.  See notes at end.
"""

# int iauTdbtcb(double tdb1, double tdb2, double *tcb1, double *tcb2)
function iauTdbtcb(tdb1::Real, tdb2::Real)
   ref_tcb1 = Ref{Float64}(0.0)
   ref_tcb2 = Ref{Float64}(0.0)

   status = ccall((:iauTdbtcb, libsofa_c), Cint,
                   (Cdouble, Cdouble, Ptr{Cdouble}, Ptr{Cdouble}),
                   convert(Float64, tdb1), convert(Float64, tdb2), ref_tcb1, ref_tcb2)

   return status, ref_tcb1[], ref_tcb2[]
end


text:
using GLVisualize, GLAbstraction, Reactive, GeometryTypes, Colors, GLWindow
import GLVisualize: widget, mm, play_slider

if !isdefined(:runtests)
    window = glscreen()
end

description = """
Usage of the play slider widget
"""

iconsize = 8mm
editarea, viewarea = x_partition_abs(window.area, round(Int, 8.2 * iconsize))
edit_screen = Screen(window, area = editarea)
viewscreen = Screen(window, area = viewarea)

function xy_data(x,y,i, N)
    x = ((x/N)-0.5f0)*i
    y = ((y/N)-0.5f0)*i
    r = sqrt(x*x + y*y)
    Float32(sin(r)/r)
end
surf(i, N) = Float32[xy_data(x, y, i, N) for x=1:N, y=1:N]

play_viz, slider_value = play_slider(
    edit_screen, iconsize, linspace(1f0, 50f0, 100)
)
color_viz, color_s = widget(RGBA{Float32}.(colormap("Blues", 7)), edit_screen)

# startstop is not needed here, since the slider value will start and stop
my_animation = map(slider_value) do t
    surf(t, 128)
end

# pair of name => any visualization object
# will be layouted nicely
controls = Pair[
    "color" => color_viz,
    "play" => play_viz,
]

_view(visualize(my_animation, :surface, color_map = color_s), viewscreen)
_view(visualize(
    controls,
    text_scale = 4mm,
    width = 8iconsize
), edit_screen, camera = :fixed_pixel)

if !isdefined(:runtests)
    renderloop(window)
end


text:
abstract type BBO end

for j = string.(BlackBoxOptim.SingleObjectiveMethodNames)
    eval(Meta.parse("Base.@kwdef struct BBO_"*j*" <: BBO method=:"*j*" end"))
    eval(Meta.parse("export BBO_"*j))
end

decompose_trace(opt::BlackBoxOptim.OptRunController) = BlackBoxOptim.best_candidate(opt)

function __map_optimizer_args(prob::OptimizationProblem, opt::BBO;
    cb=nothing,
    maxiters::Union{Number, Nothing}=nothing,
    maxtime::Union{Number, Nothing}=nothing,
    abstol::Union{Number, Nothing}=nothing,
    reltol::Union{Number, Nothing}=nothing,
    kwargs...)

    if !isnothing(reltol)
        @warn "common reltol is currently not used by $(opt)"
    end

    mapped_args = (; Method = opt.method,
    SearchRange = [(prob.lb[i], prob.ub[i]) for i in 1:length(prob.lb)])

    if !isnothing(cb)
        mapped_args = (; mapped_args..., CallbackFunction = cb, CallbackInterval = 0.0)
    end

    mapped_args = (; mapped_args..., kwargs...)

    if !isnothing(maxiters)
        mapped_args = (; mapped_args..., MaxSteps=maxiters)
    end

    if !isnothing(maxtime)
        mapped_args = (; mapped_args..., MaxTime=maxtime)
    end

    if !isnothing(abstol)
        mapped_args = (; mapped_args..., MinDeltaFitnessTolerance=abstol)
  end

    return mapped_args
end

function __solve(prob::OptimizationProblem, opt::BBO, data = DEFAULT_DATA;
                 cb = (args...) -> (false),
                 maxiters::Union{Number, Nothing} = nothing,
                 maxtime::Union{Number, Nothing} = nothing,
                 abstol::Union{Number, Nothing}=nothing,
                 reltol::Union{Number, Nothing}=nothing,
                 progress = false, kwargs...)

    local x, cur, state

    if data != DEFAULT_DATA
        maxiters = length(data)
    end

    cur, state = iterate(data)

    function _cb(trace)
      cb_call = cb(decompose_trace(trace),x...)
      if !(typeof(cb_call) <: Bool)
        error("The callback should return a boolean `halt` for whether to stop the optimization process.")
      end
      if cb_call == true
        BlackBoxOptim.shutdown_optimizer!(trace) #doesn't work
      end
      cur, state = iterate(data, state)
      cb_call
    end

    maxiters = _check_and_convert_maxiters(maxiters)
    maxtime = _check_and_convert_maxtime(maxtime)

    _loss = function(θ)
        x = prob.f(θ, prob.p, cur...)
        return first(x)
    end

    opt_args = _map_optimizer_args(prob, opt, cb=_cb, maxiters=maxiters, maxtime=maxtime,abstol=abstol, reltol=reltol; kwargs...)

    opt_setup = BlackBoxOptim.bbsetup(_loss; opt_args...)

    t0 = time()
    opt_res = BlackBoxOptim.bboptimize(opt_setup)
    t1 = time()

    opt_ret = Symbol(opt_res.stop_reason)

    SciMLBase.build_solution(prob, opt, BlackBoxOptim.best_candidate(opt_res),
                             BlackBoxOptim.best_fitness(opt_res); original=opt_res, retcode=opt_ret)
end


text:
module Torch

import ..Pickle: AbstractPickle, protocol, isbinary

include("./storage.jl")
include("./torch_load.jl")
include("./torch_save.jl")

end


text:
module Stores

using CSV
using Chain
using DataFrames

using Stonks.Models: AbstractStonksRecord, AssetInfo, AssetPrice, ExchangeRate

export AbstractStore, FileStore, load, save, update

"""
Abstract type to be subtyped by all types of Stores, like `FileStore`. `DatabaseStore`.
"""
abstract type AbstractStore{T <: AbstractStonksRecord} end

writer_csv(df::AbstractDataFrame, path::String) = CSV.write(path, df)
reader_csv(path::String) = DataFrame(CSV.File(path))
ensure_path(path::String) = isabspath(path) ? path : joinpath(@__DIR__, path)

"""
    FileStore{T<:AbstractStonksRecord}

Stores all information needed for data storage and retrieval.

### Fields
- `path::String`: absolute or relative path
- `ids::AbstractVector{AbstractString}`: list of identifiers. maximum 2 identifiers
- `format::String`: file format. all files will have the ending like "data.{format}". default = "csv"
- `partitions::AbstractVector{AbstractString}`: columns used for data partitioning. columns have to be members of T
- `time_colum::Union{AbstractString, Missing}`: column representing time dimension. Can be skipped for `AssetPrice` and `ExchangeRate`.
- `reader::Function`: reader(path::String) -> DataFrame
- `writer::Function`: writer(df::AbstractDataFrame, path::String) -> DataFrame

### Constructors
```julia
FileStore{<:AbstractStonksRecord}(;
  path,
  ids,
  format="csv",
  partitions=[],
  time_column=missing,
  reader=reader_csv,
  writer=writer_csv,
)

# where,
reader_csv(path::String) = DataFrame(CSV.File(path))
writer_csv(df::AbstractDataFrame, path::String) = CSV.write(path, df)
```

### Examples
```julia
using Stonks
dest = joinpath(@__DIR__, "data/stonks")
FileStore{AssetInfo}(; path=dest, ids=["symbol"])
FileStore{AssetPrice}(; path=dest, ids=["symbol"], time_column="date")
FileStore{AssetPrice}(; path=dest, ids=[:symbol], partitions=[:symbol], time_column ="date")

using Arrow, DataFrames
read = read_arrow(path::String) = Arrow.Table(path) |> DataFrame
write = write_arrow(df::AbstractDataFrame, path::String) = open(path, "w") do io Arrow.write(io, df) end
FileStore{ExchangeRate}(; path=dest, ids=[:base, :target], time_column="date", reader=read, writer=write)
```
"""
struct FileStore{T<:AbstractStonksRecord,R,W} <: AbstractStore{T}
  path::String
  ids::AbstractVector{AbstractString}
  format::String
  partitions::AbstractVector{AbstractString}
  time_column::Union{AbstractString,Missing}
  reader::R
  writer::W
  function FileStore{T}(
    path,
    ids,
    format="csv",
    partitions=[],
    time_column=missing,
    reader=reader_csv,
    writer=writer_csv,
  ) where {T<:AbstractStonksRecord}
    @chain [ids, partitions, [time_column]] begin
      filter(!isempty, _)
      foreach(x -> if !ismissing(first(x))
        check_type_membership(T, x)
      end, _)
    end
    length(ids) > 2 && throw(DomainError("`ids` must contain at most 2 items."))
    return new{T,typeof(reader),typeof(writer)}(
      ensure_path(path),
      map(String, ids),
      format,
      map(String, partitions),
      ismissing(time_column) ? infer_time_column(T) : time_column,
      reader,
      writer,
    )
  end
end

function infer_time_column(::Type{T})::Union{String,Missing} where {T<:AbstractStonksRecord}
  T_fields = [String(x) for x in fieldnames(T)]
  T === AssetInfo && return missing
  if T === AssetPrice || T === ExchangeRate
    "date" in T_fields && return "date"
    "timestamp" in T_fields && return "timestamp"
  end
  return missing
end

# function infer_ids(::Type{T}) where {T<:AbstractStonksRecord}
#   T_fields = [String(x) for x in fieldnames(T)]
#   if T === AssetPrice || T === AssetInfo
#     "symbol" in T_fields && return ["symbol"]
#   end
#   if T === ExchangeRate
#     "base" in T_fields && return ["base", "target"]
#   end
#   return missing
# end

function check_type_membership(
  ::Type{T}, fields::Vector{String}
) where {T<:AbstractStonksRecord}
  T_fields = [String(x) for x in fieldnames(T)]
  for field in map(String, fields)
    !(field in T_fields) && throw(DomainError("$field not a member of $T"))
  end
end

# kwargs constructor
function FileStore{T}(;
  path,
  ids,
  format="csv",
  partitions=[],
  time_column=missing,
  reader=reader_csv,
  writer=writer_csv,
) where {T<:AbstractStonksRecord}
  return FileStore{T}(
    ensure_path(path),
    map(String, ids),
    format,
    map(String, partitions),
    time_column,
    reader,
    writer,
  )
end

include("stores/utils.jl")
include("stores/atomic_write.jl")
include("stores/interface.jl")

end


text:
struct SMOSolver <: SVDDSolver
    opt_precision::Float64
    max_iterations::Int
end

"""
    takeStep!(α, i1, i2, K, C, opt_precision)

    Take optimization step for i1 and i2 and update α.
"""
function takeStep!(α, i1, i2, K, C, opt_precision)
    i1 == i2 && return false

    L = max(0, α[i1] + α[i2] - C)
    H = min(C, α[i1] + α[i2])
    (abs(L - H) < opt_precision) && return false

    Δ = α[i1] + α[i2]
    c(i) = sum(α[j]*K[i,j] for j in eachindex(α) if !(j in [i1, i2]))

    alpha2 = ((2*Δ*(K[i1, i1] - K[i1,i2]) + c(i1) - c(i2) - K[i1, i1] + K[i2, i2]) /
             (2*K[i1,i1] - 4*K[i1,i2] + 2*K[i2,i2]))

    if alpha2 > H
        alpha2 = H
    elseif alpha2 < L
        alpha2 = L
    end

    # see page 10
    # J. Platt, "Sequential minimal optimization: A fast algorithm for training support vector machines," 1998.
    if abs(α[i2] - alpha2) < opt_precision * (alpha2 + α[i2] + opt_precision)
        return false
    else
        alpha1 = Δ - alpha2
        α[i1] = alpha1
        α[i2] = alpha2
        return true
    end
end

# TODO: this is a faster version of predict from classifier_svdd for in-sample predictions
# this should be the default for in-sample predictions
"""
    calculate_predictions(α, K, C, opt_precision)
"""
function calculate_predictions(α, K, C, opt_precision)
    sv_larger_than_zero = α .> opt_precision
    sv_smaller_than_C = α .< (C - opt_precision)

    sv_larger_than_zero_idx = findall(sv_larger_than_zero)
    const_term = sum(α[i] * α[j] * K[i,j] for i in sv_larger_than_zero_idx for j in sv_larger_than_zero_idx)
    distances_to_center = [K[z, z] - 2 * sum(α[i] * K[i, z] for i in sv_larger_than_zero_idx) + const_term for z in eachindex(α)]

    ## see W.-C. Chang, C.-P. Lee, and C.-J. Lin, "A revisit to support vector data description," 2013
    if any(sv_larger_than_zero .& sv_smaller_than_C)
        R = mean(distances_to_center[sv_larger_than_zero .& sv_smaller_than_C])
    else
        R = (minimum(distances_to_center[sv_larger_than_zero]) + maximum(distances_to_center[sv_larger_than_zero])) / 2
    end

    distances_to_decision_boundary = distances_to_center .- R
    return (distances_to_center, distances_to_decision_boundary, R)
end
"""
    violates_KKT_condition(i2, distances_to_decision_boundary, α, C, opt_precision)

"""
function violates_KKT_condition(i2, distances_to_decision_boundary, α, C, opt_precision)
    p1 = (α[i2] > opt_precision) && (distances_to_decision_boundary[i2] < -opt_precision) # inlier, but alpha > 0
    p2 = (α[i2] < C - opt_precision) && (distances_to_decision_boundary[i2] > opt_precision) # outlier, but alpha != C
    return p1 || p2
end

# See Equation (4.8) in
# B. Schölkopf, J. C. Platt, J. Shawe-Taylor, A. J. Smola, and R. C. Williamson,
# "Estimating the support of a high-dimensional distribution," 2001
"""
    second_choice_heuristic(i2, α, distances_to_center, C, opt_precision)
"""
function second_choice_heuristic(i2, α, distances_to_center, C, opt_precision)
    SV_nb = (α .> opt_precision) .& (α .< C - opt_precision)
    if !any(SV_nb)
        SV_nb = α .> opt_precision
    end
    findall(SV_nb)[findmax(abs.(distances_to_center[i2] .- distances_to_center[SV_nb]))[2]]
end

"""
    examineExample!(α, i2, distances_to_center, K, C, opt_precision)

    The fallback strategies if second choice heuristic returns false follow recommendations in
    J. Platt, "Sequential minimal optimization: A fast algorithm for training support vector machines," 1998.
"""
function examineExample!(α, i2, distances_to_center, K, C, opt_precision)
    # use the second choice heuristic
    i1 = second_choice_heuristic(i2, α, distances_to_center, C, opt_precision)
    takeStep!(α, i1, i2, K, C, opt_precision) && return true

    # loop over all non-zero and non-C alpha, starting at random position
    candidates = findall((α .> opt_precision) .& (α .< C - opt_precision))
    if !isempty(candidates)
        for i1 in shuffle(candidates)
            takeStep!(α, i1, i2, K, C, opt_precision) && return true
        end
    end

    # loop over all
    for i1 in shuffle(eachindex(α))
        takeStep!(α, i1, i2, K, C, opt_precision) && return true
    end
    return false
end

"""
    initialize_alpha(data, C)
"""
function initialize_alpha(data, C)
    n_init = trunc(Int, 1 / (C)) + 1
    α = fill(0.0, size(data, 2))
    α[sample(1:size(data, 2), n_init, replace=false)] .= 1 / n_init
    @assert sum(α) ≈ 1 && all(α .<= C)
    return α
end

"""
    examine_and_update_predictions!(α, distances_to_center, distances_to_decision_boundary, R,
        KKT_violations, black_list, K, C, opt_precision)
"""
function examine_and_update_predictions!(α, distances_to_center, distances_to_decision_boundary, R,
        KKT_violations, black_list, K, C, opt_precision)
    i2 = sample(KKT_violations)
    if examineExample!(α, i2, distances_to_center, K, C, opt_precision)
        distances_to_center, distances_to_decision_boundary, R = calculate_predictions(α, K, C, opt_precision)
    else
        push!(black_list, i2)
    end
    return distances_to_center, distances_to_decision_boundary, R
end

"""
    smo(α, K, C, opt_precision, max_iterations)

"""
function smo(α, K, C, opt_precision, max_iterations)
    distances_to_center, distances_to_decision_boundary, R = calculate_predictions(α, K, C, opt_precision)

    iter = 0
    while iter < max_iterations
        # Fall back strategy: add indices to black list if examine can not make positive step.
        # See page 9, J. Platt, "Sequential minimal optimization: A fast algorithm for training support vector machines," 1998.
        black_list = Set{Int}()
        iter += 1

        # scan over all data
        KKT_violation_all_idx = filter(i -> violates_KKT_condition(i, distances_to_decision_boundary, α, C, opt_precision) && i ∉ black_list, eachindex(α))
        if isempty(KKT_violation_all_idx)
            return build_result(α, distances_to_decision_boundary, R, K, C, opt_precision, JuMP.MathOptInterface.OPTIMAL, "No more KKT_violations.")
        else
            distances_to_center, distances_to_decision_boundary, R = examine_and_update_predictions!(α, distances_to_center, distances_to_decision_boundary, R, KKT_violation_all_idx, black_list, K, C, opt_precision)
        end

        # scan over SV_nb
        SV_nb = (α .> opt_precision) .& (α .< C - opt_precision)
        KKT_violations_in_SV_nb = filter(i -> violates_KKT_condition(i, distances_to_decision_boundary, α, C, opt_precision) && i ∉ black_list, findall(SV_nb))
        while length(KKT_violations_in_SV_nb) > 0 && iter < max_iterations
            iter += 1
            distances_to_center, distances_to_decision_boundary, R = examine_and_update_predictions!(α, distances_to_center, distances_to_decision_boundary, R, KKT_violations_in_SV_nb, black_list, K, C, opt_precision)
            KKT_violations_in_SV_nb = filter(i -> violates_KKT_condition(i, distances_to_decision_boundary, α, C, opt_precision) && i ∉ black_list, findall(SV_nb))
        end
    end
    return build_result(α, distances_to_decision_boundary, R, K, C, opt_precision, :UserLimit, "Reached max number of iterations.")
end

function calculate_duality_gap(α, distances_to_decision_boundary, R, K, C, opt_precision)
    sv_larger_than_zero_idx = findall(α .> opt_precision)
    primal_obj = R + sum(distances_to_decision_boundary[distances_to_decision_boundary .> opt_precision] * C)
    dual_obj = sum(α[i] * K[i,i] for i in sv_larger_than_zero_idx) - sum(α[i] * α[j] * K[i,j] for i in sv_larger_than_zero_idx for j in sv_larger_than_zero_idx)
    duality_gap = primal_obj - dual_obj
    return primal_obj, dual_obj, duality_gap
end

function build_result(α, distances_to_decision_boundary, R, K, C, opt_precision, status, msg)
    if status == JuMP.MathOptInterface.OPTIMAL
        info(LOGGER, "Exit with status: $status. ($msg)")
    else
        warn(LOGGER, "Exit with status: $status. ($msg)")
    end
    primal_obj, dual_obj, duality_gap = calculate_duality_gap(α, distances_to_decision_boundary, R, K, C, opt_precision)
    info(LOGGER, "duality gap: $duality_gap, primal objective: $primal_obj, dual objective: $dual_obj")
   return α, primal_obj, duality_gap, duality_gap, status
end

"""
    solve!(model::VanillaSVDD, solver::SMOSolver)
"""
function solve!(model::VanillaSVDD, solver::SMOSolver)
    α = initialize_alpha(model.data, model.C)
    model.alpha_values, primal_obj, dual_obj, duality_gap, status = smo(α, model.K, model.C, solver.opt_precision, solver.max_iterations)
    return status
end