# 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 TestNLModel

using Test

import MathOptInterface as MOI
import MathOptInterface.FileFormats: NL

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

function _test_nlexpr(
    expr::NL._NLExpr,
    nonlinear_terms,
    linear_terms,
    constant;
    force_nonlinear::Bool = false,
)
    if force_nonlinear
        @test expr.is_linear == false
    else
        @test expr.is_linear == (length(nonlinear_terms) == 0)
    end
    @test expr.nonlinear_terms == nonlinear_terms
    @test expr.linear_terms == linear_terms
    @test expr.constant == constant
    return
end

function _test_nlexpr(x, args...; kwargs...)
    return _test_nlexpr(NL._NLExpr(x), args...; kwargs...)
end

function test_nlexpr_singlevariable()
    x = MOI.VariableIndex(1)
    _test_nlexpr(x, NL._NLTerm[], Dict(x => 1.0), 0.0)
    return
end

function test_nlexpr_scalaraffine()
    x = MOI.VariableIndex.(1:3)
    f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, x), 4.0)
    return _test_nlexpr(f, NL._NLTerm[], Dict(x .=> 1), 4.0)
end

function test_nlexpr_scalarquadratic_0()
    x = MOI.VariableIndex(1)
    f = MOI.ScalarQuadraticFunction(
        MOI.ScalarQuadraticTerm{Float64}[],
        [MOI.ScalarAffineTerm(1.1, x)],
        3.0,
    )
    return _test_nlexpr(
        f,
        NL._NLTerm[],
        Dict(x => 1.1),
        3.0;
        force_nonlinear = true,
    )
end

function test_nlexpr_scalarquadratic_1a()
    x = MOI.VariableIndex(1)
    f = MOI.ScalarQuadraticFunction(
        [MOI.ScalarQuadraticTerm(2.0, x, x)],
        [MOI.ScalarAffineTerm(1.1, x)],
        3.0,
    )
    terms = [NL.OPMULT, x, x]
    return _test_nlexpr(f, terms, Dict(x => 1.1), 3.0)
end

function test_nlexpr_scalarquadratic_1b()
    x = MOI.VariableIndex(1)
    f = MOI.ScalarQuadraticFunction(
        [MOI.ScalarQuadraticTerm(2.5, x, x)],
        [MOI.ScalarAffineTerm(1.1, x)],
        3.0,
    )
    terms = [NL.OPMULT, 1.25, NL.OPMULT, x, x]
    return _test_nlexpr(f, terms, Dict(x => 1.1), 3.0)
end

function test_nlexpr_scalarquadratic_1c()
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    f = MOI.ScalarQuadraticFunction(
        [MOI.ScalarQuadraticTerm(1.0, x, y)],
        [MOI.ScalarAffineTerm(1.1, x)],
        3.0,
    )
    terms = [NL.OPMULT, x, y]
    return _test_nlexpr(f, terms, Dict(x => 1.1, y => 0.0), 3.0)
end

function test_nlexpr_scalarquadratic_2()
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    f = MOI.ScalarQuadraticFunction(
        [
            MOI.ScalarQuadraticTerm(2.0, x, x),
            MOI.ScalarQuadraticTerm(1.0, x, y),
        ],
        [MOI.ScalarAffineTerm(1.1, x)],
        3.0,
    )
    terms = [NL.OPPLUS, NL.OPMULT, x, x, NL.OPMULT, x, y]
    return _test_nlexpr(f, terms, Dict(x => 1.1, y => 0.0), 3.0)
end

function test_nlexpr_scalarquadratic_3()
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    z = MOI.VariableIndex(3)
    f = MOI.ScalarQuadraticFunction(
        [
            MOI.ScalarQuadraticTerm(2.0, x, x),
            MOI.ScalarQuadraticTerm(0.5, x, y),
            MOI.ScalarQuadraticTerm(4.0, x, z),
        ],
        [MOI.ScalarAffineTerm(1.1, x)],
        3.0,
    )
    terms = [
        NL.OPSUMLIST,
        3,
        NL.OPMULT,
        x,
        x,
        NL.OPMULT,
        0.5,
        NL.OPMULT,
        x,
        y,
        NL.OPMULT,
        4.0,
        NL.OPMULT,
        x,
        z,
    ]
    return _test_nlexpr(f, terms, Dict(x => 1.1, y => 0.0, z => 0.0), 3.0)
end

function test_nlexpr_scalarnonlinearfunction_unary_special_case()
    x = MOI.VariableIndex(1)
    f = MOI.ScalarNonlinearFunction(:cbrt, Any[x])
    expr = NL._NLExpr(:(cbrt($x)))
    _test_nlexpr(f, expr.nonlinear_terms, Dict(x => 0), 0.0)
    return
end

function test_nlexpr_scalarnonlinearfunction_unary_special_case_sign()
    x = MOI.VariableIndex(1)
    f = MOI.ScalarNonlinearFunction(:sign, Any[x])
    expr = NL._NLExpr(:(ifelse($x >= 0, 1, -1)))
    _test_nlexpr(f, expr.nonlinear_terms, Dict(x => 0.0), 0.0)
    return
end

function test_nlexpr_scalarnonlinearfunction_binary_special_case()
    x = MOI.VariableIndex(1)
    f = MOI.ScalarNonlinearFunction(:\, Any[x, 1])
    expr = NL._NLExpr(:(\($x, 1)))
    _test_nlexpr(f, expr.nonlinear_terms, Dict(x => 0), 0.0)
    return
end

function test_nlexpr_scalarnonlinearfunction_ternary_multiplication()
    x = MOI.VariableIndex(1)
    f = MOI.ScalarNonlinearFunction(:*, Any[x, x, x])
    expr = NL._NLExpr(:(*($x, $x, $x)))
    _test_nlexpr(f, expr.nonlinear_terms, Dict(x => 0), 0.0)
    return
end

function test_nlexpr_scalarnonlinearfunction_inner_functions()
    x = MOI.VariableIndex(1)
    f = 1.0 * x + 1.0
    g = 2.0 * x * x + 3.0 * x + 4.0
    f = MOI.ScalarNonlinearFunction(:*, Any[2.0, x, f, g])
    ex = Expr(
        :call,
        :*,
        2.0,
        x,
        :(1.0 * $x + 1.0),
        :(2.0 * $x * $x + 3.0 * $x + 4.0),
    )
    expr = NL._NLExpr(ex)
    _test_nlexpr(f, expr.nonlinear_terms, Dict(x => 0), 0.0)
    return
end

function test_nlexpr_unary_addition()
    x = MOI.VariableIndex(1)
    return _test_nlexpr(:(+$x), [x], Dict(x => 0), 0.0)
end

function test_nlexpr_binary_addition()
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    return _test_nlexpr(
        :($x + $y),
        [NL.OPPLUS, x, y],
        Dict(x => 0.0, y => 0.0),
        0.0,
    )
end

function test_nlexpr_nary_addition()
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    return _test_nlexpr(
        :($x + $y + 1.0),
        [NL.OPSUMLIST, 3, x, y, 1.0],
        Dict(x => 0.0, y => 0.0),
        0.0,
    )
end

function test_nlexpr_unary_subtraction()
    x = MOI.VariableIndex(1)
    return _test_nlexpr(:(-$x), [NL.OPUMINUS, x], Dict(x => 0.0), 0.0)
end

function test_nlexpr_nary_multiplication()
    x = MOI.VariableIndex(1)
    return _test_nlexpr(
        :($x * $x * 2.0),
        [NL.OPMULT, x, NL.OPMULT, x, 2.0],
        Dict(x => 0.0),
        0.0,
    )
end

function test_nlexpr_unary_multiplication()
    x = MOI.VariableIndex(1)
    return _test_nlexpr(:(*($x)), [x], Dict(x => 0), 0.0)
end

function test_nlexpr_unary_specialcase()
    x = MOI.VariableIndex(1)
    return _test_nlexpr(
        :(cbrt($x)),
        [NL.OPPOW, x, NL.OPDIV, 1, 3],
        Dict(x => 0.0),
        0.0,
    )
end

function test_nlexpr_binary_specialcase()
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    return _test_nlexpr(
        :(\($x, $y)),
        [NL.OPDIV, y, x],
        Dict(x => 0.0, y => 0.0),
        0.0,
    )
end

function test_nlexpr_atan_and_atan2()
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    _test_nlexpr(:(atan($x)), [NL.OP_atan, x], Dict(x => 0.0), 0.0)
    _test_nlexpr(
        :(atan($x, $y)),
        [NL.OP_atan2, x, y],
        Dict(x => 0.0, y => 0.0),
        0.0,
    )
    return
end

function test_nlexpr_unsupportedoperation()
    x = MOI.VariableIndex(1)
    @test_throws MOI.UnsupportedNonlinearOperator(:foo) NL._NLExpr(:(foo($x)))
    return
end

function test_nlexpr_unsupportedexpression()
    x = MOI.VariableIndex(1)
    expr = :(1 <= $x <= 2)
    err = ErrorException("Unsupported expression: $(expr)")
    @test_throws err NL._NLExpr(expr)
    return
end

function test_nlexpr_ref()
    x = MOI.VariableIndex(1)
    return _test_nlexpr(:(x[$x]), [x], Dict(x => 0.0), 0.0)
end

function test_nlexpr_empty()
    return _test_nlexpr(
        :(),
        NL._NLTerm[],
        Dict{MOI.VariableIndex,Float64}(),
        0.0;
        force_nonlinear = true,
    )
end

function test_nlconstraint_interval()
    x = MOI.VariableIndex(1)
    expr = :(1.0 <= $x <= 2.0)
    con = NL._NLConstraint(expr, MOI.NLPBoundsPair(1.0, 2.0))
    @test con.lower == 1
    @test con.upper == 2
    @test con.opcode == 0
    @test con.expr == NL._NLExpr(expr.args[3])
end

function test_nlconstraint_lessthan()
    x = MOI.VariableIndex(1)
    expr = :($x <= 2.0)
    con = NL._NLConstraint(expr, MOI.NLPBoundsPair(-Inf, 2.0))
    @test con.lower == -Inf
    @test con.upper == 2
    @test con.opcode == 1
    @test con.expr == NL._NLExpr(expr.args[2])
end

function test_nlconstraint_greaterthan()
    x = MOI.VariableIndex(1)
    expr = :($x >= 2.0)
    con = NL._NLConstraint(expr, MOI.NLPBoundsPair(2.0, Inf))
    @test con.lower == 2
    @test con.upper == Inf
    @test con.opcode == 2
    @test con.expr == NL._NLExpr(expr.args[2])
end

function test_nlconstraint_equalto()
    x = MOI.VariableIndex(1)
    expr = :($x == 2.0)
    con = NL._NLConstraint(expr, MOI.NLPBoundsPair(2.0, 2.0))
    @test con.lower == 2
    @test con.upper == 2
    @test con.opcode == 4
    @test con.expr == NL._NLExpr(expr.args[2])
end

function test_nlconstraint_interval_warn()
    x = MOI.VariableIndex(1)
    expr = :(2.0 <= $x <= 1.0)
    @test_logs (:warn,) NL._NLConstraint(expr, MOI.NLPBoundsPair(1.0, 2.0))
end

function test_nlconstraint_lessthan_warn()
    x = MOI.VariableIndex(1)
    expr = :($x <= 1.0)
    @test_logs (:warn,) NL._NLConstraint(expr, MOI.NLPBoundsPair(-Inf, 2.0))
end

function test_nlconstraint_greaterthan_warn()
    x = MOI.VariableIndex(1)
    expr = :($x >= 0.0)
    @test_logs (:warn,) NL._NLConstraint(expr, MOI.NLPBoundsPair(1.0, Inf))
end

function test_nlconstraint_equalto_warn()
    x = MOI.VariableIndex(1)
    expr = :($x == 2.0)
    @test_logs (:warn,) NL._NLConstraint(expr, MOI.NLPBoundsPair(1.0, 1.0))
end

function test_nlmodel_hs071()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    v = MOI.add_variables(model, 4)
    l = [1.1, 1.2, 1.3, 1.4]
    u = [5.1, 5.2, 5.3, 5.4]
    start = [2.1, 2.2, 2.3, 2.4]
    MOI.add_constraint.(model, v, MOI.GreaterThan.(l))
    MOI.add_constraint.(model, v, MOI.LessThan.(u))
    MOI.set.(model, MOI.VariablePrimalStart(), v, start)
    lb, ub = [25.0, 40.0], [Inf, 40.0]
    evaluator = MOI.Test.HS071(true)
    block_data = MOI.NLPBlockData(MOI.NLPBoundsPair.(lb, ub), evaluator, true)
    MOI.set(model, MOI.NLPBlock(), block_data)
    MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    n = NL.Model()
    @test MOI.supports(n, MOI.NLPBlock())
    @test MOI.supports(n, MOI.ObjectiveSense())
    @test MOI.is_empty(n)
    MOI.copy_to(n, model)
    @test !MOI.is_empty(n)
    @test n.sense == MOI.MIN_SENSE
    @test n.f == NL._NLExpr(MOI.objective_expr(evaluator))
    _test_nlexpr(
        n.g[1].expr,
        [NL.OPMULT, v[1], NL.OPMULT, v[2], NL.OPMULT, v[3], v[4]],
        Dict(v .=> 0.0),
        0.0,
    )
    @test n.g[1].lower == 25.0
    @test n.g[1].upper == Inf
    @test n.g[1].opcode == 2
    _test_nlexpr(
        n.g[2].expr,
        [
            NL.OPSUMLIST,
            4,
            NL.OPPOW,
            v[1],
            2,
            NL.OPPOW,
            v[2],
            2,
            NL.OPPOW,
            v[3],
            2,
            NL.OPPOW,
            v[4],
            2,
        ],
        Dict(v .=> 0.0),
        0.0,
    )
    @test n.g[2].lower == 40.0
    @test n.g[2].upper == 40.0
    @test n.g[2].opcode == 4
    @test length(n.h) == 0
    for i in 1:4
        @test n.x[v[i]].lower == l[i]
        @test n.x[v[i]].upper == u[i]
        @test n.x[v[i]].type == NL._CONTINUOUS
        @test n.x[v[i]].jacobian_count == 2
        @test n.x[v[i]].in_nonlinear_constraint
        @test n.x[v[i]].in_nonlinear_objective
        @test 0 <= n.x[v[i]].order <= 3
    end
    @test length(n.types[1]) == 4
    @test sprint(write, n) == """
    g3 1 1 0
     4 2 1 0 1 0
     2 1
     0 0
     4 4 4
     0 0 0 1
     0 0 0 0 0
     8 4
     0 0
     0 0 0 0 0
    C0
    o2
    v0
    o2
    v1
    o2
    v2
    v3
    C1
    o54
    4
    o5
    v0
    n2
    o5
    v1
    n2
    o5
    v2
    n2
    o5
    v3
    n2
    O0 0
    o0
    o2
    v0
    o2
    v3
    o54
    3
    v0
    v1
    v2
    v2
    x4
    0 2.1
    1 2.2
    2 2.3
    3 2.4
    r
    2 25
    4 40
    b
    0 1.1 5.1
    0 1.2 5.2
    0 1.3 5.3
    0 1.4 5.4
    k3
    2
    4
    6
    J0 4
    0 0
    1 0
    2 0
    3 0
    J1 4
    0 0
    1 0
    2 0
    3 0
    G0 4
    0 0
    1 0
    2 0
    3 0
    """
    return
end

function test_nlmodel_hs071_linear_obj()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    v = MOI.add_variables(model, 4)
    l = [1.1, 1.2, 1.3, 1.4]
    u = [5.1, 5.2, 5.3, 5.4]
    start = [2.1, 2.2, 2.3, 2.4]
    MOI.add_constraint.(model, v, MOI.GreaterThan.(l))
    MOI.add_constraint.(model, v, MOI.LessThan.(u))
    MOI.add_constraint(model, v[2], MOI.ZeroOne())
    MOI.add_constraint(model, v[3], MOI.Integer())
    MOI.set.(model, MOI.VariablePrimalStart(), v, start)
    lb, ub = [25.0, 40.0], [Inf, 40.0]
    evaluator = MOI.Test.HS071(true)
    block_data = MOI.NLPBlockData(MOI.NLPBoundsPair.(lb, ub), evaluator, false)
    MOI.set(model, MOI.NLPBlock(), block_data)
    f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(l, v), 2.0)
    MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
    MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
    n = NL.Model()
    @test MOI.supports(n, MOI.VariablePrimalStart(), MOI.VariableIndex)
    @test MOI.supports(n, MOI.ObjectiveFunction{typeof(f)}())
    index_map = MOI.copy_to(n, model)
    @test n.sense == MOI.MAX_SENSE
    @test n.f == NL._NLExpr(f)
    _test_nlexpr(
        n.g[1].expr,
        [NL.OPMULT, v[1], NL.OPMULT, v[2], NL.OPMULT, v[3], v[4]],
        Dict(v .=> 0.0),
        0.0,
    )
    @test n.g[1].lower == 25.0
    @test n.g[1].upper == Inf
    @test n.g[1].opcode == 2
    _test_nlexpr(
        n.g[2].expr,
        [
            NL.OPSUMLIST,
            4,
            NL.OPPOW,
            v[1],
            2,
            NL.OPPOW,
            v[2],
            2,
            NL.OPPOW,
            v[3],
            2,
            NL.OPPOW,
            v[4],
            2,
        ],
        Dict(v .=> 0.0),
        0.0,
    )
    @test n.g[2].lower == 40.0
    @test n.g[2].upper == 40.0
    @test n.g[2].opcode == 4
    @test length(n.h) == 0
    types = [NL._CONTINUOUS, NL._BINARY, NL._INTEGER, NL._CONTINUOUS]
    u[2] = 1.0
    for i in 1:4
        @test n.x[v[i]].start == start[i]
        @test n.x[v[i]].lower == l[i]
        @test n.x[v[i]].upper == u[i]
        @test n.x[v[i]].type == types[i]
        @test n.x[v[i]].jacobian_count == 2
        @test n.x[v[i]].in_nonlinear_constraint
        @test !n.x[v[i]].in_nonlinear_objective
        @test 0 <= n.x[v[i]].order <= 3
    end
    @test length(n.types[3]) == 2
    @test length(n.types[4]) == 2
    @test v[1] in n.types[3]
    @test v[2] in n.types[4]
    @test v[3] in n.types[4]
    @test v[4] in n.types[3]
    @test sprint(write, n) == """
    g3 1 1 0
     4 2 1 0 1 0
     2 1
     0 0
     4 0 0
     0 0 0 1
     0 0 0 2 0
     8 4
     0 0
     0 0 0 0 0
    C0
    o2
    v0
    o2
    v2
    o2
    v3
    v1
    C1
    o54
    4
    o5
    v0
    n2
    o5
    v2
    n2
    o5
    v3
    n2
    o5
    v1
    n2
    O0 1
    n2
    x4
    0 2.1
    1 2.4
    2 2.2
    3 2.3
    r
    2 25
    4 40
    b
    0 1.1 5.1
    0 1.4 5.4
    0 1.2 1
    0 1.3 5.3
    k3
    2
    4
    6
    J0 4
    0 0
    1 0
    2 0
    3 0
    J1 4
    0 0
    1 0
    2 0
    3 0
    G0 4
    0 1.1
    1 1.4
    2 1.2
    3 1.3
    """
    return
end

function test_nlmodel_linear_quadratic()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variables(model, 4)
    MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
    MOI.add_constraint.(model, x, MOI.LessThan(2.0))
    MOI.add_constraint(model, x[2], MOI.ZeroOne())
    MOI.add_constraint(model, x[3], MOI.Integer())
    f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, x[2:4]), 2.0)
    g = MOI.ScalarQuadraticFunction(
        [MOI.ScalarQuadraticTerm(2.0, x[1], x[2])],
        [MOI.ScalarAffineTerm(1.0, x[1])],
        3.0,
    )
    h = MOI.ScalarQuadraticFunction(
        [MOI.ScalarQuadraticTerm(1.0, x[1], x[2])],
        [MOI.ScalarAffineTerm(1.0, x[3])],
        0.0,
    )
    MOI.add_constraint(model, f, MOI.Interval(1.0, 10.0))
    MOI.add_constraint(model, g, MOI.LessThan(5.0))
    MOI.set(model, MOI.ObjectiveFunction{typeof(h)}(), h)
    MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
    n = NL.Model()
    MOI.copy_to(n, model)
    @test n.sense == MOI.MAX_SENSE
    @test n.f == NL._NLExpr(h)
    terms = [NL.OPMULT, 2.0, NL.OPMULT, x[1], x[2]]
    _test_nlexpr(n.g[1].expr, terms, Dict(x[1] => 1.0, x[2] => 0.0), 3.0)
    @test n.g[1].opcode == 1
    @test n.g[1].lower == -Inf
    @test n.g[1].upper == 5.0
    @test n.h[1].expr == NL._NLExpr(f)
    @test n.h[1].opcode == 0
    @test n.h[1].lower == 1.0
    @test n.h[1].upper == 10.0
    @test n.types[1] == [x[1]]  # Continuous in both
    @test n.types[2] == [x[2]]  # Discrete in both
    @test n.types[6] == [x[3]]  # Discrete in objective only
    @test n.types[7] == [x[4]]  # Continuous in linear
    @test sprint(write, n) == """
    g3 1 1 0
     4 2 1 1 0 0
     1 1
     0 0
     2 3 2
     0 0 0 1
     0 0 1 0 1
     5 3
     0 0
     0 0 0 0 0
    C0
    o0
    n3
    o2
    n2
    o2
    v0
    v1
    C1
    n2
    O0 1
    o2
    v0
    v1
    x4
    0 0
    1 0
    2 0
    3 0
    r
    1 5
    0 1 10
    b
    0 0 2
    0 0 1
    0 0 2
    0 0 2
    k3
    1
    3
    4
    J0 2
    0 1
    1 0
    J1 3
    1 1
    2 1
    3 1
    G0 3
    0 0
    1 0
    2 1
    """
    return
end

function test_nlmodel_quadratic_interval()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variable(model)
    g = MOI.ScalarQuadraticFunction(
        [MOI.ScalarQuadraticTerm(2.0, x, x)],
        [MOI.ScalarAffineTerm(1.0, x)],
        3.0,
    )
    MOI.add_constraint(model, g, MOI.Interval(1.0, 10.0))
    n = NL.Model()
    MOI.copy_to(n, model)
    @test sprint(write, n) == """
    g3 1 1 0
     1 1 1 1 0 0
     1 1
     0 0
     1 0 0
     0 0 0 1
     0 0 0 0 0
     1 0
     0 0
     0 0 0 0 0
    C0
    o0
    n3
    o2
    v0
    v0
    O0 0
    n0
    x1
    0 0
    r
    0 1 10
    b
    3
    k0
    J0 1
    0 1
    """
    return
end

function test_nlmodel_scalar_nonlinear_function_model()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variable(model)
    g = MOI.ScalarNonlinearFunction(
        :+,
        Any[
            MOI.ScalarNonlinearFunction(:*, Any[x, x]),
            MOI.ScalarNonlinearFunction(:*, Any[1.0, x]),
            3.0,
        ],
    )
    MOI.add_constraint(model, g, MOI.Interval(1.0, 10.0))
    MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
    MOI.set(
        model,
        MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}(),
        MOI.ScalarNonlinearFunction(:log, Any[x]),
    )
    n = NL.Model()
    MOI.copy_to(n, model)
    @test sprint(write, n) == """
    g3 1 1 0
     1 1 1 1 0 0
     1 1
     0 0
     1 1 1
     0 0 0 1
     0 0 0 0 0
     1 1
     0 0
     0 0 0 0 0
    C0
    o54
    3
    o2
    v0
    v0
    o2
    n1
    v0
    n3
    O0 1
    o43
    v0
    x1
    0 0
    r
    0 1 10
    b
    3
    k0
    J0 1
    0 0
    G0 1
    0 0
    """
    return
end

function test_read_nlmodel_nlpblock_model()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variable(model)
    nlp = MOI.Nonlinear.Model()
    MOI.Nonlinear.add_constraint(nlp, :(log($x)), MOI.LessThan(1.0))
    MOI.Nonlinear.add_constraint(nlp, :(log($x)), MOI.GreaterThan(2.0))
    MOI.Nonlinear.add_constraint(nlp, :(log($x)), MOI.EqualTo(3.0))
    MOI.Nonlinear.add_constraint(nlp, :(log($x)), MOI.Interval(4.0, 5.0))
    evaluator =
        MOI.Nonlinear.Evaluator(nlp, MOI.Nonlinear.SparseReverseMode(), [x])
    block = MOI.NLPBlockData(evaluator)
    MOI.set(model, MOI.NLPBlock(), block)
    src = NL.Model()
    MOI.copy_to(src, model)
    text = sprint(write, src)
    # Now test reading back in
    dest = NL.Model(; use_nlp_block = true)
    io = IOBuffer(text)
    MOI.read!(io, dest)
    nlp_block = MOI.get(dest, MOI.NLPBlock())
    @test length(nlp_block.constraint_bounds) == 4
    @test nlp_block.constraint_bounds == [
        MOI.NLPBoundsPair(-Inf, 1.0),
        MOI.NLPBoundsPair(2.0, Inf),
        MOI.NLPBoundsPair(3.0, 3.0),
        MOI.NLPBoundsPair(4.0, 5.0),
    ]
    return
end

function test_read_nlmodel_scalar_nonlinear_function_model()
    function build_model(x)
        objective = MOI.ScalarNonlinearFunction(:log, Any[x])
        g = MOI.ScalarNonlinearFunction(
            :+,
            Any[
                MOI.ScalarNonlinearFunction(:*, Any[x, x]),
                MOI.ScalarNonlinearFunction(:*, Any[1.0, x]),
                3.0,
            ],
        )
        constraints = [
            g => MOI.Interval(1.0, 10.0),
            MOI.ScalarNonlinearFunction(:*, Any[x, x]) => MOI.EqualTo(0.0),
            MOI.ScalarNonlinearFunction(:/, Any[1, x]) => MOI.LessThan(1.0),
            MOI.ScalarNonlinearFunction(:sin, Any[x]) =>
                MOI.GreaterThan(0.0),
        ]
        return objective, constraints
    end
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variable(model)
    obj, cons = build_model(x)
    MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
    MOI.set(model, MOI.ObjectiveFunction{typeof(obj)}(), obj)
    for (f, s) in cons
        MOI.add_constraint(model, f, s)
    end
    src = NL.Model()
    MOI.copy_to(src, model)
    text = sprint(write, src)
    # Now test reading back in
    dest = NL.Model(; use_nlp_block = false)
    io = IOBuffer(text)
    MOI.read!(io, dest)
    v = MOI.get(dest, MOI.ListOfVariableIndices())
    @test length(v) == 1
    x = v[1]
    obj, cons = build_model(x)
    @test isapprox(MOI.get(dest, MOI.ObjectiveFunction{typeof(obj)}()), obj)
    for (f, s) in cons
        F, S = typeof(f), typeof(s)
        indices = MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
        @test length(indices) == 1
        @test isapprox(MOI.get(model, MOI.ConstraintFunction(), indices[1]), f)
        @test MOI.get(model, MOI.ConstraintSet(), indices[1]) == s
    end
    return
end

function test_read_nlmodel_scalar_nonlinear_function_model_no_objective()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variable(model)
    MOI.add_constraint(
        model,
        MOI.ScalarNonlinearFunction(:log, Any[x]),
        MOI.LessThan(1.0),
    )
    src = NL.Model()
    MOI.copy_to(src, model)
    text = sprint(write, src)
    # Now test reading back in
    dest = NL.Model(; use_nlp_block = false)
    io = IOBuffer(text)
    MOI.read!(io, dest)
    v = MOI.get(dest, MOI.ListOfVariableIndices())
    @test length(v) == 1
    x = v[1]
    @test !in(
        MOI.ObjectiveFunction{MOI.ScalarNonlinearFunction}(),
        MOI.get(model, MOI.ListOfModelAttributesSet()),
    )
    return
end

function test_eval_singlevariable()
    x = MOI.VariableIndex(1)
    f = NL._NLExpr(x)
    @test NL._evaluate(f, Dict(x => 1.2)) == 1.2
end

function test_eval_scalaraffine()
    x = MOI.VariableIndex.(1:3)
    f = NL._NLExpr(MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, x), 4.0))
    @test NL._evaluate(f, Dict(x[i] => Float64(i) for i in 1:3)) == 10.0
end

function test_eval_scalarquadratic()
    x = MOI.VariableIndex(1)
    f = MOI.ScalarQuadraticFunction(
        [MOI.ScalarQuadraticTerm(2.0, x, x)],
        [MOI.ScalarAffineTerm(1.1, x)],
        3.0,
    )
    @test NL._evaluate(NL._NLExpr(f), Dict(x => 1.1)) == 5.42
end

function test_eval_unary_addition()
    x = MOI.VariableIndex(1)
    @test NL._evaluate(NL._NLExpr(:(+$x)), Dict(x => 1.1)) == 1.1
end

function test_eval_binary_addition()
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    @test NL._evaluate(NL._NLExpr(:($x + $y)), Dict(x => 1.1, y => 2.2)) ≈ 3.3
end

function test_eval_nary_addition()
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    @test NL._evaluate(NL._NLExpr(:($x + $y + 1.0)), Dict(x => 1.1, y => 2.2)) ≈
          4.3
end

function test_eval_unary_subtraction()
    x = MOI.VariableIndex(1)
    @test NL._evaluate(NL._NLExpr(:(-$x)), Dict(x => 1.1)) == -1.1
end

function test_eval_nary_multiplication()
    x = MOI.VariableIndex(1)
    @test NL._evaluate(NL._NLExpr(:($x * $x * 2.0)), Dict(x => 1.1)) ≈ 2.42
end

function test_eval_unary_specialcases()
    x = MOI.VariableIndex(1)
    S = [:acoth, :asec, :acsc, :acot, :asecd, :acscd, :acotd]
    for (k, v) in NL._UNARY_SPECIAL_CASES
        expr = NL._NLExpr(:($k($x)))
        xv = k in S ? 1.49 : 0.49
        @test NL._evaluate(expr, Dict(x => xv)) ≈ getfield(Main, k)(xv)
    end
end

function test_eval_binary_specialcases()
    x = MOI.VariableIndex(1)
    y = MOI.VariableIndex(2)
    for (k, v) in NL._BINARY_SPECIAL_CASES
        expr = NL._NLExpr(:($k($x, $y)))
        target = getfield(Main, k)(1.0, 2.0)
        @test NL._evaluate(expr, Dict(x => 1.0, y => 2.0)) ≈ target
    end
end

function test_evaluate_domain_error()
    x = MOI.VariableIndex(1)
    expr = NL._NLExpr(:(ifelse($x > 0, log($x), 0.0)))
    @test NL._evaluate(expr, Dict(x => 1.1)) == log(1.1)
    @test NL._evaluate(expr, Dict(x => 0.0)) == 0.0
    expr = NL._NLExpr(:(ifelse($x > 0, $x^1.5, -(-$x)^1.5)))
    @test NL._evaluate(expr, Dict(x => 1.1)) ≈ 1.1^1.5
    @test NL._evaluate(expr, Dict(x => -1.1)) ≈ -(1.1^1.5)
    return
end

"""
    test_issue_79()

Test the problem

    min (z - 0.5)^2 = z^2 - z + 1/4
    s.t. x * z <= 0
         z ∈ {0, 1}
"""
function test_issue_79()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variable(model)
    z = MOI.add_variable(model)
    MOI.add_constraint(model, z, MOI.ZeroOne())
    f = MOI.ScalarQuadraticFunction(
        [MOI.ScalarQuadraticTerm(1.0, z, z)],
        [MOI.ScalarAffineTerm(-1.0, z)],
        0.25,
    )
    MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
    MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    MOI.add_constraint(
        model,
        MOI.ScalarQuadraticFunction(
            [MOI.ScalarQuadraticTerm(1.0, x, z)],
            MOI.ScalarAffineTerm{Float64}[],
            0.0,
        ),
        MOI.LessThan(0.0),
    )
    n = NL.Model()
    MOI.copy_to(n, model)
    # x is continuous in a nonlinear constraint             [Priority 3]
    # z is discrete in a nonlinear constraint and objective [Priority 2]
    @test n.x[x].order == 1
    @test n.x[z].order == 0
end

function test_empty_constraint()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variable(model)
    ci = MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{Float64}[], 1.0),
        MOI.LessThan(2.0),
    )
    n = NL.Model()
    mapping = MOI.copy_to(n, model)
    @test haskey(mapping, ci)
end

function test_writing_free_constraint()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variable(model)
    f = MOI.ScalarNonlinearFunction(:log, Any[x])
    MOI.add_constraint(model, f, MOI.Interval(-Inf, Inf))
    n = NL.Model()
    _ = MOI.copy_to(n, model)
    @test occursin("r\n3\nb", sprint(write, n))
    return
end

function test_malformed_constraint_error()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    x = MOI.add_variable(model)
    MOI.add_constraint(
        model,
        MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{Float64}[], 1.0),
        MOI.LessThan(0.0),
    )
    n = NL.Model()
    @test_throws ErrorException MOI.copy_to(n, model)
end

struct NoExprGraph <: MOI.AbstractNLPEvaluator end
MOI.features_available(::NoExprGraph) = Symbol[]

function test_noexpr_graph()
    model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
    block_data = MOI.NLPBlockData(MOI.NLPBoundsPair[], NoExprGraph(), false)
    MOI.set(model, MOI.NLPBlock(), block_data)
    n = NL.Model()
    @test_throws ErrorException MOI.copy_to(n, model)
end

function test_Name()
    model = NL.Model()
    @test MOI.supports(model, MOI.Name())
    MOI.set(model, MOI.Name(), "MyModel")
    @test MOI.get(model, MOI.Name()) == "MyModel"
end

function test_SolverName()
    model = NL.Model()
    @test MOI.get(model, MOI.SolverName()) == "AmplNLWriter"
end

function test_show()
    @test sprint(summary, NL.Model()) == "MOI.FileFormats.NL.Model"
    return
end

function test_linear_constraint_types()
    model = MOI.Utilities.Model{Float64}()
    n = NL.Model()
    y = MOI.add_variables(model, 3)
    MOI.add_constraint(model, y[1], MOI.ZeroOne())
    MOI.add_constraint(model, y[2], MOI.Integer())
    @test MOI.supports_constraint(n, MOI.VariableIndex, MOI.ZeroOne)
    @test MOI.supports_constraint(n, MOI.VariableIndex, MOI.Integer)
    for set in [
        MOI.GreaterThan(0.0),
        MOI.LessThan(1.0),
        MOI.EqualTo(2.0),
        MOI.Interval(3.0, 4.0),
    ]
        x = MOI.add_variable(model)
        @test MOI.supports_constraint(n, MOI.VariableIndex, typeof(set))
        @test MOI.supports_constraint(
            n,
            MOI.ScalarAffineFunction{Float64},
            typeof(set),
        )
        MOI.add_constraint(model, x, set)
        MOI.add_constraint(
            model,
            MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0),
            set,
        )
    end
    map = MOI.copy_to(n, model)
    @test length(n.g) == 0
    @test length(n.h) == 4
    @test length(n.x) == 7
    @test n.order == [
        MOI.VariableIndex(3),
        MOI.VariableIndex(4),
        MOI.VariableIndex(5),
        MOI.VariableIndex(6),
        MOI.VariableIndex(7),
        MOI.VariableIndex(1),
        MOI.VariableIndex(2),
    ]
    @test sprint(Base.write, n) == """
    g3 1 1 0
     7 4 1 1 1 0
     0 1
     0 0
     0 0 0
     0 0 0 1
     1 1 0 0 0
     4 0
     0 0
     0 0 0 0 0
    C0
    n0
    C1
    n0
    C2
    n0
    C3
    n0
    O0 0
    n0
    x7
    0 0
    1 0
    2 0
    3 0
    4 0
    5 0
    6 0
    r
    4 2
    2 0
    1 1
    0 3 4
    b
    3
    2 0
    1 1
    4 2
    0 3 4
    0 0 1
    3
    k6
    0
    1
    2
    3
    4
    4
    J0 1
    3 1
    J1 1
    1 1
    J2 1
    2 1
    J3 1
    4 1
    """
    return
end

function test_empty()
    model = MOI.Utilities.Model{Float64}()
    y = MOI.add_variables(model, 3)
    n = NL.Model()
    map = MOI.copy_to(n, model)
    @test !MOI.is_empty(n)
    MOI.empty!(n)
    @test MOI.is_empty(n)
end

function test_moi()
    MOI.Test.runtests(
        NL.Model(),
        MOI.Test.Config(exclude = Any[MOI.optimize!]),
        include = ["test_model_copy_to_Unsupported"],
    )
    return
end

function test_float_rounding()
    @test NL._str(1.0) == "1"
    @test NL._str(1.2) == "1.2"
    @test NL._str(1e50) == "1.0e50"
    @test NL._str(-1e50) == "-1.0e50"
    return
end

function test_attribute_error()
    model = NL.Model()
    attr = MOI.ObjectiveSense()
    @test_throws(
        ErrorException(
            "Unable get attribute $attr because `NL.Model` only supports " *
            "getting attributes when the model was read from a file.",
        ),
        MOI.get(model, attr),
    )
    return
end

function test_infinite_interval()
    model = NL.Model()
    src = MOI.Utilities.Model{Float64}()
    x = MOI.add_variable(src)
    MOI.add_constraint(src, 1.0 * x, MOI.Interval(-Inf, Inf))
    MOI.add_constraint(src, 1.0 * x, MOI.Interval(-Inf, 1.0))
    MOI.add_constraint(src, 1.0 * x, MOI.Interval(2.0, Inf))
    MOI.add_constraint(src, 1.0 * x, MOI.Interval(3.0, 4.0))
    MOI.copy_to(model, src)
    @test sprint(write, model) == """
g3 1 1 0
 1 4 1 1 0 0
 0 1
 0 0
 0 0 0
 0 0 0 1
 0 0 0 0 0
 4 0
 0 0
 0 0 0 0 0
C0
n0
C1
n0
C2
n0
C3
n0
O0 0
n0
x1
0 0
r
3
1 1
2 2
0 3 4
b
3
k0
J0 1
0 1
J1 1
0 1
J2 1
0 1
J3 1
0 1
"""
    return
end

function test_copy_name_issue_2445()
    src = MOI.Utilities.Model{Float64}()
    MOI.set(src, MOI.Name(), "TestModel")
    dest = NL.Model()
    MOI.copy_to(dest, src)
    @test MOI.get(dest, MOI.Name()) == "TestModel"
    return
end

function test_unsupported_variable_types()
    for set in (
        MOI.Parameter(2.0),
        MOI.Semicontinuous(2.0, 3.0),
        MOI.Semiinteger(2.0, 3.0),
    )
        src = MOI.Utilities.Model{Float64}()
        MOI.add_constrained_variable(src, set)
        dest = NL.Model()
        @test_throws MOI.UnsupportedConstraint MOI.copy_to(dest, src)
    end
    return
end

function test_unsupported_objectives()
    model = NL.Model()
    for (F, ret) in [
        MOI.VariableIndex => true,
        MOI.ScalarAffineFunction{Float64} => true,
        MOI.ScalarQuadraticFunction{Float64} => true,
        MOI.ScalarNonlinearFunction => true,
        MOI.VectorOfVariables => false,
        MOI.VectorAffineFunction{Float64} => false,
        MOI.VectorQuadraticFunction{Float64} => false,
        MOI.VectorNonlinearFunction => false,
    ]
        @test MOI.supports(model, MOI.ObjectiveFunction{F}()) == ret
    end
    return
end

end

TestNLModel.runtests()

include("read.jl")
include("sol.jl")
