/*******************************************************************************
 *   Licensed to the Apache Software Foundation (ASF) under one
 *   or more contributor license agreements.  See the NOTICE file
 *   distributed with this work for additional information
 *   regarding copyright ownership.  The ASF licenses this file
 *   to you 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 org.apache.hadoop.yarn.server.resourcemanager.reservation;

import org.apache.hadoop.yarn.api.records.ReservationDefinition;
import org.apache.hadoop.yarn.api.records.ReservationId;
import org.apache.hadoop.yarn.api.records.ReservationRequest;
import org.apache.hadoop.yarn.api.records.ReservationRequestInterpreter;
import org.apache.hadoop.yarn.api.records.ReservationRequests;
import org.apache.hadoop.yarn.api.records.Resource;
import org.apache.hadoop.yarn.api.records.impl.pb.ReservationDefinitionPBImpl;
import org.apache.hadoop.yarn.api.records.impl.pb.ReservationRequestsPBImpl;
import org.apache.hadoop.yarn.server.resourcemanager.reservation.exceptions.PlanningException;
import org.apache.hadoop.yarn.server.resourcemanager.scheduler.QueueMetrics;
import org.apache.hadoop.yarn.util.Clock;
import org.apache.hadoop.yarn.util.resource.DefaultResourceCalculator;
import org.apache.hadoop.yarn.util.resource.ResourceCalculator;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class TestInMemoryPlan {

  private String user = "yarn";
  private String planName = "test-reservation";
  private ResourceCalculator resCalc;
  private Resource minAlloc;
  private Resource maxAlloc;
  private Resource totalCapacity;

  private Clock clock;
  private QueueMetrics queueMetrics;
  private SharingPolicy policy;
  private ReservationAgent agent;
  private Planner replanner;

  @Before
  public void setUp() throws PlanningException {
    resCalc = new DefaultResourceCalculator();
    minAlloc = Resource.newInstance(1024, 1, 1);
    maxAlloc = Resource.newInstance(64 * 1024, 20, 20);
    totalCapacity = Resource.newInstance(100 * 1024, 100, 100);

    clock = mock(Clock.class);
    queueMetrics = mock(QueueMetrics.class);
    policy = mock(SharingPolicy.class);
    replanner = mock(Planner.class);

    when(clock.getTime()).thenReturn(1L);
  }

  @After
  public void tearDown() {
    resCalc = null;
    minAlloc = null;
    maxAlloc = null;
    totalCapacity = null;

    clock = null;
    queueMetrics = null;
    policy = null;
    replanner = null;
  }

  @Test
  public void testAddReservation() {
    Plan plan =
        new InMemoryPlan(queueMetrics, policy, agent, totalCapacity, 1L,
            resCalc, minAlloc, maxAlloc, planName, replanner, true);
    ReservationId reservationID =
        ReservationSystemTestUtil.getNewReservationId();
    int[] alloc = { 10, 10, 10, 10, 10, 10 };
    int start = 100;
    Map<ReservationInterval, ReservationRequest> allocations =
        generateAllocation(start, alloc, false);
    ReservationDefinition rDef =
        createSimpleReservationDefinition(start, start + alloc.length,
            alloc.length, allocations.values());
    ReservationAllocation rAllocation =
        new InMemoryReservationAllocation(reservationID, rDef, user, planName,
            start, start + alloc.length, allocations, resCalc, minAlloc);
    Assert.assertNull(plan.getReservationById(reservationID));
    try {
      plan.addReservation(rAllocation);
    } catch (PlanningException e) {
      Assert.fail(e.getMessage());
    }
    doAssertions(plan, rAllocation);
    for (int i = 0; i < alloc.length; i++) {
      Assert.assertEquals(Resource.newInstance(1024 * (alloc[i]), (alloc[i]), (alloc[i])),
              plan.getTotalCommittedResources(start + i));
      Assert.assertEquals(Resource.newInstance(1024 * (alloc[i]), (alloc[i]), (alloc[i])),
              plan.getConsumptionForUser(user, start + i));
    }
  }

  @Test
  public void testAddEmptyReservation() {
    Plan plan =
        new InMemoryPlan(queueMetrics, policy, agent, totalCapacity, 1L,
            resCalc, minAlloc, maxAlloc, planName, replanner, true);
    ReservationId reservationID =
        ReservationSystemTestUtil.getNewReservationId();
    int[] alloc = {};
    int start = 100;
    Map<ReservationInterval, ReservationRequest> allocations =
        new HashMap<ReservationInterval, ReservationRequest>();
    ReservationDefinition rDef =
        createSimpleReservationDefinition(start, start + alloc.length,
            alloc.length, allocations.values());
    ReservationAllocation rAllocation =
        new InMemoryReservationAllocation(reservationID, rDef, user, planName,
            start, start + alloc.length, allocations, resCalc, minAlloc);
    Assert.assertNull(plan.getReservationById(reservationID));
    try {
      plan.addReservation(rAllocation);
    } catch (PlanningException e) {
      Assert.fail(e.getMessage());
    }
  }

  @Test
  public void testAddReservationAlreadyExists() {
    // First add a reservation
    Plan plan =
        new InMemoryPlan(queueMetrics, policy, agent, totalCapacity, 1L,
            resCalc, minAlloc, maxAlloc, planName, replanner, true);
    ReservationId reservationID =
        ReservationSystemTestUtil.getNewReservationId();
    int[] alloc = { 10, 10, 10, 10, 10, 10 };
    int start = 100;
    Map<ReservationInterval, ReservationRequest> allocations =
        generateAllocation(start, alloc, false);
    ReservationDefinition rDef =
        createSimpleReservationDefinition(start, start + alloc.length,
            alloc.length, allocations.values());
    ReservationAllocation rAllocation =
        new InMemoryReservationAllocation(reservationID, rDef, user, planName,
            start, start + alloc.length, allocations, resCalc, minAlloc);
    Assert.assertNull(plan.getReservationById(reservationID));
    try {
      plan.addReservation(rAllocation);
    } catch (PlanningException e) {
      Assert.fail(e.getMessage());
    }
    doAssertions(plan, rAllocation);
    for (int i = 0; i < alloc.length; i++) {
      Assert.assertEquals(Resource.newInstance(1024 * (alloc[i]), (alloc[i]), (alloc[i])),
              plan.getTotalCommittedResources(start + i));
      Assert.assertEquals(Resource.newInstance(1024 * (alloc[i]), (alloc[i]), (alloc[i])),
              plan.getConsumptionForUser(user, start + i));
    }

    // Try to add it again
    try {
      plan.addReservation(rAllocation);
      Assert.fail("Add should fail as it already exists");
    } catch (IllegalArgumentException e) {
      Assert.assertTrue(e.getMessage().endsWith("already exists"));
    } catch (PlanningException e) {
      Assert.fail(e.getMessage());
    }
    doAssertions(plan, rAllocation);
  }

  @Test
  public void testUpdateReservation() {
    Plan plan =
        new InMemoryPlan(queueMetrics, policy, agent, totalCapacity, 1L,
            resCalc, minAlloc, maxAlloc, planName, replanner, true);
    ReservationId reservationID =
        ReservationSystemTestUtil.getNewReservationId();
    // First add a reservation
    int[] alloc = { 10, 10, 10, 10, 10, 10 };
    int start = 100;
    Map<ReservationInterval, ReservationRequest> allocations =
        generateAllocation(start, alloc, false);
    ReservationDefinition rDef =
        createSimpleReservationDefinition(start, start + alloc.length,
            alloc.length, allocations.values());
    ReservationAllocation rAllocation =
        new InMemoryReservationAllocation(reservationID, rDef, user, planName,
            start, start + alloc.length, allocations, resCalc, minAlloc);
    Assert.assertNull(plan.getReservationById(reservationID));
    try {
      plan.addReservation(rAllocation);
    } catch (PlanningException e) {
      Assert.fail(e.getMessage());
    }
    doAssertions(plan, rAllocation);
    for (int i = 0; i < alloc.length; i++) {
      Assert.assertEquals(Resource.newInstance(1024 * (alloc[i]), (alloc[i]), (alloc[i])),
              plan.getTotalCommittedResources(start + i));
      Assert.assertEquals(Resource.newInstance(1024 * (alloc[i]), (alloc[i]), (alloc[i])),
              plan.getConsumptionForUser(user, start + i));
    }

    // Now update it
    start = 110;
    int[] updatedAlloc = { 0, 5, 10, 10, 5, 0 };
    allocations = generateAllocation(start, updatedAlloc, true);
    rDef =
        createSimpleReservationDefinition(start, start + updatedAlloc.length,
            updatedAlloc.length, allocations.values());
    rAllocation =
        new InMemoryReservationAllocation(reservationID, rDef, user, planName,
            start, start + updatedAlloc.length, allocations, resCalc, minAlloc);
    try {
      plan.updateReservation(rAllocation);
    } catch (PlanningException e) {
      Assert.fail(e.getMessage());
    }
    doAssertions(plan, rAllocation);
    for (int i = 0; i < updatedAlloc.length; i++) {
      Assert.assertEquals(
              Resource.newInstance(1024 * (updatedAlloc[i] + i), updatedAlloc[i]
                      + i, updatedAlloc[i] + i), plan.getTotalCommittedResources(start + i));
      Assert.assertEquals(
              Resource.newInstance(1024 * (updatedAlloc[i] + i), updatedAlloc[i]
                      + i, updatedAlloc[i] + i), plan.getConsumptionForUser(user, start + i));
    }
  }

  @Test
  public void testUpdateNonExistingReservation() {
    Plan plan =
        new InMemoryPlan(queueMetrics, policy, agent, totalCapacity, 1L,
            resCalc, minAlloc, maxAlloc, planName, replanner, true);
    ReservationId reservationID =
        ReservationSystemTestUtil.getNewReservationId();
    // Try to update a reservation without adding
    int[] alloc = { 10, 10, 10, 10, 10, 10 };
    int start = 100;
    Map<ReservationInterval, ReservationRequest> allocations =
        generateAllocation(start, alloc, false);
    ReservationDefinition rDef =
        createSimpleReservationDefinition(start, start + alloc.length,
            alloc.length, allocations.values());
    ReservationAllocation rAllocation =
        new InMemoryReservationAllocation(reservationID, rDef, user, planName,
            start, start + alloc.length, allocations, resCalc, minAlloc);
    Assert.assertNull(plan.getReservationById(reservationID));
    try {
      plan.updateReservation(rAllocation);
      Assert.fail("Update should fail as it does not exist in the plan");
    } catch (IllegalArgumentException e) {
      Assert.assertTrue(e.getMessage().endsWith("does not exist in the plan"));
    } catch (PlanningException e) {
      Assert.fail(e.getMessage());
    }
    Assert.assertNull(plan.getReservationById(reservationID));
  }

  @Test
  public void testDeleteReservation() {
    // First add a reservation
    Plan plan =
        new InMemoryPlan(queueMetrics, policy, agent, totalCapacity, 1L,
            resCalc, minAlloc, maxAlloc, planName, replanner, true);
    ReservationId reservationID =
        ReservationSystemTestUtil.getNewReservationId();
    int[] alloc = { 10, 10, 10, 10, 10, 10 };
    int start = 100;
    Map<ReservationInterval, ReservationRequest> allocations =
        generateAllocation(start, alloc, true);
    ReservationDefinition rDef =
        createSimpleReservationDefinition(start, start + alloc.length,
            alloc.length, allocations.values());
    ReservationAllocation rAllocation =
        new InMemoryReservationAllocation(reservationID, rDef, user, planName,
            start, start + alloc.length, allocations, resCalc, minAlloc);
    Assert.assertNull(plan.getReservationById(reservationID));
    try {
      plan.addReservation(rAllocation);
    } catch (PlanningException e) {
      Assert.fail(e.getMessage());
    }
    doAssertions(plan, rAllocation);
    for (int i = 0; i < alloc.length; i++) {
      Assert.assertEquals(
              Resource.newInstance(1024 * (alloc[i] + i), (alloc[i] + i), (alloc[i] + i)),
              plan.getTotalCommittedResources(start + i));
      Assert.assertEquals(
          Resource.newInstance(1024 * (alloc[i] + i), (alloc[i] + i), (alloc[i] + i)),
          plan.getConsumptionForUser(user, start + i));
    }

    // Now delete it
    try {
      plan.deleteReservation(reservationID);
    } catch (PlanningException e) {
      Assert.fail(e.getMessage());
    }
    Assert.assertNull(plan.getReservationById(reservationID));
    for (int i = 0; i < alloc.length; i++) {
      Assert.assertEquals(Resource.newInstance(0, 0, 0),
          plan.getTotalCommittedResources(start + i));
      Assert.assertEquals(Resource.newInstance(0, 0, 0),
          plan.getConsumptionForUser(user, start + i));
    }
  }

  @Test
  public void testDeleteNonExistingReservation() {
    Plan plan =
        new InMemoryPlan(queueMetrics, policy, agent, totalCapacity, 1L,
            resCalc, minAlloc, maxAlloc, planName, replanner, true);
    ReservationId reservationID =
        ReservationSystemTestUtil.getNewReservationId();
    // Try to delete a reservation without adding
    Assert.assertNull(plan.getReservationById(reservationID));
    try {
      plan.deleteReservation(reservationID);
      Assert.fail("Delete should fail as it does not exist in the plan");
    } catch (IllegalArgumentException e) {
      Assert.assertTrue(e.getMessage().endsWith("does not exist in the plan"));
    } catch (PlanningException e) {
      Assert.fail(e.getMessage());
    }
    Assert.assertNull(plan.getReservationById(reservationID));
  }

  @Test
  public void testArchiveCompletedReservations() {
    Plan plan =
        new InMemoryPlan(queueMetrics, policy, agent, totalCapacity, 1L,
            resCalc, minAlloc, maxAlloc, planName, replanner, true);
    ReservationId reservationID1 =
        ReservationSystemTestUtil.getNewReservationId();
    // First add a reservation
    int[] alloc1 = { 10, 10, 10, 10, 10, 10 };
    int start = 100;
    Map<ReservationInterval, ReservationRequest> allocations1 =
        generateAllocation(start, alloc1, false);
    ReservationDefinition rDef1 =
        createSimpleReservationDefinition(start, start + alloc1.length,
            alloc1.length, allocations1.values());
    ReservationAllocation rAllocation =
        new InMemoryReservationAllocation(reservationID1, rDef1, user,
            planName, start, start + alloc1.length, allocations1, resCalc,
            minAlloc);
    Assert.assertNull(plan.getReservationById(reservationID1));
    try {
      plan.addReservation(rAllocation);
    } catch (PlanningException e) {
      Assert.fail(e.getMessage());
    }
    doAssertions(plan, rAllocation);
    for (int i = 0; i < alloc1.length; i++) {
      Assert.assertEquals(
          Resource.newInstance(1024 * (alloc1[i]), (alloc1[i]), (alloc1[i])),
          plan.getTotalCommittedResources(start + i));
      Assert.assertEquals(
          Resource.newInstance(1024 * (alloc1[i]), (alloc1[i]), (alloc1[i])),
          plan.getConsumptionForUser(user, start + i));
    }

    // Now add another one
    ReservationId reservationID2 =
        ReservationSystemTestUtil.getNewReservationId();
    int[] alloc2 = { 0, 5, 10, 5, 0 };
    Map<ReservationInterval, ReservationRequest> allocations2 =
        generateAllocation(start, alloc2, true);
    ReservationDefinition rDef2 =
        createSimpleReservationDefinition(start, start + alloc2.length,
            alloc2.length, allocations2.values());
    rAllocation =
        new InMemoryReservationAllocation(reservationID2, rDef2, user,
            planName, start, start + alloc2.length, allocations2, resCalc,
            minAlloc);
    Assert.assertNull(plan.getReservationById(reservationID2));
    try {
      plan.addReservation(rAllocation);
    } catch (PlanningException e) {
      Assert.fail(e.getMessage());
    }
    Assert.assertNotNull(plan.getReservationById(reservationID2));
    for (int i = 0; i < alloc2.length; i++) {
      Assert.assertEquals(
          Resource.newInstance(1024 * (alloc1[i] + alloc2[i] + i), alloc1[i]
              + alloc2[i] + i, alloc1[i] + alloc2[i] + i), plan.getTotalCommittedResources(start + i));
      Assert.assertEquals(
          Resource.newInstance(1024 * (alloc1[i] + alloc2[i] + i), alloc1[i]
              + alloc2[i] + i, alloc1[i] + alloc2[i] + i), plan.getConsumptionForUser(user, start + i));
    }

    // Now archive completed reservations
    when(clock.getTime()).thenReturn(106L);
    when(policy.getValidWindow()).thenReturn(1L);
    try {
      // will only remove 2nd reservation as only that has fallen out of the
      // archival window
      plan.archiveCompletedReservations(clock.getTime());
    } catch (PlanningException e) {
      Assert.fail(e.getMessage());
    }
    Assert.assertNotNull(plan.getReservationById(reservationID1));
    Assert.assertNull(plan.getReservationById(reservationID2));
    for (int i = 0; i < alloc1.length; i++) {
      Assert.assertEquals(
          Resource.newInstance(1024 * (alloc1[i]), (alloc1[i]), (alloc1[i])),
          plan.getTotalCommittedResources(start + i));
      Assert.assertEquals(
          Resource.newInstance(1024 * (alloc1[i]), (alloc1[i]), (alloc1[i])),
          plan.getConsumptionForUser(user, start + i));
    }
    when(clock.getTime()).thenReturn(107L);
    try {
      // will remove 1st reservation also as it has fallen out of the archival
      // window
      plan.archiveCompletedReservations(clock.getTime());
    } catch (PlanningException e) {
      Assert.fail(e.getMessage());
    }
    Assert.assertNull(plan.getReservationById(reservationID1));
    for (int i = 0; i < alloc1.length; i++) {
      Assert.assertEquals(Resource.newInstance(0, 0, 0),
          plan.getTotalCommittedResources(start + i));
      Assert.assertEquals(Resource.newInstance(0, 0, 0),
          plan.getConsumptionForUser(user, start + i));
    }
  }

  private void doAssertions(Plan plan, ReservationAllocation rAllocation) {
    ReservationId reservationID = rAllocation.getReservationId();
    Assert.assertNotNull(plan.getReservationById(reservationID));
    Assert.assertEquals(rAllocation, plan.getReservationById(reservationID));
    Assert.assertTrue(((InMemoryPlan) plan).getAllReservations().size() == 1);
    Assert.assertEquals(rAllocation.getEndTime(), plan.getLastEndTime());
    Assert.assertEquals(totalCapacity, plan.getTotalCapacity());
    Assert.assertEquals(minAlloc, plan.getMinimumAllocation());
    Assert.assertEquals(maxAlloc, plan.getMaximumAllocation());
    Assert.assertEquals(resCalc, plan.getResourceCalculator());
    Assert.assertEquals(planName, plan.getQueueName());
    Assert.assertTrue(plan.getMoveOnExpiry());
  }

  private ReservationDefinition createSimpleReservationDefinition(long arrival,
      long deadline, long duration, Collection<ReservationRequest> resources) {
    // create a request with a single atomic ask
    ReservationDefinition rDef = new ReservationDefinitionPBImpl();
    ReservationRequests reqs = new ReservationRequestsPBImpl();
    reqs.setReservationResources(new ArrayList<ReservationRequest>(resources));
    reqs.setInterpreter(ReservationRequestInterpreter.R_ALL);
    rDef.setReservationRequests(reqs);
    rDef.setArrival(arrival);
    rDef.setDeadline(deadline);
    return rDef;
  }

  private Map<ReservationInterval, ReservationRequest> generateAllocation(
      int startTime, int[] alloc, boolean isStep) {
    Map<ReservationInterval, ReservationRequest> req =
        new HashMap<ReservationInterval, ReservationRequest>();
    int numContainers = 0;
    for (int i = 0; i < alloc.length; i++) {
      if (isStep) {
        numContainers = alloc[i] + i;
      } else {
        numContainers = alloc[i];
      }
      ReservationRequest rr =
          ReservationRequest.newInstance(Resource.newInstance(1024, 1, 1),
              (numContainers));
      req.put(new ReservationInterval(startTime + i, startTime + i + 1), rr);
    }
    return req;
  }

}