/*
 * 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.hdds.scm.node.states;

import org.apache.hadoop.hdds.scm.container.ContainerID;
import org.apache.hadoop.hdds.scm.exceptions.SCMException;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Test classes for Node2ContainerMap.
 */
public class Node2ContainerMapTest {
  private final static int DATANODE_COUNT = 300;
  private final static int CONTAINER_COUNT = 1000;
  private final Map<UUID, TreeSet<ContainerID>> testData = new
      ConcurrentHashMap<>();

  @Rule
  public ExpectedException thrown = ExpectedException.none();

  private void generateData() {
    for (int dnIndex = 1; dnIndex <= DATANODE_COUNT; dnIndex++) {
      TreeSet<ContainerID> currentSet = new TreeSet<>();
      for (int cnIndex = 1; cnIndex <= CONTAINER_COUNT; cnIndex++) {
        long currentCnIndex = (dnIndex * CONTAINER_COUNT) + cnIndex;
        currentSet.add(new ContainerID(currentCnIndex));
      }
      testData.put(UUID.randomUUID(), currentSet);
    }
  }

  private UUID getFirstKey() {
    return testData.keySet().iterator().next();
  }

  @Before
  public void setUp() throws Exception {
    generateData();
  }

  @After
  public void tearDown() throws Exception {
  }

  @Test
  public void testIsKnownDatanode() throws SCMException {
    Node2ContainerMap map = new Node2ContainerMap();
    UUID knownNode = getFirstKey();
    UUID unknownNode = UUID.randomUUID();
    Set<ContainerID> containerIDs = testData.get(knownNode);
    map.insertNewDatanode(knownNode, containerIDs);
    Assert.assertTrue("Not able to detect a known node",
        map.isKnownDatanode(knownNode));
    Assert.assertFalse("Unknown node detected",
        map.isKnownDatanode(unknownNode));
  }

  @Test
  public void testInsertNewDatanode() throws SCMException {
    Node2ContainerMap map = new Node2ContainerMap();
    UUID knownNode = getFirstKey();
    Set<ContainerID> containerIDs = testData.get(knownNode);
    map.insertNewDatanode(knownNode, containerIDs);
    Set<ContainerID> readSet = map.getContainers(knownNode);

    // Assert that all elements are present in the set that we read back from
    // node map.
    Set newSet = new TreeSet((readSet));
    Assert.assertTrue(newSet.removeAll(containerIDs));
    Assert.assertTrue(newSet.size() == 0);

    thrown.expect(SCMException.class);
    thrown.expectMessage("already exists");
    map.insertNewDatanode(knownNode, containerIDs);

    map.removeDatanode(knownNode);
    map.insertNewDatanode(knownNode, containerIDs);

  }

  @Test
  public void testProcessReportCheckOneNode() throws SCMException {
    UUID key = getFirstKey();
    Set<ContainerID> values = testData.get(key);
    Node2ContainerMap map = new Node2ContainerMap();
    map.insertNewDatanode(key, values);
    Assert.assertTrue(map.isKnownDatanode(key));
    ReportResult result = map.processReport(key, values);
    Assert.assertEquals(result.getStatus(),
        Node2ContainerMap.ReportStatus.ALL_IS_WELL);
  }

  @Test
  public void testProcessReportInsertAll() throws SCMException {
    Node2ContainerMap map = new Node2ContainerMap();

    for (Map.Entry<UUID, TreeSet<ContainerID>> keyEntry : testData.entrySet()) {
      map.insertNewDatanode(keyEntry.getKey(), keyEntry.getValue());
    }
    // Assert all Keys are known datanodes.
    for (UUID key : testData.keySet()) {
      Assert.assertTrue(map.isKnownDatanode(key));
    }
  }

  /*
  For ProcessReport we have to test the following scenarios.

  1. New Datanode - A new datanode appears and we have to add that to the
  SCM's Node2Container Map.

  2.  New Container - A Datanode exists, but a new container is added to that
   DN. We need to detect that and return a list of added containers.

  3. Missing Container - A Datanode exists, but one of the expected container
   on that datanode is missing. We need to detect that.

   4. We get a container report that has both the missing and new containers.
    We need to return separate lists for these.
   */

  /**
   * Assert that we are able to detect the addition of a new datanode.
   *
   * @throws SCMException
   */
  @Test
  public void testProcessReportDetectNewDataNode() throws SCMException {
    Node2ContainerMap map = new Node2ContainerMap();
    // If we attempt to process a node that is not present in the map,
    // we get a result back that says, NEW_NODE_FOUND.
    UUID key = getFirstKey();
    TreeSet<ContainerID> values = testData.get(key);
    ReportResult result = map.processReport(key, values);
    Assert.assertEquals(Node2ContainerMap.ReportStatus.NEW_DATANODE_FOUND,
        result.getStatus());
    Assert.assertEquals(result.getNewContainers().size(), values.size());
  }

  /**
   * This test asserts that processReport is able to detect new containers
   * when it is added to a datanode. For that we populate the DN with a list
   * of containerIDs and then add few more containers and make sure that we
   * are able to detect them.
   *
   * @throws SCMException
   */
  @Test
  public void testProcessReportDetectNewContainers() throws SCMException {
    Node2ContainerMap map = new Node2ContainerMap();
    UUID key = getFirstKey();
    TreeSet<ContainerID> values = testData.get(key);
    map.insertNewDatanode(key, values);

    final int newCount = 100;
    // This is not a mistake, the treeset seems to be reverse sorted.
    ContainerID last = values.pollFirst();
    TreeSet<ContainerID> addedContainers = new TreeSet<>();
    for (int x = 1; x <= newCount; x++) {
      long cTemp = last.getId() + x;
      addedContainers.add(new ContainerID(cTemp));
    }

    // This set is the super set of existing containers and new containers.
    TreeSet<ContainerID> newContainersSet = new TreeSet<>(values);
    newContainersSet.addAll(addedContainers);

    ReportResult result = map.processReport(key, newContainersSet);

    //Assert that expected size of missing container is same as addedContainers
    Assert.assertEquals(Node2ContainerMap.ReportStatus.NEW_CONTAINERS_FOUND,
        result.getStatus());

    Assert.assertEquals(addedContainers.size(),
        result.getNewContainers().size());

    // Assert that the Container IDs are the same as we added new.
    Assert.assertTrue("All objects are not removed.",
        result.getNewContainers().removeAll(addedContainers));
  }

  /**
   * This test asserts that processReport is able to detect missing containers
   * if they are misssing from a list.
   *
   * @throws SCMException
   */
  @Test
  public void testProcessReportDetectMissingContainers() throws SCMException {
    Node2ContainerMap map = new Node2ContainerMap();
    UUID key = getFirstKey();
    TreeSet<ContainerID> values = testData.get(key);
    map.insertNewDatanode(key, values);

    final int removeCount = 100;
    Random r = new Random();

    ContainerID first = values.pollLast();
    TreeSet<ContainerID> removedContainers = new TreeSet<>();

    // Pick a random container to remove it is ok to collide no issues.
    for (int x = 0; x < removeCount; x++) {
      int startBase = (int) first.getId();
      long cTemp = r.nextInt(values.size());
      removedContainers.add(new ContainerID(cTemp + startBase));
    }

    // This set is a new set with some containers removed.
    TreeSet<ContainerID> newContainersSet = new TreeSet<>(values);
    newContainersSet.removeAll(removedContainers);

    ReportResult result = map.processReport(key, newContainersSet);


    //Assert that expected size of missing container is same as addedContainers
    Assert.assertEquals(Node2ContainerMap.ReportStatus.MISSING_CONTAINERS,
        result.getStatus());
    Assert.assertEquals(removedContainers.size(),
        result.getMissingContainers().size());

    // Assert that the Container IDs are the same as we added new.
    Assert.assertTrue("All missing containers not found.",
        result.getMissingContainers().removeAll(removedContainers));
  }

  @Test
  public void testProcessReportDetectNewAndMissingContainers() throws
      SCMException {
    Node2ContainerMap map = new Node2ContainerMap();
    UUID key = getFirstKey();
    TreeSet<ContainerID> values = testData.get(key);
    map.insertNewDatanode(key, values);

    Set<ContainerID> insertedSet = new TreeSet<>();
    // Insert nodes from 1..30
    for (int x = 1; x <= 30; x++) {
      insertedSet.add(new ContainerID(x));
    }


    final int removeCount = 100;
    Random r = new Random();

    ContainerID first = values.pollLast();
    TreeSet<ContainerID> removedContainers = new TreeSet<>();

    // Pick a random container to remove it is ok to collide no issues.
    for (int x = 0; x < removeCount; x++) {
      int startBase = (int) first.getId();
      long cTemp = r.nextInt(values.size());
      removedContainers.add(new ContainerID(cTemp + startBase));
    }

    Set<ContainerID> newSet = new TreeSet<>(values);
    newSet.addAll(insertedSet);
    newSet.removeAll(removedContainers);

    ReportResult result = map.processReport(key, newSet);


    Assert.assertEquals(
        Node2ContainerMap.ReportStatus.MISSING_AND_NEW_CONTAINERS_FOUND,
        result.getStatus());
    Assert.assertEquals(removedContainers.size(),
        result.getMissingContainers().size());


    // Assert that the Container IDs are the same as we added new.
    Assert.assertTrue("All missing containers not found.",
        result.getMissingContainers().removeAll(removedContainers));

    Assert.assertEquals(insertedSet.size(),
        result.getNewContainers().size());

    // Assert that the Container IDs are the same as we added new.
    Assert.assertTrue("All inserted containers are not found.",
        result.getNewContainers().removeAll(insertedSet));
  }
}