// Copyright 2010-2022 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.constraintsolver;

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.auto.value.AutoValue;
import com.google.ortools.Loader;
import com.google.ortools.constraintsolver.RoutingModelParameters;
import com.google.ortools.constraintsolver.RoutingSearchParameters;
import com.google.protobuf.Duration;
import java.util.ArrayList;
import java.util.function.LongBinaryOperator;
import java.util.function.LongUnaryOperator;
import java.util.stream.IntStream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/** Tests the Routing java interface. */
public final class RoutingSolverTest {
  @AutoValue
  abstract static class Location {
    static Location create(Integer latitude, Integer longitude) {
      return new AutoValue_RoutingSolverTest_Location(latitude, longitude);
    }
    abstract Integer latitude();
    abstract Integer longitude();
  }
  private ArrayList<Location> coordinates;

  @BeforeEach
  public void setUp() {
    Loader.loadNativeLibraries();
    coordinates = new ArrayList<>();
    coordinates.add(Location.create(0, 0));
    coordinates.add(Location.create(-1, 0));
    coordinates.add(Location.create(-1, 2));
    coordinates.add(Location.create(2, 1));
    coordinates.add(Location.create(1, 0));
  }

  public LongBinaryOperator createManhattanCostCallback(RoutingIndexManager manager) {
    return (long i, long j) -> {
      final int firstIndex = manager.indexToNode(i);
      final int secondIndex = manager.indexToNode(j);
      final Location firstCoordinate = coordinates.get(firstIndex);
      final Location secondCoordinate = coordinates.get(secondIndex);
      return (long) Math.abs(firstCoordinate.latitude() - secondCoordinate.latitude())
          + Math.abs(firstCoordinate.longitude() - secondCoordinate.longitude());
    };
  }

  public LongUnaryOperator createUnaryCostCallback(RoutingIndexManager manager) {
    return (long fromIndex) -> {
      final int fromNode = manager.indexToNode(fromIndex);
      final Location firstCoordinate = coordinates.get(fromNode);
      return (long) Math.abs(firstCoordinate.latitude()) + Math.abs(firstCoordinate.longitude());
    };
  }

  public LongBinaryOperator createReturnOneCallback() {
    return (long i, long j) -> 1;
  }

  @Test
  public void testRoutingModel_checkGlobalRefGuard() {
    for (int i = 0; i < 500; ++i) {
      final RoutingIndexManager manager = new RoutingIndexManager(coordinates.size(), 1, 0);
      assertNotNull(manager);
      final RoutingModel model = new RoutingModel(manager);
      assertNotNull(model);
      LongBinaryOperator transit = (long fromIndex, long toIndex) -> {
        final int fromNode = manager.indexToNode(fromIndex);
        final int toNode = manager.indexToNode(toIndex);
        return (long) Math.abs(toNode - fromNode);
      };
      model.registerTransitCallback(transit);
      System.gc(); // model should keep alive the callback
    }
  }

  @Test
  public void testRoutingIndexManager() {
    final RoutingIndexManager manager = new RoutingIndexManager(42, 3, 7);
    assertNotNull(manager);
    assertEquals(42, manager.getNumberOfNodes());
    assertEquals(3, manager.getNumberOfVehicles());
    assertEquals(42 + 3 * 2 - 1, manager.getNumberOfIndices());
    for (int i = 0; i < manager.getNumberOfVehicles(); ++i) {
      assertEquals(7, manager.indexToNode(manager.getStartIndex(i)));
      assertEquals(7, manager.indexToNode(manager.getEndIndex(i)));
    }
  }

  @Test
  public void testRoutingIndexManager_multiDepotSame() {
    final RoutingIndexManager manager =
        new RoutingIndexManager(42, 3, new int[] {7, 7, 7}, new int[] {7, 7, 7});
    assertNotNull(manager);
    assertEquals(42, manager.getNumberOfNodes());
    assertEquals(3, manager.getNumberOfVehicles());
    assertEquals(42 + 3 * 2 - 1, manager.getNumberOfIndices());
    for (int i = 0; i < manager.getNumberOfVehicles(); ++i) {
      assertEquals(7, manager.indexToNode(manager.getStartIndex(i)));
      assertEquals(7, manager.indexToNode(manager.getEndIndex(i)));
    }
  }

  @Test
  public void testRoutingIndexManager_multiDepotAllDifferent() {
    final RoutingIndexManager manager =
        new RoutingIndexManager(42, 3, new int[] {1, 2, 3}, new int[] {4, 5, 6});
    assertNotNull(manager);
    assertEquals(42, manager.getNumberOfNodes());
    assertEquals(3, manager.getNumberOfVehicles());
    assertEquals(42, manager.getNumberOfIndices());
    for (int i = 0; i < manager.getNumberOfVehicles(); ++i) {
      assertEquals(i + 1, manager.indexToNode(manager.getStartIndex(i)));
      assertEquals(i + 4, manager.indexToNode(manager.getEndIndex(i)));
    }
  }

  @Test
  public void testRoutingModel() {
    final RoutingIndexManager manager =
        new RoutingIndexManager(42, 3, new int[] {1, 2, 3}, new int[] {4, 5, 6});
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    for (int i = 0; i < manager.getNumberOfVehicles(); ++i) {
      assertEquals(i + 1, manager.indexToNode(model.start(i)));
      assertEquals(i + 4, manager.indexToNode(model.end(i)));
    }
  }

  @Test
  public void testRoutingModelParameters() {
    final RoutingModelParameters parameters = main.defaultRoutingModelParameters();
    final RoutingIndexManager manager = new RoutingIndexManager(coordinates.size(), 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager, parameters);
    assertNotNull(model);
    assertEquals(1, model.vehicles());
  }

  @Test
  public void testRoutingModel_solveWithParameters() {
    final RoutingIndexManager manager = new RoutingIndexManager(coordinates.size(), 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    final RoutingSearchParameters parameters = main.defaultRoutingSearchParameters();
    final LongBinaryOperator callback = createManhattanCostCallback(manager);
    final int cost = model.registerTransitCallback(callback);
    System.gc();
    model.setArcCostEvaluatorOfAllVehicles(cost);

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    Assignment solution = model.solveWithParameters(parameters);
    assertEquals(10, solution.objectiveValue());
    solution = model.solveFromAssignmentWithParameters(solution, parameters);
    assertEquals(10, solution.objectiveValue());
  }

  @Test
  public void testRoutingModel_costsAndSolve() {
    final RoutingIndexManager manager = new RoutingIndexManager(coordinates.size(), 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    assertEquals(5, model.nodes());
    final LongBinaryOperator callback = createManhattanCostCallback(manager);
    final int cost = model.registerTransitCallback(callback);
    System.gc();
    model.setArcCostEvaluatorOfAllVehicles(cost);

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    final Assignment solution = model.solve(null);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    assertEquals(10, solution.objectiveValue());
  }

  @Test
  public void testRoutingModel_matrixTransitOwnership() {
    final RoutingIndexManager manager = new RoutingIndexManager(coordinates.size(), 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    assertEquals(5, model.nodes());
    final long[][] matrix = {
        {0, 1, 3, 3, 1},
        {1, 0, 2, 4, 2},
        {3, 2, 0, 4, 4},
        {3, 4, 4, 0, 2},
        {1, 2, 4, 2, 0},
    };
    final int cost = model.registerTransitMatrix(matrix);
    System.gc(); // model should keep alive the callback
    model.setArcCostEvaluatorOfAllVehicles(cost);

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    final Assignment solution = model.solve(null);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    assertEquals(10, solution.objectiveValue());
  }

  @Test
  public void testRoutingModel_transitCallbackOwnership() {
    final RoutingIndexManager manager = new RoutingIndexManager(coordinates.size(), 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    assertEquals(5, model.nodes());
    final int cost = model.registerTransitCallback(createManhattanCostCallback(manager));
    System.gc(); // model should keep alive the callback
    model.setArcCostEvaluatorOfAllVehicles(cost);

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    final Assignment solution = model.solve(null);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    assertEquals(10, solution.objectiveValue());
  }

  @Test
  public void testRoutingModel_lambdaTransitCallbackOwnership() {
    final RoutingIndexManager manager = new RoutingIndexManager(coordinates.size(), 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    assertEquals(5, model.nodes());
    final int cost = model.registerTransitCallback((long fromIndex, long toIndex) -> {
      final int fromNode = manager.indexToNode(fromIndex);
      final int toNode = manager.indexToNode(toIndex);
      return (long) Math.abs(toNode - fromNode);
    });
    System.gc(); // model should keep alive the callback
    model.setArcCostEvaluatorOfAllVehicles(cost);

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    final Assignment solution = model.solve(null);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    assertEquals(8, solution.objectiveValue());
  }

  @Test
  public void testRoutingModel_unaryTransitVectorOwnership() {
    final RoutingIndexManager manager = new RoutingIndexManager(10, 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    assertEquals(10, model.nodes());
    final long[] vector = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    final int cost = model.registerUnaryTransitVector(vector);
    System.gc(); // model should keep alive the callback
    model.setArcCostEvaluatorOfAllVehicles(cost);

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    final Assignment solution = model.solve(null);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    assertEquals(45, solution.objectiveValue());
  }

  @Test
  public void testRoutingModel_unaryTransitCallbackOwnership() {
    final RoutingIndexManager manager = new RoutingIndexManager(coordinates.size(), 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    assertEquals(5, model.nodes());
    final int cost = model.registerUnaryTransitCallback(createUnaryCostCallback(manager));
    System.gc(); // model should keep alive the callback
    model.setArcCostEvaluatorOfAllVehicles(cost);

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    Assignment solution = model.solve(null);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    assertEquals(8, solution.objectiveValue());
  }

  @Test
  public void testRoutingModel_lambdaUnaryTransitCallbackOwnership() {
    final RoutingIndexManager manager = new RoutingIndexManager(coordinates.size(), 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    assertEquals(5, model.nodes());
    final int cost = model.registerUnaryTransitCallback((long fromIndex) -> {
      final int fromNode = manager.indexToNode(fromIndex);
      return (long) Math.abs(fromNode);
    });
    System.gc(); // model should keep alive the callback
    model.setArcCostEvaluatorOfAllVehicles(cost);

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    Assignment solution = model.solve(null);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    assertEquals(10, solution.objectiveValue());
  }

  @Test
  public void testRoutingModel_routesToAssignment() {
    final int vehicles = coordinates.size() - 1;
    final RoutingIndexManager manager = new RoutingIndexManager(coordinates.size(), vehicles, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    model.closeModel();
    long[][] routes = new long[vehicles][];
    for (int i = 0; i < vehicles; ++i) {
      // Each route has a single node
      routes[i] = new long[1];
      routes[i][0] = manager.nodeToIndex(i + 1);
    }
    Assignment assignment = new Assignment(model.solver());
    model.routesToAssignment(routes, false, true, assignment);
    for (int i = 0; i < vehicles; ++i) {
      assertEquals(assignment.value(model.nextVar(model.start(i))), i + 1);
      assertEquals(assignment.value(model.nextVar(i + 1)), model.end(i));
    }
  }

  @Test
  public void testRoutingModel_addDisjunction() {
    final RoutingIndexManager manager = new RoutingIndexManager(coordinates.size(), 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    final LongBinaryOperator callback = createManhattanCostCallback(manager);
    final int cost = model.registerTransitCallback(callback);
    model.setArcCostEvaluatorOfAllVehicles(cost);

    int[] a = new int[2];
    a[0] = 2;
    a[1] = 3;
    int[] b = new int[1];
    b[0] = 1;
    int[] c = new int[1];
    c[0] = 4;
    model.addDisjunction(manager.nodesToIndices(a));
    model.addDisjunction(manager.nodesToIndices(b));
    model.addDisjunction(manager.nodesToIndices(c));

    Assignment solution = model.solve(null);
    assertEquals(8, solution.objectiveValue());
  }

  @Test
  public void testRoutingModel_addConstantDimension() {
    final RoutingIndexManager manager = new RoutingIndexManager(10, 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    assertEquals(10, model.nodes());
    final IntBoolPair pair = model.addConstantDimension(1,
        /*capacity=*/100,
        /*fix_start_cumul_to_zero=*/true, "Dimension");
    assertEquals(1, pair.getFirst());
    assertTrue(pair.getSecond());
    RoutingDimension dimension = model.getMutableDimension("Dimension");
    dimension.setSpanCostCoefficientForAllVehicles(2);

    RoutingSearchParameters searchParameters =
        main.defaultRoutingSearchParameters()
            .toBuilder()
            .setTimeLimit(Duration.newBuilder().setSeconds(10))
            .build();

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    final Assignment solution = model.solveWithParameters(searchParameters);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    assertEquals(20, solution.objectiveValue());
  }

  @Test
  public void testRoutingModel_addVectorDimension() {
    final RoutingIndexManager manager = new RoutingIndexManager(10, 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    assertEquals(10, model.nodes());
    final long[] vector = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    final IntBoolPair pair = model.addVectorDimension(vector,
        /*capacity=*/100,
        /*fix_start_cumul_to_zero=*/true, "Dimension");
    assertEquals(1, pair.getFirst());
    assertTrue(pair.getSecond());
    System.gc(); // model should keep alive the callback
    model.setArcCostEvaluatorOfAllVehicles(pair.getFirst());

    RoutingSearchParameters searchParameters =
        main.defaultRoutingSearchParameters()
            .toBuilder()
            .setTimeLimit(Duration.newBuilder().setSeconds(10))
            .build();

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    final Assignment solution = model.solveWithParameters(searchParameters);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    assertEquals(45, solution.objectiveValue());
  }

  @Test
  public void testRoutingModel_addMatrixDimension() {
    final RoutingIndexManager manager = new RoutingIndexManager(5, 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    assertEquals(5, model.nodes());
    final long[][] matrix = {
        {0, 1, 3, 3, 1},
        {1, 0, 2, 4, 2},
        {3, 2, 0, 4, 4},
        {3, 4, 4, 0, 2},
        {1, 2, 4, 2, 0},
    };
    final IntBoolPair pair = model.addMatrixDimension(matrix,
        /*capacity=*/100,
        /*fix_start_cumul_to_zero=*/true, "Dimension");
    assertEquals(1, pair.getFirst());
    assertTrue(pair.getSecond());
    System.gc(); // model should keep alive the callback
    model.setArcCostEvaluatorOfAllVehicles(pair.getFirst());

    final RoutingSearchParameters searchParameters =
        main.defaultRoutingSearchParameters()
            .toBuilder()
            .setTimeLimit(Duration.newBuilder().setSeconds(10))
            .build();

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    final Assignment solution = model.solveWithParameters(searchParameters);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    assertEquals(10, solution.objectiveValue());
  }

  @Test
  public void testRoutingModel_addDimension() {
    final RoutingIndexManager manager = new RoutingIndexManager(coordinates.size(), 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    final LongBinaryOperator manhattanCostCallback = createManhattanCostCallback(manager);
    final int cost = model.registerTransitCallback(manhattanCostCallback);
    model.setArcCostEvaluatorOfAllVehicles(cost);

    final LongBinaryOperator transit = (long firstIndex, long secondIndex) -> {
      int firstNode = manager.indexToNode(firstIndex);
      int secondNode = manager.indexToNode(secondIndex);
      if (firstNode >= coordinates.size()) {
        firstNode = 0;
      }
      if (secondNode >= coordinates.size()) {
        secondNode = 0;
      }
      long distanceTime = manhattanCostCallback.applyAsLong(firstIndex, secondIndex) * 10;
      long visitingTime = 1;
      return distanceTime + visitingTime;
    };
    assertTrue(
        model.addDimension(model.registerTransitCallback(transit), 1000, 1000, true, "time"));
    RoutingDimension dimension = model.getMutableDimension("time");

    for (int i = 1; i < coordinates.size(); i++) {
      int[] a = new int[1];
      a[0] = i;
      model.addDisjunction(manager.nodesToIndices(a), 10);

      dimension.cumulVar(i).setMin(0);
      dimension.cumulVar(i).setMax(40);
    }

    RoutingSearchParameters searchParameters =
        main.defaultRoutingSearchParameters()
            .toBuilder()
            .setTimeLimit(Duration.newBuilder().setSeconds(10))
            .build();

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    Assignment solution = model.solveWithParameters(searchParameters);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    solution = model.solve(solution);
    assertNotNull(solution);
    assertTrue(solution.objectiveValue() >= 24 && solution.objectiveValue() <= 30);
    for (long i = 1; i < coordinates.size(); i++) {
      assertTrue(
          solution.min(dimension.cumulVar(i)) >= 0 && solution.max(dimension.cumulVar(i)) <= 40);
    }
  }

  @Test
  public void testRoutingModel_dimensionVehicleSpanCost() {
    final RoutingIndexManager manager = new RoutingIndexManager(2, 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    final LongBinaryOperator callback = createReturnOneCallback();
    assertTrue(
        model.addDimension(model.registerTransitCallback(callback), 1000, 1000, true, "time"));
    RoutingDimension dimension = model.getMutableDimension("time");
    dimension.cumulVar(1).setMin(10);
    dimension.setSpanCostCoefficientForAllVehicles(2);
    assertEquals(2, dimension.getSpanCostCoefficientForVehicle(0));

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    final Assignment solution = model.solve(null);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    assertEquals(2 * (10 + 1), solution.objectiveValue());
  }

  @Test
  public void testRoutingModel_dimensionGlobalSpanCost() {
    final RoutingIndexManager manager = new RoutingIndexManager(3, 2, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    final LongBinaryOperator callback = createReturnOneCallback();
    assertTrue(
        model.addDimension(model.registerTransitCallback(callback), 1000, 1000, false, "time"));
    RoutingDimension timeDimension = model.getMutableDimension("time");
    timeDimension.cumulVar(1).setMin(10);
    model.vehicleVar(1).setValue(0);
    timeDimension.cumulVar(2).setMax(2);
    model.vehicleVar(2).setValue(1);
    timeDimension.setGlobalSpanCostCoefficient(2);
    assertEquals(2, timeDimension.getGlobalSpanCostCoefficient());

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    final Assignment solution = model.solve(null);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    assertEquals(2 * (11 - 1), solution.objectiveValue());
  }

  @Test
  public void testRoutingModel_cumulVarSoftUpperBound() {
    final RoutingIndexManager manager = new RoutingIndexManager(2, 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    final LongBinaryOperator callback = createReturnOneCallback();
    assertTrue(
        model.addDimension(model.registerTransitCallback(callback), 1000, 1000, false, "time"));
    RoutingDimension dimension = model.getMutableDimension("time");
    assertEquals(1000, dimension.getCumulVarSoftUpperBound(1));
    assertEquals(0, dimension.getCumulVarSoftUpperBoundCoefficient(1));
    dimension.setCumulVarSoftUpperBound(1, 5, 1);
    assertEquals(5, dimension.getCumulVarSoftUpperBound(1));
    assertEquals(1, dimension.getCumulVarSoftUpperBoundCoefficient(1));
  }

  @Test
  public void testRoutingModel_addDimensionWithVehicleCapacity() {
    final RoutingIndexManager manager = new RoutingIndexManager(1, 3, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    final LongBinaryOperator callback = createReturnOneCallback();
    final long[] capacity = {5, 6, 7};
    model.addDimensionWithVehicleCapacity(
        model.registerTransitCallback(callback), 1000, capacity, false, "dim");
    RoutingDimension dimension = model.getMutableDimension("dim");

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    final Assignment solution = model.solve(null);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    for (int vehicle = 0; vehicle < 3; ++vehicle) {
      assertEquals(vehicle + 4, solution.max(dimension.cumulVar(model.start(vehicle))));
      assertEquals(vehicle + 5, solution.max(dimension.cumulVar(model.end(vehicle))));
    }
  }

  @Test
  public void testRoutingModel_addDimensionWithVehicleTransits() {
    final RoutingIndexManager manager = new RoutingIndexManager(1, 3, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    final LongBinaryOperator[] callbacks = new LongBinaryOperator[3];
    int[] transits = new int[3];
    for (int i = 0; i < 3; ++i) {
      final int value = i + 1;
      callbacks[i] = (long firstIndex, long secondIndex) -> value;
      transits[i] = model.registerTransitCallback(callbacks[i]);
    }
    long capacity = 5;
    model.addDimensionWithVehicleTransits(transits, 1000, capacity, false, "dim");
    RoutingDimension dimension = model.getMutableDimension("dim");

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    final Assignment solution = model.solve(null);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    for (int vehicle = 0; vehicle < 3; ++vehicle) {
      assertEquals(
          capacity - (vehicle + 1), solution.max(dimension.cumulVar(model.start(vehicle))));
      assertEquals(capacity, solution.max(dimension.cumulVar(model.end(vehicle))));
    }
  }

  @Test
  public void testRoutingModel_addDimensionWithVehicleTransitAndCapacity() {
    final RoutingIndexManager manager = new RoutingIndexManager(1, 3, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    final LongBinaryOperator[] callbacks = new LongBinaryOperator[3];
    final int[] transits = new int[3];
    for (int i = 0; i < 3; ++i) {
      final int value = i + 1;
      callbacks[i] = (long firstIndex, long secondIndex) -> value;
      transits[i] = model.registerTransitCallback(callbacks[i]);
    }
    long[] capacity = new long[3];
    for (int i = 0; i < 3; ++i) {
      capacity[i] = i + 5L;
    }
    model.addDimensionWithVehicleTransitAndCapacity(transits, 1000, capacity, false, "dim");
    final RoutingDimension dimension = model.getMutableDimension("dim");

    assertEquals(RoutingModel.ROUTING_NOT_SOLVED, model.status());
    final Assignment solution = model.solve(null);
    assertEquals(RoutingModel.ROUTING_SUCCESS, model.status());
    assertNotNull(solution);
    for (int vehicle = 0; vehicle < 3; ++vehicle) {
      assertEquals(4, solution.max(dimension.cumulVar(model.start(vehicle))));
      assertEquals(vehicle + 5, solution.max(dimension.cumulVar(model.end(vehicle))));
    }
  }

  @Test
  public void testRoutingModel_intVarVectorGetter() {
    final RoutingIndexManager manager = new RoutingIndexManager(coordinates.size(), 1, 0);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    final LongBinaryOperator manhattanCostCallback = createManhattanCostCallback(manager);
    final int cost = model.registerTransitCallback(manhattanCostCallback);
    model.setArcCostEvaluatorOfAllVehicles(cost);

    final LongBinaryOperator transit = (long firstIndex, long secondIndex) -> {
      int firstNode = manager.indexToNode(firstIndex);
      int secondNode = manager.indexToNode(secondIndex);
      if (firstNode >= coordinates.size()) {
        firstNode = 0;
      }
      if (secondNode >= coordinates.size()) {
        secondNode = 0;
      }
      long distanceTime = manhattanCostCallback.applyAsLong(firstIndex, secondIndex) * 10;
      long visitingTime = 1;
      return distanceTime + visitingTime;
    };
    assertTrue(
        model.addDimension(model.registerTransitCallback(transit), 1000, 1000, true, "time"));
    RoutingDimension timeDimension = model.getMutableDimension("time");
    IntVar[] cumuls = timeDimension.cumuls();
    assertThat(cumuls).isNotEmpty();
    IntVar[] transits = timeDimension.transits();
    assertThat(transits).isNotEmpty();
    IntVar[] slacks = timeDimension.slacks();
    assertThat(slacks).isNotEmpty();
    IntVar[] nexts = model.nexts();
    assertThat(nexts).isNotEmpty();
    IntVar[] vehicleVars = model.vehicleVars();
    assertThat(vehicleVars).isNotEmpty();
  }

  @Test
  public void testBoundCost_ctor() {
    // Create Routing Index Manager
    BoundCost boundCost = new BoundCost();
    assertNotNull(boundCost);
    assertEquals(0, boundCost.getBound());
    assertEquals(0, boundCost.getCost());

    boundCost = new BoundCost(/*bound=*/97, /*cost=*/101);
    assertNotNull(boundCost);
    assertEquals(97, boundCost.getBound());
    assertEquals(101, boundCost.getCost());
  }

  @Test
  public void testRoutingDimension_ctor() {
    final RoutingIndexManager manager = new RoutingIndexManager(31, 7, 3);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    final int transitIndex = model.registerTransitCallback((long fromIndex, long toIndex) -> {
      final int fromNode = manager.indexToNode(fromIndex);
      final int toNode = manager.indexToNode(toIndex);
      return (long) Math.abs(toNode - fromNode);
    });
    assertTrue(model.addDimension(transitIndex, 100, 100, true, "Dimension"));
    final RoutingDimension dimension = model.getMutableDimension("Dimension");
    assertNotNull(dimension);
  }

  @Test
  public void testRoutingDimension_softSpanUpperBound() {
    final RoutingIndexManager manager = new RoutingIndexManager(31, 7, 3);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    final int transitIndex = model.registerTransitCallback((long fromIndex, long toIndex) -> {
      final int fromNode = manager.indexToNode(fromIndex);
      final int toNode = manager.indexToNode(toIndex);
      return (long) Math.abs(toNode - fromNode);
    });
    assertTrue(model.addDimension(transitIndex, 100, 100, true, "Dimension"));
    final RoutingDimension dimension = model.getMutableDimension("Dimension");

    final BoundCost boundCost = new BoundCost(/*bound=*/97, /*cost=*/101);
    assertNotNull(boundCost);
    assertFalse(dimension.hasSoftSpanUpperBounds());
    for (int v : IntStream.range(0, manager.getNumberOfVehicles()).toArray()) {
      dimension.setSoftSpanUpperBoundForVehicle(boundCost, v);
      final BoundCost bc = dimension.getSoftSpanUpperBoundForVehicle(v);
      assertNotNull(bc);
      assertEquals(97, bc.getBound());
      assertEquals(101, bc.getCost());
    }
    assertTrue(dimension.hasSoftSpanUpperBounds());
  }

  @Test
  public void testRoutingDimension_quadraticCostSoftSpanUpperBound() {
    final RoutingIndexManager manager = new RoutingIndexManager(31, 7, 3);
    assertNotNull(manager);
    final RoutingModel model = new RoutingModel(manager);
    assertNotNull(model);
    final int transitIndex = model.registerTransitCallback((long fromIndex, long toIndex) -> {
      final int fromNode = manager.indexToNode(fromIndex);
      final int toNode = manager.indexToNode(toIndex);
      return (long) Math.abs(toNode - fromNode);
    });
    assertTrue(model.addDimension(transitIndex, 100, 100, true, "Dimension"));
    final RoutingDimension dimension = model.getMutableDimension("Dimension");

    final BoundCost boundCost = new BoundCost(/*bound=*/97, /*cost=*/101);
    assertNotNull(boundCost);
    assertFalse(dimension.hasQuadraticCostSoftSpanUpperBounds());
    for (int v : IntStream.range(0, manager.getNumberOfVehicles()).toArray()) {
      dimension.setQuadraticCostSoftSpanUpperBoundForVehicle(boundCost, v);
      final BoundCost bc = dimension.getQuadraticCostSoftSpanUpperBoundForVehicle(v);
      assertNotNull(bc);
      assertEquals(97, bc.getBound());
      assertEquals(101, bc.getCost());
    }
    assertTrue(dimension.hasQuadraticCostSoftSpanUpperBounds());
  }
}
