#  Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
#  This Source Code Form is subject to the terms of the Mozilla Public
#  License, v. 2.0. If a copy of the MPL was not distributed with this
#  file, You can obtain one at https://mozilla.org/MPL/2.0/.
#############################################################################
# JuMP
# An algebraic modeling language for Julia
# See https://github.com/jump-dev/JuMP.jl
#############################################################################
# test/print.jl
# Testing for all pretty-printing-related functionality
#############################################################################

module TestPrint

using JuMP
using LinearAlgebra
using Test

include("JuMPExtension.jl")

# Helper function to test IO methods work correctly
function _io_test_show(::MIME"text/plain", obj, exp_str)
    @test sprint(show, obj) == exp_str
    return
end

_math_mode(::Any, s) = string("\$\$ ", s, " \$\$")
_math_mode(::AbstractJuMPScalar, s) = string("\$ ", s, " \$")

function _io_test_show(::MIME"text/latex", obj, exp_str)
    @test sprint(show, "text/latex", obj) == _math_mode(obj, exp_str)
    return
end

function _io_test_print(::MIME"text/plain", obj, exp_str)
    @test sprint(print, obj) == exp_str
    return
end

function _io_test_print(::MIME"text/latex", obj, exp_str)
    @test sprint(show, "text/latex", obj) == _math_mode(obj, exp_str)
    return
end

function _io_test_print(::MIME"text/plain", obj::AbstractModel, exp_str)
    @test sprint(print, obj) == exp_str
    @test model_string(MIME("text/plain"), obj) == exp_str
    return
end

function _io_test_print(::MIME"text/latex", obj::AbstractModel, exp_str)
    model = latex_formulation(obj)
    @test sprint(io -> show(io, MIME("text/latex"), model)) ==
          string("\$\$ ", exp_str, " \$\$")
    @test model_string(MIME("text/latex"), obj) ==
          string("\$\$ ", exp_str, " \$\$")
    # TODO(odow): I don't know how to test an IJulia display without adding
    # IJulia as a test-dependency, so just print and check it doesn't error.
    print(obj)
    return
end

function io_test(mode, obj, exp_str; repl = :both)
    if repl == :show || repl == :both
        _io_test_show(mode, obj, exp_str)
    end
    if repl == :print || repl == :both
        _io_test_print(mode, obj, exp_str)
    end
    return
end

# Used to test that JuMP printing works correctly for types for which
# oneunit is not convertible to Float64
struct UnitNumber <: Number
    α::Float64
end
Base.zero(::Union{UnitNumber,Type{UnitNumber}}) = UnitNumber(0.0)
Base.oneunit(::Union{UnitNumber,Type{UnitNumber}}) = UnitNumber(1.0)
Base.:(+)(u::UnitNumber, v::UnitNumber) = UnitNumber(u.α + v.α)
Base.:(-)(u::UnitNumber, v::UnitNumber) = UnitNumber(u.α - v.α)
Base.:(*)(α::Float64, u::UnitNumber) = UnitNumber(α * u.α)
Base.abs(u::UnitNumber) = UnitNumber(abs(u.α))
Base.isless(u::UnitNumber, v::UnitNumber) = isless(u.α, v.α)

# Used to test extensibility of JuMP printing for `AbstractConstraint`
struct CustomConstraint{S<:AbstractShape} <: AbstractConstraint
    function_str::String
    in_set_str::String
    shape::S
end

function JuMP.function_string(mode, constraint::CustomConstraint)
    return constraint.function_str
end

function JuMP.in_set_string(mode, constraint::CustomConstraint)
    return constraint.in_set_str
end
struct CustomIndex
    value::Int
end

function JuMP.add_constraint(
    model::GenericModel,
    constraint::CustomConstraint,
    name::String,
)
    if !haskey(model.ext, :custom)
        model.ext[:custom_constraints] = CustomConstraint[]
        model.ext[:custom_names] = String[]
    end
    constraints = model.ext[:custom_constraints]
    push!(constraints, constraint)
    push!(model.ext[:custom_names], name)
    return ConstraintRef(
        model,
        CustomIndex(length(constraints)),
        constraint.shape,
    )
end

function JuMP.is_valid(
    model::M,
    con_ref::ConstraintRef{M,CustomIndex},
) where {M<:GenericModel}
    return 1 <= con_ref.index.value <= length(model.ext[:custom_constraints])
end

function JuMP.constraint_object(cref::ConstraintRef{Model,CustomIndex})
    return cref.model.ext[:custom_constraints][cref.index.value]
end

function JuMP.name(cref::ConstraintRef{Model,CustomIndex})
    return cref.model.ext[:custom_names][cref.index.value]
end

function test_printing_expressions()
    # Most of the expression logic is well covered by test/operator.jl
    # This is really just to check IJulia printing for expressions
    mod = Model()
    @variable(mod, x[1:5])
    @variable(mod, y[i=2:4, j=i:5])
    @variable(mod, z)

    ex = @expression(mod, x[1] + 2 * y[2, 3])
    io_test(MIME("text/plain"), ex, "x[1] + 2 y[2,3]")
    io_test(MIME("text/latex"), ex, "x_{1} + 2 y_{2,3}")

    ex = @expression(mod, x[1] + 2 * y[2, 3] + x[1])
    io_test(MIME("text/plain"), ex, "2 x[1] + 2 y[2,3]")
    io_test(MIME("text/latex"), ex, "2 x_{1} + 2 y_{2,3}")

    # TODO: These tests shouldn't depend on order of the two variables in
    # quadratic terms, that is, x*y vs y*x.
    ex = @expression(mod, (x[1] + x[2]) * (y[2, 2] + 3.0))
    io_test(
        MIME("text/plain"),
        ex,
        "x[1]*y[2,2] + x[2]*y[2,2] + 3 x[1] + 3 x[2]",
    )
    io_test(
        MIME("text/latex"),
        ex,
        "x_{1}\\times y_{2,2} + x_{2}\\times y_{2,2} + 3 x_{1} + 3 x_{2}",
    )

    ex = @expression(mod, (y[2, 2] + 3.0) * (x[1] + x[2]))
    io_test(
        MIME("text/plain"),
        ex,
        "y[2,2]*x[1] + y[2,2]*x[2] + 3 x[1] + 3 x[2]",
    )
    io_test(
        MIME("text/latex"),
        ex,
        "y_{2,2}\\times x_{1} + y_{2,2}\\times x_{2} + 3 x_{1} + 3 x_{2}",
    )

    ex = @expression(mod, (x[1] + x[2]) * (y[2, 2] + 3.0) + z^2 - 1)
    repl_sq = JuMP._math_symbol(MIME("text/plain"), :sq)
    io_test(
        MIME("text/plain"),
        ex,
        "x[1]*y[2,2] + x[2]*y[2,2] + z$repl_sq + 3 x[1] + 3 x[2] - 1",
    )
    ijulia_sq = JuMP._math_symbol(MIME("text/latex"), :sq)
    io_test(
        MIME("text/latex"),
        ex,
        "x_{1}\\times y_{2,2} + x_{2}\\times y_{2,2} + z$ijulia_sq + 3 x_{1} + 3 x_{2} - 1",
    )

    ex = @expression(mod, -z * x[1] - z * x[1] + x[1] * x[2] + 0 * z^2)
    io_test(MIME("text/plain"), ex, "-2 z*x[1] + x[1]*x[2]")
    io_test(MIME("text/latex"), ex, "-2 z\\times x_{1} + x_{1}\\times x_{2}")

    ex = z^2 + x[1] - z^2 - x[1]
    io_test(MIME("text/plain"), ex, "0 z² + 0 x[1]")
    io_test(MIME("text/latex"), ex, "0 z$ijulia_sq + 0 x_{1}")
    return
end

# See https://github.com/jump-dev/JuMP.jl/pull/1352
function test_printing_expression_unit_coefficient_type()
    m = Model()
    @variable m x
    @variable m y
    u = UnitNumber(2.0)
    z = UnitNumber(0.0)
    aff = GenericAffExpr(zero(u), x => u, y => zero(u))
    io_test(MIME("text/plain"), aff, "$u x + $z y")
    io_test(MIME("text/latex"), aff, "$u x + $z y")
    drop_zeros!(aff)
    io_test(MIME("text/plain"), aff, "$u x")
    io_test(MIME("text/latex"), aff, "$u x")
    quad = aff * x
    io_test(MIME("text/plain"), quad, "$u x² + $z")
    io_test(MIME("text/latex"), quad, "$u x^2 + $z")
    return
end

function test_nonlinear_expressions()
    model = Model()
    @variable(model, x)
    expr = @NLexpression(model, x + 1)
    io_test(MIME("text/plain"), expr, "subexpression[1]: x + 1.0")
    return
end

function test_nonlinear_parameters()
    model = Model()
    param = @NLparameter(model, value = 1.0)
    io_test(MIME("text/plain"), param, "parameter[1] == 1.0")
    return
end

function test_registered_nonlinear_parameters()
    model = Model()
    @NLparameter(model, param == 1.0)
    io_test(MIME("text/plain"), param, "param == 1.0")
    return
end

function test_printing_NLPEvaluator()
    model = Model()
    evaluator = NLPEvaluator(model)
    io_test(
        MIME("text/plain"),
        evaluator,
        "Nonlinear.Evaluator with available features:\n" *
        "  * :Grad\n" *
        "  * :Jac\n" *
        "  * :JacVec\n" *
        "  * :Hess\n" *
        "  * :HessVec\n" *
        "  * :ExprGraph",
    )
    return
end

function test_nonlinear_constraints()
    le = JuMP._math_symbol(MIME("text/plain"), :leq)
    ge = JuMP._math_symbol(MIME("text/plain"), :geq)
    eq = JuMP._math_symbol(MIME("text/plain"), :eq)

    model = Model()
    @variable(model, x)
    constr_le = @NLconstraint(model, sin(x) <= 1)
    constr_ge = @NLconstraint(model, sin(x) >= 1)
    constr_eq = @NLconstraint(model, sin(x) == 1)
    constr_range = @NLconstraint(model, 0 <= sin(x) <= 1)
    constr_exponent_1 = @NLconstraint(model, x^4 <= 1)
    constr_exponent_2 = @NLconstraint(model, x^40.23 <= 1)
    constr_exponent_3 = @NLconstraint(model, x^4 + x^3 + x^2 <= 1)
    constr_exponent_4 = @NLconstraint(model, x^-4 + x^-3 + x^-2 <= 1)
    constr_exponent_5 = @NLconstraint(model, x^(x * x) <= 1)
    constr_exponent_6 = @NLconstraint(model, x^(2x) <= 1)
    constr_exponent_7 = @NLconstraint(model, x^(2 * 5) <= 1)
    constr_exponent_8 = @NLconstraint(model, x^(x^2) <= 1)

    io_test(MIME("text/plain"), constr_le, "sin(x) - 1.0 $le 0")
    io_test(MIME("text/plain"), constr_ge, "sin(x) - 1.0 $ge 0")
    io_test(MIME("text/plain"), constr_eq, "sin(x) - 1.0 $eq 0")
    # Note: This is inconsistent with the "x in [-1, 1]" printing for
    # regular constraints.
    io_test(MIME("text/plain"), constr_range, "0 $le sin(x) $le 1")
    io_test(MIME("text/plain"), constr_exponent_1, "x ^ 4.0 - 1.0 $le 0")
    io_test(MIME("text/plain"), constr_exponent_2, "x ^ 40.23 - 1.0 $le 0")
    io_test(
        MIME("text/plain"),
        constr_exponent_3,
        "(x ^ 4.0 + x ^ 3.0 + x ^ 2.0) - 1.0 $le 0",
    )
    io_test(
        MIME("text/plain"),
        constr_exponent_4,
        "(x ^ -4.0 + x ^ -3.0 + x ^ -2.0) - 1.0 $le 0",
    )
    io_test(MIME("text/plain"), constr_exponent_5, "x ^ (x * x) - 1.0 $le 0")
    io_test(MIME("text/plain"), constr_exponent_6, "x ^ (2.0 * x) - 1.0 $le 0")
    io_test(
        MIME("text/plain"),
        constr_exponent_7,
        "x ^ (2.0 * 5.0) - 1.0 $le 0",
    )
    io_test(MIME("text/plain"), constr_exponent_8, "x ^ (x ^ 2.0) - 1.0 $le 0")

    io_test(MIME("text/latex"), constr_le, "sin(x) - 1.0 \\leq 0")
    io_test(MIME("text/latex"), constr_ge, "sin(x) - 1.0 \\geq 0")
    io_test(MIME("text/latex"), constr_eq, "sin(x) - 1.0 = 0")
    io_test(MIME("text/latex"), constr_range, "0 \\leq sin(x) \\leq 1")
    io_test(MIME("text/latex"), constr_exponent_1, "x ^ {4.0} - 1.0 \\leq 0")
    io_test(MIME("text/latex"), constr_exponent_2, "x ^ {40.23} - 1.0 \\leq 0")
    io_test(
        MIME("text/latex"),
        constr_exponent_3,
        "(x ^ {4.0} + x ^ {3.0} + x ^ {2.0}) - 1.0 \\leq 0",
    )
    io_test(
        MIME("text/latex"),
        constr_exponent_4,
        "(x ^ {-4.0} + x ^ {-3.0} + x ^ {-2.0}) - 1.0 \\leq 0",
    )
    io_test(MIME("text/latex"), constr_exponent_5, "x ^ {x * x} - 1.0 \\leq 0")
    io_test(
        MIME("text/latex"),
        constr_exponent_6,
        "x ^ {2.0 * x} - 1.0 \\leq 0",
    )
    io_test(
        MIME("text/latex"),
        constr_exponent_7,
        "x ^ {2.0 * 5.0} - 1.0 \\leq 0",
    )
    io_test(
        MIME("text/latex"),
        constr_exponent_8,
        "x ^ {x ^ {2.0}} - 1.0 \\leq 0",
    )
    return
end

function test_nonlinear_constraint_with_anon_param_and_subexpression()
    le = JuMP._math_symbol(MIME("text/plain"), :leq)
    model = Model()
    @variable(model, x)
    expr = @NLexpression(model, x + 1)
    param = @NLparameter(model, value = 1.0)
    constr = @NLconstraint(model, expr - param <= 0)
    io_test(
        MIME("text/plain"),
        constr,
        "(subexpression[1] - parameter[1]) - 0.0 $le 0",
    )
    io_test(
        MIME("text/latex"),
        constr,
        "(subexpression_{1} - parameter_{1}) - 0.0 \\leq 0",
    )
    return
end

function test_nonlinear_constraint_with_named_param_and_subexpression()
    le = JuMP._math_symbol(MIME("text/plain"), :leq)
    model = Model()
    @variable(model, x)
    expr = @NLexpression(model, x + 1)
    model[:param] = @NLparameter(model, param == 1.0)
    constr = @NLconstraint(model, expr - param <= 0)
    io_test(
        MIME("text/plain"),
        constr,
        "(subexpression[1] - param) - 0.0 $le 0",
    )
    io_test(
        MIME("text/latex"),
        constr,
        "(subexpression_{1} - param) - 0.0 \\leq 0",
    )
    return
end

function test_printing_custom_constraint()
    model = Model()
    function test_constraint(function_str, in_set_str, name)
        constraint = CustomConstraint(function_str, in_set_str, ScalarShape())
        cref = add_constraint(model, constraint, name)
        @test string(cref) == "$name : $function_str $in_set_str"
    end
    test_constraint("fun", "set", "name")
    test_constraint("a", "b", "c")
    return
end

function test_extension_printing_variable_ref(
    ModelType = Model,
    VariableRefType = VariableRef,
)
    m = ModelType()
    @variable(m, 0 <= x <= 2)
    @test name(x) == "x"
    io_test(MIME("text/plain"), x, "x")
    io_test(MIME("text/latex"), x, "x")

    set_name(x, "x2")
    @test name(x) == "x2"
    io_test(MIME("text/plain"), x, "x2")
    io_test(MIME("text/latex"), x, "x2")
    set_name(x, "")
    @test name(x) == ""
    if x isa GenericVariableRef
        io_test(MIME("text/plain"), x, "_[1]")
        io_test(MIME("text/latex"), x, "{\\_}_{1}")
    else
        io_test(MIME("text/plain"), x, "anon")
        io_test(MIME("text/latex"), x, "anon")
    end
    @variable(m, z[1:2, 3:5])
    @test name(z[1, 3]) == "z[1,3]"
    io_test(MIME("text/plain"), z[1, 3], "z[1,3]")
    io_test(MIME("text/latex"), z[1, 3], "z_{1,3}")
    @test name(z[2, 4]) == "z[2,4]"
    io_test(MIME("text/plain"), z[2, 4], "z[2,4]")
    io_test(MIME("text/latex"), z[2, 4], "z_{2,4}")
    @test name(z[2, 5]) == "z[2,5]"
    io_test(MIME("text/plain"), z[2, 5], "z[2,5]")
    io_test(MIME("text/latex"), z[2, 5], "z_{2,5}")
    @variable(m, w[3:9, ["red", "blue", "green"]])
    @test name(w[7, "green"]) == "w[7,green]"
    io_test(MIME("text/plain"), w[7, "green"], "w[7,green]")
    io_test(MIME("text/latex"), w[7, "green"], "w_{7,green}")
    rng = 2:5
    @variable(m, v[rng, rng, rng, rng, rng, rng, rng])
    a_v = v[4, 5, 2, 3, 2, 2, 4]
    @test name(a_v) == "v[4,5,2,3,2,2,4]"
    io_test(MIME("text/plain"), a_v, "v[4,5,2,3,2,2,4]")
    io_test(MIME("text/latex"), a_v, "v_{4,5,2,3,2,2,4}")
    return
end

function test_extension_printing_base_name_keyword_argument(
    ModelType = Model,
    VariableRefType = VariableRef,
)
    m = ModelType()
    @variable(m, x, base_name = "foo")
    @variable(m, y[1:3], base_name = "bar")
    num = 123
    @variable(m, z[[:red, :blue]], base_name = "color_$num")
    @variable(m, v[1:2, 1:2], PSD, base_name = string("i", "$num", num))
    @variable(m, w[1:3, 1:3], Symmetric, base_name = "symm")
    io_test(MIME("text/plain"), x, "foo")
    io_test(MIME("text/latex"), x, "foo")
    io_test(MIME("text/plain"), y[2], "bar[2]")
    io_test(MIME("text/latex"), y[2], "bar_{2}")
    io_test(MIME("text/plain"), z[:red], "color_123[red]")
    io_test(MIME("text/latex"), z[:red], "color\\_123_{red}")
    io_test(MIME("text/plain"), v[2, 1], "i123123[1,2]")
    io_test(MIME("text/latex"), v[2, 1], "i123123_{1,2}")
    io_test(MIME("text/plain"), w[1, 3], "symm[1,3]")
    io_test(MIME("text/latex"), w[1, 3], "symm_{1,3}")
    return
end

function test_extension_printing_vectorofvariables(
    ModelType = Model,
    VariableRefType = VariableRef,
)
    ge = JuMP._math_symbol(MIME("text/plain"), :geq)
    in_sym = JuMP._math_symbol(MIME("text/plain"), :in)
    model = ModelType()
    @variable(model, x)
    @variable(model, y)
    zero_constr = @constraint(model, [x, y] in MOI.Zeros(2))
    io_test(
        MIME("text/plain"),
        zero_constr,
        "[x, y] $in_sym MathOptInterface.Zeros(2)",
    )
    # TODO: Test in IJulia mode and do nice printing for Zeros().
    return
end

function test_extension_printing_scalaraffinefunction_constraints(
    ModelType = Model,
    VariableRefType = VariableRef,
)
    le = JuMP._math_symbol(MIME("text/plain"), :leq)
    ge = JuMP._math_symbol(MIME("text/plain"), :geq)
    eq = JuMP._math_symbol(MIME("text/plain"), :eq)
    in_sym = JuMP._math_symbol(MIME("text/plain"), :in)
    model = ModelType()
    @variable(model, x)
    @constraint(model, linear_le, x + 0 <= 1)
    @constraint(model, linear_ge, x + 0 >= 1)
    @constraint(model, linear_eq, x + 0 == 1)
    @constraint(model, linear_range, -1 <= x + 0 <= 1)
    linear_noname = @constraint(model, x + 0 <= 1)
    io_test(MIME("text/plain"), linear_le, "linear_le : x $le 1")
    io_test(MIME("text/plain"), linear_eq, "linear_eq : x $eq 1")
    io_test(
        MIME("text/plain"),
        linear_range,
        "linear_range : x $in_sym [-1, 1]",
    )
    io_test(MIME("text/plain"), linear_noname, "x $le 1")
    # io_test doesn't work here because constraints print with a mix of math
    # and non-math.
    @test sprint(show, "text/latex", linear_le) == "\$\$ x \\leq 1 \$\$"
    @test sprint(show, "text/latex", linear_ge) == "\$\$ x \\geq 1 \$\$"
    @test sprint(show, "text/latex", linear_eq) == "\$\$ x = 1 \$\$"
    @test sprint(show, "text/latex", linear_range) == "\$\$ x \\in [-1, 1] \$\$"
    @test sprint(show, "text/latex", linear_noname) == "\$\$ x \\leq 1 \$\$"
    return
end

function test_extension_printing_vectoraffinefunction_constraints(
    ModelType = Model,
    VariableRefType = VariableRef,
)
    in_sym = JuMP._math_symbol(MIME("text/plain"), :in)
    model = ModelType()
    @variable(model, x)
    @constraint(model, soc_constr, [x - 1, x + 1] in SecondOrderCone())
    io_test(
        MIME("text/plain"),
        soc_constr,
        "soc_constr : " *
        "[x - 1, x + 1] $in_sym MathOptInterface.SecondOrderCone(2)",
    )
    # TODO: Test in IJulia mode.
    return
end

function test_extension_printing_scalarquadraticfunction_constraints(
    ModelType = Model,
    VariableRefType = VariableRef,
)
    in_sym = JuMP._math_symbol(MIME("text/plain"), :in)
    le = JuMP._math_symbol(MIME("text/plain"), :leq)
    sq = JuMP._math_symbol(MIME("text/plain"), :sq)

    model = ModelType()
    @variable(model, x)
    quad_constr = @constraint(model, 2x^2 <= 1)

    io_test(MIME("text/plain"), quad_constr, "2 x$sq $le 1")
    # TODO: Test in IJulia mode.
    return
end

function test_extension_printing_indicator_constraints(
    ModelType = Model,
    VariableRefType = VariableRef,
)
    le = JuMP._math_symbol(MIME("text/plain"), :leq)

    model = ModelType()
    @variable(model, x, Bin)
    @variable(model, y)
    ind_constr = @constraint(model, !x => {y <= 1})

    io_test(MIME("text/plain"), ind_constr, "!x --> {y $le 1}")
    # TODO: Test in IJulia mode.
    return
end

# Test printing of models of type `ModelType` for which the model is stored in
# an MOI backend
function test_model_printing()
    ModelType = Model
    repl(s) = JuMP._math_symbol(MIME("text/plain"), s)
    le, ge, eq, inset = repl(:leq), repl(:geq), repl(:eq), repl(:in)
    #------------------------------------------------------------------

    model_1 = ModelType()
    @variable(model_1, a >= 1)
    @variable(model_1, b <= 1)
    @variable(model_1, -1 <= c <= 1)
    @variable(model_1, a1 >= 1, Int)
    @variable(model_1, b1 <= 1, Int)
    @variable(model_1, -1 <= c1 <= 1, Int)
    @variable(model_1, x, Bin)
    @variable(model_1, y)
    @variable(model_1, z, Int)
    @variable(model_1, u[1:3], Bin)
    @variable(model_1, fi == 9)
    @objective(model_1, Max, a - b + 2a1 - 10x)
    @constraint(model_1, con, a + b - 10c + c1 - 2x <= 1)
    @constraint(model_1, a * b <= 2)
    @constraint(model_1, soc, [1 - a; u] in SecondOrderCone())
    @constraint(model_1, [a b; c x] in PSDCone())
    @constraint(model_1, Symmetric([a b; b x]) in PSDCone())
    @constraint(model_1, [a, b, c] in MOI.PositiveSemidefiniteConeTriangle(2))
    @constraint(model_1, [a, b, c, x] in MOI.PositiveSemidefiniteConeSquare(2))

    VariableType = typeof(a)

    io_test(
        MIME("text/plain"),
        model_1,
        """
Max a - b + 2 a1 - 10 x
Subject to
 con : a + b - 10 c + c1 - 2 x $le 1
 a*b $le 2
 [a  b
  ⋯  x] $inset $(PSDCone())
 [a, b, c] $inset $(MOI.PositiveSemidefiniteConeTriangle(2))
 [a  b
  c  x] $inset $(PSDCone())
 [a, b, c, x] $inset $(MOI.PositiveSemidefiniteConeSquare(2))
 soc : [-a + 1, u[1], u[2], u[3]] $inset $(MOI.SecondOrderCone(4))
 fi $eq 9
 a $ge 1
 c $ge -1
 a1 $ge 1
 c1 $ge -1
 b $le 1
 c $le 1
 b1 $le 1
 c1 $le 1
 a1 integer
 b1 integer
 c1 integer
 z integer
 x binary
 u[1] binary
 u[2] binary
 u[3] binary
""";
        repl = :print,
    )

    io_test(
        MIME("text/plain"),
        model_1,
        """
        A JuMP Model
        ├ solver: none
        ├ objective_sense: MAX_SENSE
        │ └ objective_function_type: $(GenericAffExpr{Float64,VariableType})
        ├ num_variables: 13
        ├ num_constraints: 24
        │ ├ $(GenericAffExpr{Float64,VariableType}) in MOI.LessThan{Float64}: 1
        │ ├ $(GenericQuadExpr{Float64,VariableType}) in MOI.LessThan{Float64}: 1
        │ ├ $(Vector{VariableType}) in MOI.PositiveSemidefiniteConeTriangle: 2
        │ ├ $(Vector{VariableType}) in MOI.PositiveSemidefiniteConeSquare: 2
        │ ├ $(Vector{GenericAffExpr{Float64,VariableType}}) in MOI.SecondOrderCone: 1
        │ ├ $VariableType in MOI.EqualTo{Float64}: 1
        │ ├ $VariableType in MOI.GreaterThan{Float64}: 4
        │ ├ $VariableType in MOI.LessThan{Float64}: 4
        │ ├ $VariableType in MOI.Integer: 4
        │ └ $VariableType in MOI.ZeroOne: 4
        └ Names registered in the model
          └ :a, :a1, :b, :b1, :c, :c1, :con, :fi, :soc, :u, :x, :y, :z""";
        repl = :show,
    )

    io_test(
        MIME("text/latex"),
        model_1,
        "\\begin{aligned}\n" *
        "\\max\\quad & a - b + 2 a1 - 10 x\\\\\n" *
        "\\text{Subject to} \\quad & a + b - 10 c + c1 - 2 x \\leq 1\\\\\n" *
        " & a\\times b \\leq 2\\\\\n" *
        " & \\begin{bmatrix}\n" *
        "a & b\\\\\n" *
        "\\cdots & x\\\\\n" *
        "\\end{bmatrix} \\in \\text{$(PSDCone())}\\\\\n" *
        " & [a, b, c] \\in \\text{MathOptInterface.PositiveSemidefiniteConeTriangle(2)}\\\\\n" *
        " & \\begin{bmatrix}\n" *
        "a & b\\\\\n" *
        "c & x\\\\\n" *
        "\\end{bmatrix} \\in \\text{$(PSDCone())}\\\\\n" *
        " & [a, b, c, x] \\in \\text{MathOptInterface.PositiveSemidefiniteConeSquare(2)}\\\\\n" *
        " & [-a + 1, u_{1}, u_{2}, u_{3}] \\in \\text{MathOptInterface.SecondOrderCone(4)}\\\\\n" *
        " & fi = 9\\\\\n" *
        " & a \\geq 1\\\\\n" *
        " & c \\geq -1\\\\\n" *
        " & a1 \\geq 1\\\\\n" *
        " & c1 \\geq -1\\\\\n" *
        " & b \\leq 1\\\\\n" *
        " & c \\leq 1\\\\\n" *
        " & b1 \\leq 1\\\\\n" *
        " & c1 \\leq 1\\\\\n" *
        " & a1 \\in \\mathbb{Z}\\\\\n" *
        " & b1 \\in \\mathbb{Z}\\\\\n" *
        " & c1 \\in \\mathbb{Z}\\\\\n" *
        " & z \\in \\mathbb{Z}\\\\\n" *
        " & x \\in \\{0, 1\\}\\\\\n" *
        " & u_{1} \\in \\{0, 1\\}\\\\\n" *
        " & u_{2} \\in \\{0, 1\\}\\\\\n" *
        " & u_{3} \\in \\{0, 1\\}\\\\\n" *
        "\\end{aligned}";
        repl = :print,
    )

    #------------------------------------------------------------------

    model_2 = ModelType()
    @variable(model_2, x, Bin)
    @variable(model_2, y, Int)
    @constraint(model_2, x * y <= 1)
    io_test(
        MIME("text/plain"),
        model_2,
        """
        A JuMP Model
        ├ solver: none
        ├ objective_sense: FEASIBILITY_SENSE
        ├ num_variables: 2
        ├ num_constraints: 3
        │ ├ $(GenericQuadExpr{Float64,VariableType}) in MOI.LessThan{Float64}: 1
        │ ├ $VariableType in MOI.Integer: 1
        │ └ $VariableType in MOI.ZeroOne: 1
        └ Names registered in the model
          └ :x, :y""";
        repl = :show,
    )

    model_3 = ModelType()
    @variable(model_3, x)
    @constraint(model_3, x <= 3)

    io_test(
        MIME("text/plain"),
        model_3,
        """
        A JuMP Model
        ├ solver: none
        ├ objective_sense: FEASIBILITY_SENSE
        ├ num_variables: 1
        ├ num_constraints: 1
        │ └ $(GenericAffExpr{Float64,VariableType}) in MOI.LessThan{Float64}: 1
        └ Names registered in the model
          └ :x""";
        repl = :show,
    )
    return
end

function test_printing_model_with_nonlinear()
    eq = JuMP._math_symbol(MIME("text/plain"), :eq)
    model = Model()
    @variable(model, x)
    @NLobjective(model, Max, sin(x))
    c = @NLexpression(model, cos(x))
    @NLconstraint(model, c == 0)

    io_test(
        MIME("text/plain"),
        model,
        """
        A JuMP Model
        ├ solver: none
        ├ objective_sense: MAX_SENSE
        │ └ objective_function_type: $AffExpr
        ├ num_variables: 1
        ├ num_constraints: 1
        │ └ Nonlinear: 1
        └ Names registered in the model
          └ :x""";
        repl = :show,
    )

    io_test(
        MIME("text/plain"),
        model,
        """
Max sin(x)
Subject to
 subexpression[1] - 0.0 $eq 0
With NL expressions
 subexpression[1]: cos(x)
""";
        repl = :print,
    )

    io_test(
        MIME("text/latex"),
        model,
        "\\begin{aligned}\n" *
        "\\max\\quad & sin(x)\\\\\n" *
        "\\text{Subject to} \\quad & subexpression_{1} - 0.0 = 0\\\\\n" *
        "\\text{With NL expressions} \\quad & subexpression_{1}: cos(x)\\\\\n" *
        "\\end{aligned}";
        repl = :print,
    )
    return
end

function test_SingleVariable_constraints()
    ge = JuMP._math_symbol(MIME("text/plain"), :geq)
    model = Model()
    @variable(model, x >= 10)
    zero_one = @constraint(model, x in MOI.ZeroOne())
    io_test(MIME("text/plain"), LowerBoundRef(x), "x $ge 10")
    io_test(MIME("text/plain"), zero_one, "x binary")
    # TODO: Test in IJulia mode
    return
end

function test_latex_feasibility()
    io_test(
        MIME("text/latex"),
        Model(),
        "\\begin{aligned}\n\\text{feasibility}\\\\\n\\end{aligned}";
        repl = :print,
    )
    return
end

function test_latex_min()
    model = Model()
    @variable(model, x)
    @objective(model, Min, x)
    io_test(
        MIME("text/latex"),
        model,
        "\\begin{aligned}\n\\min\\quad & x\\\\\n\\end{aligned}";
        repl = :print,
    )
    return
end

function test_print_IJulia_with_math_operators()
    model = Model()
    @variable(model, x_ab)
    io_test(MIME("text/latex"), x_ab, "x\\_ab")
    @variable(model, y[1:2], base_name = "y[:a]")
    io_test(MIME("text/latex"), y[1], "y[:a]_{1}")
    io_test(MIME("text/latex"), y[2], "y[:a]_{2}")
    @variable(model, z, base_name = "z^1")
    io_test(MIME("text/latex"), z, "z\\^1")
    return
end

struct _UnsupportedNameOptimizer <: MOI.AbstractOptimizer end
MOI.is_empty(::_UnsupportedNameOptimizer) = true

function test_Name_direct_mode()
    model = direct_model(_UnsupportedNameOptimizer())
    @test name(model) == "A JuMP Model"
    return
end

function test_print_summary_min_sense()
    model = Model()
    @variable(model, x)
    @objective(model, Min, x)
    @test occursin("objective_sense: MIN_SENSE", sprint(show, model))
end

function test_show_latex_parameter()
    model = Model()
    @variable(model, x)
    @NLparameter(model, p == 1)
    q = @NLparameter(model, value = 1)
    @test sprint(show, MIME("text/latex"), p) == "p == 1.0"
    @test sprint(show, MIME("text/latex"), q) == "parameter[2] == 1.0"
    return
end

function test_complex_expr()
    model = Model()
    @variable(model, x)
    @variable(model, y)
    f = 1.0im * x + 1.0im
    @test sprint(show, im * f) == "-x - 1"
    @test sprint(show, -f) == "-x im - im"
    @test sprint(show, f * x) == "x² im + x im"
    @test sprint(show, f * y) == "x*y im + y im"
    @test sprint(show, f + y) == "x im + y + im"
    @test sprint(show, 2f) == "2im x + 2im"
    return
end

function test_print_hermitian_psd_cone()
    model = Model()
    @variable(model, x[1:2])
    in_sym = JuMP._math_symbol(MIME("text/plain"), :in)
    H = Hermitian([x[1] 1im; -1im x[2]])
    c = @constraint(model, H in HermitianPSDCone())
    @test sprint(io -> show(io, MIME("text/plain"), c)) ==
          "[x[1]  im\n -im   x[2]] $in_sym $(HermitianPSDCone())"
    @test sprint(io -> show(io, MIME("text/latex"), c)) ==
          "\$\$ \\begin{bmatrix}\nx_{1} & i\\\\\n-i & x_{2}\\\\\n\\end{bmatrix} \\in \\text{$(HermitianPSDCone())} \$\$"
    return
end

function test_print_complex_string_round()
    for (test, result) in Any[
        1.0+0.0im=>"1",
        -1.0+0.0im=>"-1",
        1.0-0.0im=>"1",
        -1.0-0.0im=>"-1",
        0.0+1.0im=>"im",
        -0.0+1.0im=>"im",
        0.0-1.0im=>"-im",
        -0.0-1.0im=>"-im",
        1.0+2.0im=>"(1 + 2im)",
        1.0-2.0im=>"(1 - 2im)",
        -1.0+2.0im=>"(-1 + 2im)",
        -1.0-2.0im=>"(-1 - 2im)",
        1.0+1.0im=>"(1 + im)",
        1.0-1.0im=>"(1 - im)",
        -1.0+1.0im=>"(-1 + im)",
        -1.0-1.0im=>"(-1 - im)",
    ]
        @test JuMP._string_round(MIME("text/plain"), test) == result
        latex_result = replace(result, "im" => "i")
        @test JuMP._string_round(MIME("text/latex"), test) == latex_result
    end
    return
end

function test_print_huge_integer_string_round()
    for (test, result) in Any[
        -1+Float32(typemax(Int32))=>"2147483648",
        -1+Float32(typemin(Int32))=>"-2147483648",
        -1+Float64(typemax(Int32))=>"2147483646",
        -1+Float64(typemin(Int32))=>"-2147483649",
        Float32(typemax(Int32))=>"2147483648",
        Float32(typemin(Int32))=>"-2147483648",
        Float64(typemax(Int32))=>"2147483647",
        Float64(typemin(Int32))=>"-2147483648",
        1+Float32(typemax(Int32))=>"2147483648",
        1+Float32(typemin(Int32))=>"-2147483648",
        1+Float64(typemax(Int32))=>"2147483648",
        1+Float64(typemin(Int32))=>"-2147483647",
        2*Float32(typemax(Int32))=>"4294967296",
        2*Float32(typemin(Int32))=>"-4294967296",
        2*Float64(typemax(Int32))=>"4294967294",
        2*Float64(typemin(Int32))=>"-4294967296",
        -1+Float32(typemax(Int64))=>"9.223372e18",
        -1+Float32(typemin(Int64))=>"-9223372036854775808",
        -1+Float64(typemax(Int64))=>"9.223372036854776e18",
        -1+Float64(typemin(Int64))=>"-9223372036854775808",
        Float32(typemax(Int64))=>"9.223372e18",
        Float32(typemin(Int64))=>"-9223372036854775808",
        Float64(typemax(Int64))=>"9.223372036854776e18",
        Float64(typemin(Int64))=>"-9223372036854775808",
        1+Float32(typemax(Int64))=>"9.223372e18",
        1+Float32(typemin(Int64))=>"-9223372036854775808",
        1+Float64(typemax(Int64))=>"9.223372036854776e18",
        1+Float64(typemin(Int64))=>"-9223372036854775808",
        2*Float32(typemax(Int64))=>"1.8446744e19",
        2*Float32(typemin(Int64))=>"-1.8446744e19",
        2*Float64(typemax(Int64))=>"1.8446744073709552e19",
        2*Float64(typemin(Int64))=>"-1.8446744073709552e19",
    ]
        @test JuMP._string_round(MIME("text/plain"), test) == result
    end
    return
end

function test_print_model_with_huge_integers()
    model = Model()
    @variable(model, x)
    c = @constraint(model, 1e20 * x == 42)
    eq = JuMP._math_symbol(MIME("text/plain"), :eq)
    @test sprint(io -> show(io, MIME("text/plain"), c)) == "1.0e20 x $eq 42"
    eq = JuMP._math_symbol(MIME("text/latex"), :eq)
    @test sprint(io -> show(io, MIME("text/latex"), c)) ==
          "\$\$ 1.0 \\times 10^{20} x $eq 42 \$\$"
    return
end

function test_print_text_latex_interval_set()
    @test in_set_string(MIME("text/latex"), MOI.Interval(1, 2)) == "\\in [1, 2]"
    @test in_set_string(MIME("text/latex"), MOI.Interval(1.0, 2.0)) ==
          "\\in [1, 2]"
    @test in_set_string(MIME("text/latex"), MOI.Interval(1.5, 2.5)) ==
          "\\in [1.5, 2.5]"
    return
end

function test_truncated_printing()
    model = Model()
    @variable(model, x[1:1000])
    y = sum(x)
    s = function_string(MIME("text/plain"), y)
    @test occursin("x[30] + [[...940 terms omitted...]] + x[971]", s)
    @test occursin(
        "x_{30} + [[\\ldots\\text{940 terms omitted}\\ldots]] + x_{971}",
        function_string(MIME("text/latex"), y),
    )
    ret = JuMP._TERM_LIMIT_FOR_PRINTING[]
    JuMP._TERM_LIMIT_FOR_PRINTING[] = 3
    @test function_string(MIME("text/plain"), y) ==
          "x[1] + x[2] + [[...997 terms omitted...]] + x[1000]"
    @test function_string(MIME("text/latex"), y) ==
          "x_{1} + x_{2} + [[\\ldots\\text{997 terms omitted}\\ldots]] + x_{1000}"
    JuMP._TERM_LIMIT_FOR_PRINTING[] = ret
    return
end

function test_skipping_constraints()
    model = Model()
    @variable(model, 0 <= x[1:100] <= 10, Int)
    N = num_constraints(model; count_variable_in_set_constraints = true)
    @test N == 300
    str = sprint(print, model)
    for i in 1:100
        @test occursin(sprint(print, LowerBoundRef(x[i])), str) == (i <= 50)
    end
    @test occursin("[[...200 constraints skipped...]]", str)
    for i in 1:100
        @test occursin(sprint(print, IntegerRef(x[i])), str) == (i >= 51)
    end
    ret = JuMP._CONSTRAINT_LIMIT_FOR_PRINTING[]
    JuMP._CONSTRAINT_LIMIT_FOR_PRINTING[] = 3
    str = sprint(print, model)
    for i in 1:100
        @test occursin(sprint(print, LowerBoundRef(x[i])), str) == (i <= 2)
    end
    @test occursin("[[...297 constraints skipped...]]", str)
    for i in 1:100
        @test occursin(sprint(print, IntegerRef(x[i])), str) == (i >= 100)
    end
    JuMP._CONSTRAINT_LIMIT_FOR_PRINTING[] = ret
    return
end

function test_skipping_constraints_latex()
    model = Model()
    @variable(model, 0 <= x[1:100] <= 10, Int)
    N = num_constraints(model; count_variable_in_set_constraints = true)
    @test N == 300
    str = sprint(print, latex_formulation(model))
    for i in 1:100
        output = constraint_string(
            MIME("text/latex"),
            LowerBoundRef(x[i]);
            in_math_mode = true,
        )
        @test occursin(output, str) == (i <= 50)
    end
    @test occursin("[[\\ldots\\text{200 constraints skipped}\\ldots]]", str)
    for i in 1:100
        output = constraint_string(
            MIME("text/latex"),
            IntegerRef(x[i]);
            in_math_mode = true,
        )
        @test occursin(output, str) == (i >= 51)
    end
    ret = JuMP._CONSTRAINT_LIMIT_FOR_PRINTING[]
    JuMP._CONSTRAINT_LIMIT_FOR_PRINTING[] = 3
    str = sprint(print, latex_formulation(model))
    for i in 1:100
        output = constraint_string(
            MIME("text/latex"),
            LowerBoundRef(x[i]);
            in_math_mode = true,
        )
        @test occursin(output, str) == (i <= 2)
    end
    @test occursin("[[\\ldots\\text{297 constraints skipped}\\ldots]]", str)
    for i in 1:100
        output = constraint_string(
            MIME("text/latex"),
            IntegerRef(x[i]);
            in_math_mode = true,
        )
        @test occursin(output, str) == (i >= 100)
    end
    JuMP._CONSTRAINT_LIMIT_FOR_PRINTING[] = ret
    return
end

function test_print_omit_vector()
    n = 100
    model = Model()
    @variable(model, x[1:n, 1:n])
    @constraint(model, vec(x) >= 0)
    @test occursin("9970 terms omitted", sprint(print, model))
    return
end

function test_invalid_references()
    model = Model()
    @variable(model, x[1:2])
    @constraint(model, c[i in 1:2], x[i] <= i)
    delete(model, c[1])
    delete(model, x[1])
    mime = MIME("text/plain")
    @test sprint(show, mime, x[1]) == "InvalidVariableRef"
    @test occursin("InvalidVariableRef", sprint(show, mime, x))
    @test sprint(show, mime, c[1]) == "InvalidConstraintRef"
    @test occursin("InvalidConstraintRef", sprint(show, mime, c))
    mime = MIME("text/latex")
    @test sprint(show, mime, x[1]) == "\$ InvalidVariableRef \$"
    @test sprint(show, mime, c[1]) == "InvalidConstraintRef"
    return
end

function test_symmetric_constraint()
    model = Model()
    @variable(model, x[1:2, 1:2], Symmetric)
    @test function_string(MIME("text/plain"), x) ==
          "[x[1,1]  x[1,2]\n x[1,2]  x[2,2]]"
    @test function_string(MIME("text/latex"), x) ==
          "\\begin{bmatrix}\nx_{1,1} & x_{1,2}\\\\\n\\cdots & x_{2,2}\\\\\n\\end{bmatrix}"
    c = @constraint(model, x in PSDCone())
    o = constraint_object(c)
    @test function_string(MIME("text/plain"), o) ==
          "[x[1,1]  x[1,2]\n ⋯       x[2,2]]"
    @test function_string(MIME("text/latex"), o) ==
          "\\begin{bmatrix}\nx_{1,1} & x_{1,2}\\\\\n\\cdots & x_{2,2}\\\\\n\\end{bmatrix}"
    return
end

function test_show_generic_model_bigfloat()
    model = GenericModel{BigFloat}()
    ret = """
    A JuMP Model
    ├ value_type: BigFloat
    ├ solver: none
    ├ objective_sense: FEASIBILITY_SENSE
    ├ num_variables: 0
    ├ num_constraints: 0
    └ Names registered in the model: none"""
    @test sprint(show, model) == ret
    return
end

function test_small_number_latex()
    model = Model()
    @variable(model, x)
    y = 1e-8 * x
    @test function_string(MIME("text/latex"), y) == "1.0 \\times 10^{-8} x"
    @test function_string(MIME("text/plain"), y) == "1.0e-8 x"
    y = 0.23e-8 * x
    @test function_string(MIME("text/latex"), y) == "2.3 \\times 10^{-9} x"
    @test function_string(MIME("text/plain"), y) == "2.3e-9 x"
    y = 1.23 * x
    @test function_string(MIME("text/latex"), y) == "1.23 x"
    @test function_string(MIME("text/plain"), y) == "1.23 x"
    return
end

function test_print_summary()
    model = JuMPExtension.MyModel()
    @variable(model, x)
    @objective(model, Min, x)
    @test occursin("Minimization problem with:", sprint(show, model))
    return
end

function test_show_objective_summary()
    model = Model()
    @variable(model, x)
    @objective(model, Min, x)
    @test sprint(show_objective_function_summary, model) ==
          "Objective function type: JuMP.VariableRef\n"
    @NLobjective(model, Min, x)
    @test sprint(show_objective_function_summary, model) ==
          "Objective function type: Nonlinear\n"
    return
end

function test_show_constraints_summary()
    model = Model()
    @variable(model, x)
    @constraint(model, x >= 1)
    @NLconstraint(model, sin(x) == 1)
    @test sprint(show_constraints_summary, model) ==
          "`JuMP.AffExpr`-in-`MathOptInterface.GreaterThan{Float64}`: 1 constraint\nNonlinear: 1 constraint\n"
    return
end

function test_show_backend_summary()
    model = Model()
    @test sprint(show_backend_summary, model) ==
          "Model mode: AUTOMATIC\nCachingOptimizer state: NO_OPTIMIZER\nSolver name: No optimizer attached."
    return
end

function test_solver_name_not_implemented()
    model = direct_model(MOI.FileFormats.MPS.Model())
    @test occursin("Solver name: unknown", sprint(show_backend_summary, model))
    return
end

end  # TestPrint
