// Copyright 2010-2021 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.ortools.linearsolver;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.google.ortools.Loader;
import com.google.ortools.linearsolver.MPModelProto;
import com.google.ortools.linearsolver.MPModelRequest;
import com.google.ortools.linearsolver.MPSolutionResponse;
import com.google.ortools.linearsolver.MPSolverResponseStatus;
import com.google.ortools.linearsolver.MPVariableProto;
import com.google.ortools.linearsolver.PartialVariableAssignment;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/** Test the Linear Solver java interface. */
public final class LinearSolverTest {
  // Numerical tolerance for checking primal, dual, objective values
  // and other values.
  private static final double NUM_TOLERANCE = 1e-5;

  @BeforeEach
  public void setUp() {
    Loader.loadNativeLibraries();
  }

  private void runBasicCtor(MPSolver.OptimizationProblemType solverType) {
    if (!MPSolver.supportsProblemType(solverType)) {
      return;
    }
    final MPSolver solver = new MPSolver("testBasicCtor", solverType);
    assertNotNull(solver);
    solver.solve();
  }

  @Test
  public void testMPSolver_basicCtor() {
    runBasicCtor(MPSolver.OptimizationProblemType.GLOP_LINEAR_PROGRAMMING);
    runBasicCtor(MPSolver.OptimizationProblemType.GLPK_LINEAR_PROGRAMMING);
    runBasicCtor(MPSolver.OptimizationProblemType.GLPK_MIXED_INTEGER_PROGRAMMING);
    runBasicCtor(MPSolver.OptimizationProblemType.CLP_LINEAR_PROGRAMMING);
    runBasicCtor(MPSolver.OptimizationProblemType.CBC_MIXED_INTEGER_PROGRAMMING);
  }

  @Test
  public void testMPSolver_destructor() {
    final MPSolver solver =
        new MPSolver("testDestructor", MPSolver.OptimizationProblemType.GLOP_LINEAR_PROGRAMMING);
    assertNotNull(solver);
    solver.delete();
  }

  private void runLinearSolver(
      MPSolver.OptimizationProblemType problemType, boolean integerVariables) {
    if (!MPSolver.supportsProblemType(problemType)) {
      return;
    }
    final MPSolver solver = new MPSolver("Solver", problemType);
    assertNotNull(solver);

    final double infinity = MPSolver.infinity();
    final MPVariable x1 = solver.makeNumVar(0.0, infinity, "x1");
    final MPVariable x2 = solver.makeNumVar(0.0, infinity, "x2");
    final MPVariable x3 = solver.makeNumVar(0.0, infinity, "x3");
    if (integerVariables) {
      x1.setInteger(true);
      x2.setInteger(true);
      x3.setInteger(true);
    }
    assertEquals(3, solver.numVariables());

    final MPObjective objective = solver.objective();
    objective.setCoefficient(x1, 10);
    objective.setCoefficient(x2, 6);
    objective.setCoefficient(x3, 4);
    objective.setMaximization();
    assertEquals(6.0, objective.getCoefficient(x2), 1e-6);
    assertTrue(objective.maximization());
    assertFalse(objective.minimization());

    final MPConstraint c0 = solver.makeConstraint(-1000, 100.0);
    c0.setCoefficient(x1, 1);
    c0.setCoefficient(x2, 1);
    c0.setCoefficient(x3, 1);

    final MPConstraint c1 = solver.makeConstraint(-1000, 600.0);
    c1.setCoefficient(x1, 10);
    c1.setCoefficient(x2, 4);
    c1.setCoefficient(x3, 5);
    assertEquals(4.0, c1.getCoefficient(x2), 1e-6);

    final MPConstraint c2 = solver.makeConstraint(-1000, 300.0);
    c2.setCoefficient(x1, 2);
    c2.setCoefficient(x2, 2);
    c2.setCoefficient(x3, 6);

    assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve());
    if (integerVariables) {
      assertEquals(732.0, objective.value(), NUM_TOLERANCE);
      assertEquals(33.0, x1.solutionValue(), NUM_TOLERANCE);
      assertEquals(67.0, x2.solutionValue(), NUM_TOLERANCE);
      assertEquals(0.0, x3.solutionValue(), NUM_TOLERANCE);
    } else {
      assertEquals(733.333333, objective.value(), NUM_TOLERANCE);
      assertEquals(33.333333, x1.solutionValue(), NUM_TOLERANCE);
      assertEquals(66.666667, x2.solutionValue(), NUM_TOLERANCE);
      assertEquals(0, x3.solutionValue(), NUM_TOLERANCE);
    }
  }

  @Test
  public void testMPSolver_linearSolver() {
    runLinearSolver(MPSolver.OptimizationProblemType.GLOP_LINEAR_PROGRAMMING, false);
    runLinearSolver(MPSolver.OptimizationProblemType.CLP_LINEAR_PROGRAMMING, false);
    runLinearSolver(MPSolver.OptimizationProblemType.GLPK_LINEAR_PROGRAMMING, false);

    runLinearSolver(MPSolver.OptimizationProblemType.CBC_MIXED_INTEGER_PROGRAMMING, true);
    runLinearSolver(MPSolver.OptimizationProblemType.GLPK_MIXED_INTEGER_PROGRAMMING, true);
    runLinearSolver(MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING, true);
  }

  private void runFirstLinearExample(MPSolver.OptimizationProblemType problemType) {
    if (!MPSolver.supportsProblemType(problemType)) {
      return;
    }
    final MPSolver solver = new MPSolver("Solver", problemType);
    assertNotNull(solver);

    final MPVariable x1 = solver.makeNumVar(0.0, Double.POSITIVE_INFINITY, "x1");
    final MPVariable x2 = solver.makeNumVar(0.0, Double.POSITIVE_INFINITY, "x2");
    final MPVariable x3 = solver.makeNumVar(0.0, Double.POSITIVE_INFINITY, "x3");
    assertEquals(3, solver.numVariables());

    final double[] obj = {10.0, 6.0, 4.0};
    final MPObjective objective = solver.objective();
    objective.setCoefficient(x1, obj[0]);
    objective.setCoefficient(x2, obj[1]);
    objective.setCoefficient(x3, obj[2]);
    objective.setMaximization();

    final double rhs0 = 100.0;
    final MPConstraint c0 = solver.makeConstraint(-Double.POSITIVE_INFINITY, rhs0, "c0");
    final double[] coef0 = {1.0, 1.0, 1.0};
    c0.setCoefficient(x1, coef0[0]);
    c0.setCoefficient(x2, coef0[1]);
    c0.setCoefficient(x3, coef0[2]);
    final double rhs1 = 600.0;
    final MPConstraint c1 = solver.makeConstraint(-Double.POSITIVE_INFINITY, rhs1, "c1");
    final double[] coef1 = {10.0, 4.0, 5.0};
    c1.setCoefficient(x1, coef1[0]);
    c1.setCoefficient(x2, coef1[1]);
    c1.setCoefficient(x3, coef1[2]);
    final double rhs2 = 300.0;
    final MPConstraint c2 = solver.makeConstraint(-Double.POSITIVE_INFINITY, rhs2);
    final double[] coef2 = {2.0, 2.0, 6.0};
    c2.setCoefficient(x1, coef2[0]);
    c2.setCoefficient(x2, coef2[1]);
    c2.setCoefficient(x3, coef2[2]);
    assertEquals(3, solver.numConstraints());
    assertEquals("c0", c0.name());
    assertEquals("c1", c1.name());
    assertEquals("auto_c_000000002", c2.name());

    // The problem has an optimal solution.;
    assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve());

    assertEquals(733.333333, objective.value(), NUM_TOLERANCE);
    assertEquals(33.333333, x1.solutionValue(), NUM_TOLERANCE);
    assertEquals(66.666667, x2.solutionValue(), NUM_TOLERANCE);
    assertEquals(0, x3.solutionValue(), NUM_TOLERANCE);

    // c0 and c1 are binding;
    final double[] activities = solver.computeConstraintActivities();
    assertEquals(3, activities.length);
    assertEquals(3.333333, c0.dualValue(), NUM_TOLERANCE);
    assertEquals(0.666667, c1.dualValue(), NUM_TOLERANCE);
    assertEquals(rhs0, activities[c0.index()], NUM_TOLERANCE);
    assertEquals(rhs1, activities[c1.index()], NUM_TOLERANCE);
    assertEquals(MPSolver.BasisStatus.AT_UPPER_BOUND, c0.basisStatus());
    assertEquals(MPSolver.BasisStatus.AT_UPPER_BOUND, c1.basisStatus());
    // c2 is not binding;
    assertEquals(0.0, c2.dualValue(), NUM_TOLERANCE);
    assertEquals(200.0, activities[c2.index()], NUM_TOLERANCE);
    assertEquals(MPSolver.BasisStatus.BASIC, c2.basisStatus());
    // The optimum of the dual problem is equal to the optimum of the;
    // primal problem.;
    final double dualObjectiveValue = c0.dualValue() * rhs0 + c1.dualValue() * rhs1;
    assertEquals(objective.value(), dualObjectiveValue, NUM_TOLERANCE);

    // x1 and x2 are basic;
    assertEquals(0.0, x1.reducedCost(), NUM_TOLERANCE);
    assertEquals(0.0, x2.reducedCost(), NUM_TOLERANCE);
    assertEquals(MPSolver.BasisStatus.BASIC, x1.basisStatus());
    assertEquals(MPSolver.BasisStatus.BASIC, x2.basisStatus());
    // x3 is non-basic;
    final double x3ExpectedReducedCost =
        (obj[2] - coef0[2] * c0.dualValue() - coef1[2] * c1.dualValue());
    assertEquals(x3ExpectedReducedCost, x3.reducedCost(), NUM_TOLERANCE);
    assertEquals(MPSolver.BasisStatus.AT_LOWER_BOUND, x3.basisStatus());

    if (solver.problemType() == MPSolver.OptimizationProblemType.GLPK_LINEAR_PROGRAMMING) {
      assertEquals(56.333333, solver.computeExactConditionNumber(), NUM_TOLERANCE);
    }
  }

  @Test
  public void testMPSolver_firstLinearExample() {
    runFirstLinearExample(MPSolver.OptimizationProblemType.GLOP_LINEAR_PROGRAMMING);
    runFirstLinearExample(MPSolver.OptimizationProblemType.CLP_LINEAR_PROGRAMMING);
    runFirstLinearExample(MPSolver.OptimizationProblemType.GLPK_LINEAR_PROGRAMMING);
    runFirstLinearExample(MPSolver.OptimizationProblemType.GUROBI_LINEAR_PROGRAMMING);
  }

  private void runFirstMIPExample(MPSolver.OptimizationProblemType problemType) {
    if (!MPSolver.supportsProblemType(problemType)) {
      return;
    }
    final MPSolver solver = new MPSolver("Solver", problemType);
    assertNotNull(solver);

    // Integer variables shouldn't have infinite bounds, nor really large bounds:
    // it can make your solver behave erratically. If you have integer variables
    // with a truly large dynamic range you should consider making it non-integer.
    final double upperBound = 1000;
    final MPVariable x1 = solver.makeIntVar(0.0, upperBound, "x1");
    final MPVariable x2 = solver.makeIntVar(0.0, upperBound, "x2");

    solver.objective().setCoefficient(x1, 1);
    solver.objective().setCoefficient(x2, 2);

    final MPConstraint ct = solver.makeConstraint(17, Double.POSITIVE_INFINITY);
    ct.setCoefficient(x1, 3);
    ct.setCoefficient(x2, 2);

    // Check the solution.
    assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve());
    final double optObjValue = 6.0;
    assertEquals(optObjValue, solver.objective().value(), 1e-6);
    assertEquals(optObjValue, solver.objective().bestBound(), 1e-6);
    final double optRowActivity = 18.0;
    assertEquals(optRowActivity, solver.computeConstraintActivities()[ct.index()], NUM_TOLERANCE);
    // BOP does not support nodes().
    if (solver.problemType() != MPSolver.OptimizationProblemType.BOP_INTEGER_PROGRAMMING) {
      assertThat(solver.nodes()).isAtLeast(0);
    }
  }

  @Test
  public void testMPSolver_firstMIPExample() {
    runFirstMIPExample(MPSolver.OptimizationProblemType.BOP_INTEGER_PROGRAMMING);
    runFirstMIPExample(MPSolver.OptimizationProblemType.CBC_MIXED_INTEGER_PROGRAMMING);
    runFirstMIPExample(MPSolver.OptimizationProblemType.GLPK_MIXED_INTEGER_PROGRAMMING);
    runFirstMIPExample(MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING);
    runFirstMIPExample(MPSolver.OptimizationProblemType.SAT_INTEGER_PROGRAMMING);
    runFirstMIPExample(MPSolver.OptimizationProblemType.GUROBI_MIXED_INTEGER_PROGRAMMING);
  }

  private void runSuccessiveObjectives(MPSolver.OptimizationProblemType problemType) {
    if (!MPSolver.supportsProblemType(problemType)) {
      return;
    }
    final MPSolver solver = new MPSolver("Solver", problemType);
    assertNotNull(solver);

    final MPVariable x1 = solver.makeNumVar(0, 10, "var1");
    final MPVariable x2 = solver.makeNumVar(0, 10, "var2");
    final MPConstraint ct = solver.makeConstraint(0, 10);
    ct.setCoefficient(x1, 1);
    ct.setCoefficient(x2, 2);

    final MPObjective objective = solver.objective();
    objective.setCoefficient(x1, 1);
    objective.setCoefficient(x2, 0);
    objective.setOptimizationDirection(true);

    // Check the solution.
    assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve());
    assertEquals(10.0, x1.solutionValue(), NUM_TOLERANCE);
    assertEquals(0.0, x2.solutionValue(), NUM_TOLERANCE);

    objective.setCoefficient(x1, 0);
    objective.setCoefficient(x2, 1);
    objective.setOptimizationDirection(true);

    // Check the solution
    assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve());
    assertEquals(0.0, x1.solutionValue(), NUM_TOLERANCE);
    assertEquals(5.0, x2.solutionValue(), NUM_TOLERANCE);

    objective.setCoefficient(x1, -1);
    objective.setCoefficient(x2, 0);
    objective.setOptimizationDirection(false);

    // Check the solution.
    assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve());
    assertEquals(10.0, x1.solutionValue(), NUM_TOLERANCE);
    assertEquals(0.0, x2.solutionValue(), NUM_TOLERANCE);
  }

  @Test
  public void testMPSolver_successiveObjectives() {
    runSuccessiveObjectives(MPSolver.OptimizationProblemType.GLOP_LINEAR_PROGRAMMING);
    runSuccessiveObjectives(MPSolver.OptimizationProblemType.CLP_LINEAR_PROGRAMMING);
    runSuccessiveObjectives(MPSolver.OptimizationProblemType.GLPK_LINEAR_PROGRAMMING);
    runSuccessiveObjectives(MPSolver.OptimizationProblemType.GUROBI_LINEAR_PROGRAMMING);

    runSuccessiveObjectives(MPSolver.OptimizationProblemType.CBC_MIXED_INTEGER_PROGRAMMING);
    runSuccessiveObjectives(MPSolver.OptimizationProblemType.GLPK_MIXED_INTEGER_PROGRAMMING);
    runSuccessiveObjectives(MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING);
    runSuccessiveObjectives(MPSolver.OptimizationProblemType.SAT_INTEGER_PROGRAMMING);
    runSuccessiveObjectives(MPSolver.OptimizationProblemType.GUROBI_MIXED_INTEGER_PROGRAMMING);
  }

  private void runObjectiveOffset(MPSolver.OptimizationProblemType problemType) {
    if (!MPSolver.supportsProblemType(problemType)) {
      return;
    }
    final MPSolver solver = new MPSolver("Solver", problemType);
    assertNotNull(solver);

    final MPVariable x1 = solver.makeNumVar(1.0, 10.0, "x1");
    final MPVariable x2 = solver.makeNumVar(1.0, 10.0, "x2");

    final MPConstraint ct = solver.makeConstraint(0, 4.0);
    ct.setCoefficient(x1, 1);
    ct.setCoefficient(x2, 2);

    final double objectiveOffset = 10.0;
    // Simple minimization.
    final MPObjective objective = solver.objective();
    objective.setCoefficient(x1, 1.0);
    objective.setCoefficient(x2, 1.0);
    objective.setOffset(objectiveOffset);
    objective.setOptimizationDirection(false);

    assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve());
    assertEquals(2.0 + objectiveOffset, objective.value(), 1e-6);

    // Offset is provided in several separate constants.
    objective.setCoefficient(x1, 1.0);
    objective.setCoefficient(x2, 1.0);
    objective.setOffset(-1.0);
    objective.setOffset(objectiveOffset + objective.offset());
    objective.setOffset(1.0 + objective.offset());
    assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve());
    assertEquals(2.0 + objectiveOffset, objective.value(), 1e-6);

    // Simple maximization.
    objective.setCoefficient(x1, 1.0);
    objective.setCoefficient(x2, 1.0);
    objective.setOffset(objectiveOffset);
    objective.setOptimizationDirection(true);
    assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve());
    assertEquals(3.0 + objectiveOffset, objective.value(), 1e-6);
  }

  @Test
  public void testMPSolver_objectiveOffset() {
    runObjectiveOffset(MPSolver.OptimizationProblemType.GLOP_LINEAR_PROGRAMMING);
    runObjectiveOffset(MPSolver.OptimizationProblemType.CLP_LINEAR_PROGRAMMING);
    runObjectiveOffset(MPSolver.OptimizationProblemType.GLPK_LINEAR_PROGRAMMING);
    runObjectiveOffset(MPSolver.OptimizationProblemType.GUROBI_LINEAR_PROGRAMMING);

    runObjectiveOffset(MPSolver.OptimizationProblemType.CBC_MIXED_INTEGER_PROGRAMMING);
    runObjectiveOffset(MPSolver.OptimizationProblemType.GLPK_MIXED_INTEGER_PROGRAMMING);
    runObjectiveOffset(MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING);
    runObjectiveOffset(MPSolver.OptimizationProblemType.SAT_INTEGER_PROGRAMMING);
    runObjectiveOffset(MPSolver.OptimizationProblemType.GUROBI_MIXED_INTEGER_PROGRAMMING);
  }

  @Test
  public void testMPSolver_lazyConstraints() {
    final MPSolver.OptimizationProblemType problemType =
        MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING;
    if (!MPSolver.supportsProblemType(problemType)) {
      return;
    }
    final MPSolver solver = new MPSolver("testLazyConstraints", problemType);
    assertNotNull(solver);

    final double infinity = MPSolver.infinity();
    final MPVariable x = solver.makeIntVar(0, infinity, "x");
    final MPVariable y = solver.makeIntVar(0, infinity, "y");
    final MPConstraint ct1 = solver.makeConstraint(0, 10.0);
    ct1.setCoefficient(x, 2.0);
    ct1.setCoefficient(y, 1.0);
    final MPConstraint ct2 = solver.makeConstraint(0, 10.0);
    ct2.setCoefficient(x, 1.0);
    ct2.setCoefficient(y, 2.0);
    ct2.setIsLazy(true);
    assertFalse(ct1.isLazy());
    assertTrue(ct2.isLazy());
    final MPObjective objective = solver.objective();
    objective.setCoefficient(x, 1.0);
    objective.setCoefficient(y, 1.0);
    objective.setOptimizationDirection(true);
    assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve());
    assertEquals(solver.objective().value(), 6.0, NUM_TOLERANCE);
  }

  @Test
  public void testMPSolver_sameConstraintName() {
    MPSolver solver = MPSolver.createSolver("GLOP");
    assertNotNull(solver);
    boolean success = true;
    solver.makeConstraint("my_const_name");
    try {
      solver.makeConstraint("my_const_name");
    } catch (Throwable e) {
      System.out.println(e);
      success = false;
    }
    assertTrue(success);
  }

  @Test
  public void testMPSolver_exportModelToProto() {
    final MPSolver.OptimizationProblemType problemType =
        MPSolver.OptimizationProblemType.GLOP_LINEAR_PROGRAMMING;
    if (!MPSolver.supportsProblemType(problemType)) {
      return;
    }
    final MPSolver solver = new MPSolver("testExportModelToProto", problemType);
    assertNotNull(solver);
    solver.makeNumVar(0.0, 10.0, "x1");
    solver.makeConstraint(0.0, 0.0);
    solver.objective().setOptimizationDirection(true);
    final MPModelProto model = solver.exportModelToProto();
    assertEquals(1, model.getVariableCount());
    assertEquals(1, model.getConstraintCount());
    assertTrue(model.getMaximize());
  }

  @Test
  public void testMPsolver_createSolutionResponseProto() {
    final MPSolver.OptimizationProblemType problemType =
        MPSolver.OptimizationProblemType.GLOP_LINEAR_PROGRAMMING;
    if (!MPSolver.supportsProblemType(problemType)) {
      return;
    }
    final MPSolver solver = new MPSolver("testCreateSolutionResponseProto", problemType);
    assertNotNull(solver);
    final MPVariable x1 = solver.makeNumVar(0.0, 10.0, "x1");
    solver.objective().setCoefficient(x1, 1.0);
    solver.objective().setOptimizationDirection(true);
    solver.solve();
    final MPSolutionResponse response = solver.createSolutionResponseProto();
    assertEquals(MPSolverResponseStatus.MPSOLVER_OPTIMAL, response.getStatus());
    assertEquals(10.0, response.getObjectiveValue(), 1e-6);
  }

  @Test
  public void testMPSolver_solveWithProto() {
    final MPSolver.OptimizationProblemType problemType =
        MPSolver.OptimizationProblemType.GLOP_LINEAR_PROGRAMMING;
    if (!MPSolver.supportsProblemType(problemType)) {
      return;
    }
    final MPModelProto.Builder modelBuilder = MPModelProto.newBuilder().setMaximize(true);
    final MPVariableProto variable = MPVariableProto.newBuilder()
                                         .setLowerBound(0.0)
                                         .setUpperBound(10.0)
                                         .setName("x1")
                                         .setIsInteger(false)
                                         .setObjectiveCoefficient(1.0)
                                         .build();
    modelBuilder.addVariable(variable);
    final MPModelRequest request =
        MPModelRequest.newBuilder()
            .setModel(modelBuilder.build())
            .setSolverType(MPModelRequest.SolverType.GLOP_LINEAR_PROGRAMMING)
            .build();
    final MPSolutionResponse response = MPSolver.solveWithProto(request);
    assertEquals(MPSolverResponseStatus.MPSOLVER_OPTIMAL, response.getStatus());
    assertEquals(10.0, response.getObjectiveValue(), 1e-6);
  }

  @Test
  public void testModelExport() {
    final MPSolver.OptimizationProblemType problemType =
        MPSolver.OptimizationProblemType.GLOP_LINEAR_PROGRAMMING;
    if (!MPSolver.supportsProblemType(problemType)) {
      return;
    }
    final MPSolver solver = new MPSolver("tesModelExport", problemType);
    assertNotNull(solver);
    final double infinity = MPSolver.infinity();
    // x1, x2 and x3 are continuous non-negative variables.
    final MPVariable x1 = solver.makeNumVar(0.0, infinity, "x1");

    // Maximize 10 * x1.
    solver.objective().setCoefficient(x1, 10);
    solver.objective().setMinimization();

    // 5 * x1 <= 30.
    final MPConstraint c0 = solver.makeConstraint(-infinity, 100.0);
    c0.setCoefficient(x1, 5);

    final MPModelExportOptions obfuscate = new MPModelExportOptions();
    obfuscate.setObfuscate(true);
    String out = solver.exportModelAsLpFormat();
    assertThat(out).isNotEmpty();
    out = solver.exportModelAsLpFormat(obfuscate);
    assertThat(out).isNotEmpty();
    out = solver.exportModelAsMpsFormat();
    assertThat(out).isNotEmpty();
    out = solver.exportModelAsMpsFormat(obfuscate);
    assertThat(out).isNotEmpty();
  }

  @Test
  public void testMPSolver_wrongModelExport() {
    final MPSolver.OptimizationProblemType problemType =
        MPSolver.OptimizationProblemType.GLOP_LINEAR_PROGRAMMING;
    if (!MPSolver.supportsProblemType(problemType)) {
      return;
    }
    final MPSolver solver = new MPSolver("testWrongModelExport", problemType);
    assertNotNull(solver);
    // Test that forbidden names are renamed.
    solver.makeBoolVar("<-%$#!&~-+ ⌂"); // Some illegal name.
    String out = solver.exportModelAsLpFormat();
    assertThat(out).isNotEmpty();
    out = solver.exportModelAsMpsFormat();
    assertThat(out).isNotEmpty();
  }

  @Test
  public void testMPSolver_setHint() {
    final MPSolver.OptimizationProblemType problemType =
        MPSolver.OptimizationProblemType.GLOP_LINEAR_PROGRAMMING;
    if (!MPSolver.supportsProblemType(problemType)) {
      return;
    }
    final MPSolver solver = new MPSolver("testSetHint", problemType);
    assertNotNull(solver);
    final MPVariable[] variables = {
        solver.makeNumVar(0.0, 10.0, "x1"), solver.makeNumVar(0.0, 10.0, "x2")};
    final double[] values = {5.0, 6.0};
    solver.setHint(variables, values);

    final MPModelProto model = solver.exportModelToProto();
    final PartialVariableAssignment hint = model.getSolutionHint();
    assertEquals(2, hint.getVarIndexCount());
    assertEquals(2, hint.getVarValueCount());
    assertEquals(0, hint.getVarIndex(0));
    assertEquals(5.0, hint.getVarValue(0), 1e-6);
    assertEquals(1, hint.getVarIndex(1));
    assertEquals(6.0, hint.getVarValue(1), 1e-6);
  }

  @Test
  public void testMPSolver_issue132() {
    final MPSolver.OptimizationProblemType problemType =
        MPSolver.OptimizationProblemType.CLP_LINEAR_PROGRAMMING;
    if (!MPSolver.supportsProblemType(problemType)) {
      return;
    }
    final MPSolver solver = new MPSolver("CoinError", problemType);
    assertNotNull(solver);
    final double infinity = MPSolver.infinity();
    final MPVariable x0 = solver.makeNumVar(0.0, 1.0, "x0");
    final MPVariable x1 = solver.makeNumVar(0.0, 0.3, "x1");
    final MPVariable x2 = solver.makeNumVar(0.0, 0.3, "x2");
    final MPVariable x3 = solver.makeNumVar(-infinity, infinity, "x3");

    final MPObjective obj = solver.objective();
    obj.setCoefficient(x1, 2.655523);
    obj.setCoefficient(x2, -2.70917);
    obj.setCoefficient(x3, 1);
    obj.setMaximization();

    final MPConstraint c0 = solver.makeConstraint(-infinity, 0.302499);
    c0.setCoefficient(x3, 1);
    c0.setCoefficient(x0, -3.484345);

    final MPConstraint c1 = solver.makeConstraint(-infinity, 0.507194);
    c1.setCoefficient(x3, 1);
    c1.setCoefficient(x0, -3.074807);

    final MPConstraint c2 = solver.makeConstraint(0.594, 0.594);
    c2.setCoefficient(x0, 1);
    c2.setCoefficient(x1, 1.01);
    c2.setCoefficient(x2, -0.99);

    System.out.println("Number of variables = " + solver.numVariables());
    System.out.println("Number of constraints = " + solver.numConstraints());

    solver.enableOutput();
    System.out.println(solver.exportModelAsLpFormat());
    System.out.println(solver.solve());
  }

  @Test
  public void testMPSolver_setHintAndSolverGetters() {
    final MPSolver.OptimizationProblemType problemType =
        MPSolver.OptimizationProblemType.GLOP_LINEAR_PROGRAMMING;
    if (!MPSolver.supportsProblemType(problemType)) {
      return;
    }
    final MPSolver solver = new MPSolver("glop", problemType);
    assertNotNull(solver);

    // x and y are continuous non-negative variables.
    final MPVariable x = solver.makeIntVar(0.0, Double.POSITIVE_INFINITY, "x");
    final MPVariable y = solver.makeIntVar(0.0, Double.POSITIVE_INFINITY, "y");

    // Objectif function: Maximize x + 10 * y.
    final MPObjective objective = solver.objective();
    objective.setCoefficient(x, 1);
    objective.setCoefficient(y, 10);
    objective.setMaximization();

    // x + 7 * y <= 17.5.
    final MPConstraint c0 = solver.makeConstraint(-Double.POSITIVE_INFINITY, 17.5, "c0");
    c0.setCoefficient(x, 1);
    c0.setCoefficient(y, 7);

    // x <= 3.5.
    final MPConstraint c1 = solver.makeConstraint(-Double.POSITIVE_INFINITY, 3.5, "c1");
    c1.setCoefficient(x, 1);
    c1.setCoefficient(y, 0);

    // Test solver getters.
    final MPVariable[] variables = solver.variables();
    assertThat(variables).hasLength(2);
    final MPConstraint[] constraints = solver.constraints();
    assertThat(constraints).hasLength(2);

    // Test API compiles.
    solver.setHint(variables, new double[] {2.0, 3.0});
    assertEquals("y", variables[1].name());
    assertEquals("c0", constraints[0].name());
    // TODO(user): Add API to query the hint.

    assertFalse(solver.setNumThreads(4));
  }
}
