/*
 * 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.geode.cache.query.internal.index;

import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.IntStream;

import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import org.apache.geode.cache.query.TypeMismatchException;
import org.apache.geode.cache.query.data.Portfolio;
import org.apache.geode.test.junit.categories.UnitTest;

@Category(UnitTest.class)
public class HashIndexSetJUnitTest {

  Map<Integer, Portfolio> portfoliosMap;
  Set<Portfolio> portfolioSet;
  HashIndexSet his;

  private void setupHashIndexSet(int numEntries) {
    his = createHashIndexSet();
    portfoliosMap = createPortfolioObjects(numEntries, 0);
    portfolioSet = new HashSet<Portfolio>(portfoliosMap.values());
    addPortfoliosToHashIndexSet(portfoliosMap, his);
  }

  private void addPortfoliosToHashIndexSet(Map<Integer, Portfolio> portfoliosMap,
      HashIndexSet hashIndexSet) {
    portfoliosMap.forEach((k, v) -> {
      try {
        hashIndexSet.add(k, v);
      } catch (TypeMismatchException exception) {
        throw new Error(exception);
      }
    });
  }

  private HashIndexSet createHashIndexSet() {
    HashIndexSet his = new HashIndexSet();
    HashIndex.IMQEvaluator mockEvaluator = mock(HashIndex.IMQEvaluator.class);
    when(mockEvaluator.evaluateKey(any(Object.class))).thenAnswer(new EvaluateKeyAnswer());
    his.setEvaluator(mockEvaluator);
    return his;
  }

  /**
   * we are "indexed" on indexKey. Equality of portfolios is based on ID indexKeys are based on 0 ->
   * numEntries IDs are startID -> startID + numEntries
   *
   * @param numToCreate how many portfolios to create
   * @param startID the ID value to start incrementing from
   */
  private Map<Integer, Portfolio> createPortfolioObjects(int numToCreate, int startID) {
    Map<Integer, Portfolio> portfoliosMap = new HashMap<>();
    IntStream.range(0, numToCreate).forEach(e -> {
      Portfolio p = new Portfolio(e + startID);
      p.indexKey = e;
      portfoliosMap.put(p.indexKey, p);
    });
    return portfoliosMap;
  }

  @Test
  public void testHashIndexSetAdd() throws Exception {
    int numEntries = 100;
    setupHashIndexSet(numEntries);

    assertEquals(numEntries, his.size());
    his.iterator().forEachRemaining((e -> portfolioSet.remove(e)));
    assertTrue(portfolioSet.isEmpty());
  }

  @Test
  public void testHashIndexSetAddWithNullKey() throws Exception {
    int numEntries = 100;
    setupHashIndexSet(numEntries);

    assertEquals(numEntries, his.size());
    his.add(null, new Portfolio(numEntries + 1));
    assertEquals(numEntries + 1, his.size());
  }

  /**
   * we have to be sure that we dont cause a compaction or growth or else removed tokens will be
   * removed and a new backing array created
   */
  @Test
  public void testHashIndexSetAddUseRemoveTokenSlot() throws Exception {
    int numEntries = 20;
    setupHashIndexSet(numEntries);
    assertEquals(numEntries, his.size());
    his.removeAll(portfolioSet);
    assertEquals(numEntries, his.hashIndexSetProperties.removedTokens);
    assertEquals(0, his.size());
    addPortfoliosToHashIndexSet(portfoliosMap, his);

    assertEquals(0, his.hashIndexSetProperties.removedTokens);
    assertEquals(numEntries, his.size());
  }

  @Test
  public void testCompactDueToTooManyRemoveTokens() throws Exception {
    int numEntries = 10;
    setupHashIndexSet(numEntries);

    assertEquals(numEntries, his.size());
    his.removeAll(portfolioSet);
    assertEquals(numEntries, his.hashIndexSetProperties.removedTokens);

    assertEquals(0, his.size());

    // Very very bad but we fake out the number of removed tokens
    his.hashIndexSetProperties.removedTokens = his.hashIndexSetProperties.maxSize;
    addPortfoliosToHashIndexSet(portfoliosMap, his);

    // compaction should have occurred, removed tokens should now be gone
    assertEquals(0, his.hashIndexSetProperties.removedTokens);
    assertEquals(numEntries, his.size());
  }

  @Test
  public void testRehashRetainsAllValues() throws Exception {
    int numEntries = 80;
    setupHashIndexSet(numEntries);
    assertEquals(numEntries, his.size());
    his.rehash(1000);
    assertEquals(numEntries, his.size());
    his.iterator().forEachRemaining((e -> portfolioSet.remove(e)));
    assertTrue(portfolioSet.isEmpty());
  }

  @Test
  public void testShrinkByRehashRetainsAllValues() throws Exception {
    int numEntries = 20;
    setupHashIndexSet(numEntries);

    assertEquals(numEntries, his.size());
    his.rehash(64);
    assertEquals(numEntries, his.size());
    his.iterator().forEachRemaining((e -> portfolioSet.remove(e)));
    assertTrue(portfolioSet.isEmpty());
  }

  @Test
  public void testGetByKey() throws Exception {
    int numEntries = 20;
    setupHashIndexSet(numEntries);

    assertEquals(numEntries, his.size());
    his.get(1).forEachRemaining((e -> portfolioSet.remove(e)));
    assertEquals(numEntries - 1, portfolioSet.size());
  }

  @Test
  public void testGetByKeyMultipleCollisions() throws Exception {
    int numEntries = 20;
    int keyToLookup = 1;
    his = createHashIndexSet();
    Map<Integer, Portfolio> collectionOfPorts1 = this.createPortfolioObjects(numEntries, 0);
    Map<Integer, Portfolio> collectionOfPorts2 =
        this.createPortfolioObjects(numEntries, numEntries);

    addPortfoliosToHashIndexSet(collectionOfPorts1, his);
    addPortfoliosToHashIndexSet(collectionOfPorts2, his);

    assertEquals(numEntries * 2, his.size());
    Iterator iterator = his.get(keyToLookup);
    int numIterated = 0;
    while (iterator.hasNext()) {
      numIterated++;
      // verify that the returned values match what we lookedup
      assertEquals(keyToLookup, ((Portfolio) iterator.next()).indexKey);
    }
    assertEquals(2, numIterated);
  }

  @Test
  public void testGetByKeyLocatesAfterMultipleColiisionsAndRemoveToken() throws Exception {
    int numEntries = 20;
    int keyToLookup = 1;
    his = createHashIndexSet();
    Map<Integer, Portfolio> collectionOfPorts1 = this.createPortfolioObjects(numEntries, 0);
    Map<Integer, Portfolio> collectionOfPorts2 =
        this.createPortfolioObjects(numEntries, numEntries);
    Map<Integer, Portfolio> collectionOfPorts3 =
        this.createPortfolioObjects(numEntries, numEntries * 2);

    addPortfoliosToHashIndexSet(collectionOfPorts1, his);
    addPortfoliosToHashIndexSet(collectionOfPorts2, his);
    addPortfoliosToHashIndexSet(collectionOfPorts3, his);

    assertEquals(numEntries * 3, his.size());
    Iterator iterator = his.get(keyToLookup);
    int numIterated = 0;
    while (iterator.hasNext()) {
      numIterated++;
      // verify that the returned values match what we lookedup
      assertEquals(keyToLookup, ((Portfolio) iterator.next()).indexKey);
    }
    assertEquals(3, numIterated);

    // let's remove the second collision
    his.remove(keyToLookup, collectionOfPorts2.get(keyToLookup));

    iterator = his.get(keyToLookup);
    numIterated = 0;
    while (iterator.hasNext()) {
      numIterated++;
      // verify that the returned values match what we lookedup
      assertEquals(keyToLookup, ((Portfolio) iterator.next()).indexKey);
    }
    assertEquals(2, numIterated);

    // Add it back in and make sure we can iterate all 3 again
    his.add(keyToLookup, collectionOfPorts2.get(keyToLookup));
    iterator = his.get(keyToLookup);
    numIterated = 0;
    while (iterator.hasNext()) {
      numIterated++;
      // verify that the returned values match what we lookedup
      assertEquals(keyToLookup, ((Portfolio) iterator.next()).indexKey);
    }
    assertEquals(3, numIterated);

  }

  @Test
  public void testGetAllNotMatching() throws Exception {
    int numEntries = 20;
    his = createHashIndexSet();
    Map<Integer, Portfolio> collectionOfPorts1 = this.createPortfolioObjects(numEntries, 0);
    Map<Integer, Portfolio> collectionOfPorts2 =
        this.createPortfolioObjects(numEntries, numEntries);

    addPortfoliosToHashIndexSet(collectionOfPorts1, his);
    addPortfoliosToHashIndexSet(collectionOfPorts2, his);

    assertEquals(numEntries * 2, his.size());
    List<Integer> keysNotToMatch = new LinkedList<>();
    keysNotToMatch.add(3);
    keysNotToMatch.add(4);
    Iterator iterator = his.getAllNotMatching(keysNotToMatch);
    int numIterated = 0;
    while (iterator.hasNext()) {
      numIterated++;
      int idFound = ((Portfolio) iterator.next()).indexKey;
      assertTrue(idFound != 3 && idFound != 4);
    }
    // Make sure we iterated all the entries minus the entries that we decided not to match
    assertEquals(numEntries * 2 - 4, numIterated);
  }

  @Test
  public void testIndexOfObject() throws Exception {
    int numEntries = 10;
    his = createHashIndexSet();
    portfoliosMap = createPortfolioObjects(numEntries, 0);
    portfoliosMap.forEach((k, v) -> {
      try {
        int index = his.add(k, portfoliosMap.get(k));
        int foundIndex = his.index(portfoliosMap.get(k));
        assertEquals(index, foundIndex);
      } catch (TypeMismatchException ex) {
        throw new Error(ex);
      }
    });
  }

  /**
   * Add multiple portfolios with the same id they should collide, we should then be able to look up
   * each one correctly
   */
  @Test
  public void testIndexOfObjectWithCollision() throws Exception {
    int numEntries = 10;
    his = createHashIndexSet();
    Map<Integer, Portfolio> portfoliosMap1 = createPortfolioObjects(numEntries, 0);
    Map<Integer, Portfolio> portfoliosMap2 = createPortfolioObjects(numEntries, numEntries);

    portfoliosMap1.forEach((k, v) -> {
      try {
        int index = his.add(k, portfoliosMap1.get(k));
        int foundIndex = his.index(portfoliosMap1.get(k));
        assertEquals(index, foundIndex);
      } catch (TypeMismatchException ex) {
        throw new Error(ex);
      }
    });
    portfoliosMap2.forEach((k, v) -> {
      try {
        int index = his.add(k, portfoliosMap2.get(k));
        int foundIndex = his.index(portfoliosMap2.get(k));
        assertEquals(index, foundIndex);
      } catch (TypeMismatchException ex) {
        throw new Error(ex);
      }
    });
  }

  @Test
  public void testIndexWhenObjectNotInSet() {
    int numEntries = 10;
    his = createHashIndexSet();
    portfoliosMap = createPortfolioObjects(numEntries, 0);
    assertEquals(-1, his.index(portfoliosMap.get(1)));
  }

  @Test
  public void testIndexWhenObjectNotInSetWhenPopulated() {
    int numEntries = 10;
    this.setupHashIndexSet(numEntries);
    assertEquals(-1, his.index(new Portfolio(numEntries + 1)));
  }

  @Test
  public void testRemove() throws Exception {
    int numEntries = 20;
    setupHashIndexSet(numEntries);

    assertEquals(numEntries, his.size());
    portfoliosMap.forEach((k, v) -> his.remove(k, v));
    assertEquals(0, his.size());
  }

  /**
   * Test remove where we look for an instance that is not at the specified index slot
   */
  @Test
  public void testRemoveIgnoreSlot() throws Exception {
    int numEntries = 20;
    setupHashIndexSet(numEntries);

    assertEquals(numEntries, his.size());
    portfoliosMap.forEach((k, v) -> his.remove(k, v, his.index(v)));
    assertEquals(numEntries, his.size());
  }

  @Test
  public void testRemoveAtWithNull() throws Exception {
    his = createHashIndexSet();
    assertTrue(his.isEmpty());
    assertFalse(his.removeAt(0));
  }

  @Test
  public void testRemoveAtWithRemoveToken() throws Exception {
    his = createHashIndexSet();
    int index = his.add(1, new Portfolio(1));
    assertTrue(his.removeAt(index));
    assertFalse(his.removeAt(index));
  }

  @Test
  public void testHashIndexRemoveAll() throws Exception {
    int numEntries = 100;
    setupHashIndexSet(numEntries);

    assertEquals(numEntries, his.size());
    his.removeAll(portfolioSet);
    assertTrue(his.isEmpty());
  }

  /**
   * Remove all should still remove all portfolios provided, even if there are more provided then
   * contained
   */
  @Test
  public void testHashIndexRemoveAllWithAdditionalPortfolios() throws Exception {
    int numEntries = 100;
    setupHashIndexSet(numEntries);

    assertEquals(numEntries, his.size());
    portfolioSet.add(new Portfolio(numEntries + 1));
    his.removeAll(portfolioSet);
    assertTrue(his.isEmpty());
  }

  @Test
  public void testHashIndexContainsAll() throws Exception {
    int numEntries = 100;
    setupHashIndexSet(numEntries);

    assertEquals(numEntries, his.size());
    assertTrue(his.containsAll(portfolioSet));
  }

  @Test
  public void testHashIndexRetainAll() throws Exception {
    int numEntries = 10;
    setupHashIndexSet(numEntries);
    Set subset = new HashSet();
    portfolioSet.forEach(e -> {
      if (e.indexKey % 2 == 0) {
        subset.add(e);
      }
    });
    assertEquals(numEntries, his.size());
    his.retainAll(subset);
    his.iterator().forEachRemaining((e -> subset.remove(e)));
    assertTrue(subset.isEmpty());
    assertEquals(numEntries / 2, his.size());
  }

  @Test
  public void testHashIndexContainsAllShouldReturnFalse() throws Exception {
    int numEntries = 100;
    setupHashIndexSet(numEntries);

    assertEquals(numEntries, his.size());
    portfolioSet.add(new Portfolio(numEntries + 1));
    assertFalse(his.containsAll(portfolioSet));
  }

  @Test
  public void testClear() throws Exception {
    int numEntries = 100;
    setupHashIndexSet(numEntries);

    assertEquals(numEntries, his.size());
    his.clear();
    assertTrue(his.isEmpty());
    assertTrue(his.hashIndexSetProperties.removedTokens == 0);
  }

  @Test
  public void testAreNullObjectsEqual() throws Exception {
    his = createHashIndexSet();
    assertTrue(his.areObjectsEqual(null, null));
  }

  @Test
  public void testAreIndexeSetsEqualAndHashCodeSame() throws Exception {
    Map<Integer, Portfolio> portfolioMap = createPortfolioObjects(100, 0);
    HashIndexSet indexSet1 = createHashIndexSet();
    HashIndexSet indexSet2 = createHashIndexSet();

    addPortfoliosToHashIndexSet(portfolioMap, indexSet1);
    addPortfoliosToHashIndexSet(portfolioMap, indexSet2);

    assertTrue(indexSet1.equals(indexSet2));
    assertTrue(indexSet2.equals(indexSet1));
    assertEquals(indexSet1.hashCode(), indexSet2.hashCode());
  }

  @Test
  public void testAreIndexeSetsNotEqualAndHashCodeDifferent() throws Exception {
    Map<Integer, Portfolio> portfolioMap = createPortfolioObjects(100, 0);
    HashIndexSet indexSet1 = createHashIndexSet();
    HashIndexSet indexSet2 = createHashIndexSet();

    addPortfoliosToHashIndexSet(portfolioMap, indexSet1);

    indexSet2.add(1, portfolioMap.get(1));
    assertFalse(indexSet2.equals(indexSet1));
    assertFalse(indexSet1.equals(indexSet2));
    assertNotEquals(indexSet1.hashCode(), indexSet2.hashCode());
  }

  @Test
  public void testIndexSetNotEqualsOtherObjectType() {
    HashIndexSet indexSet = createHashIndexSet();
    assertFalse(indexSet.equals("Other type"));
    assertFalse(indexSet.equals(new Object()));
  }

  private static class EvaluateKeyAnswer implements Answer {

    @Override
    public Object answer(InvocationOnMock invocation) throws Throwable {
      Object evalOn = invocation.getArgument(0);
      if (evalOn instanceof Portfolio) {
        Portfolio p = (Portfolio) evalOn;
        return p.indexKey;
      }
      return null;
    }

  }

}
