/*
 * 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.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.stream.IntStream;

import org.junit.Before;
import org.junit.Test;

import org.apache.geode.cache.Region;
import org.apache.geode.cache.query.QueryService;
import org.apache.geode.cache.query.internal.index.AbstractIndex.InternalIndexStatistics;
import org.apache.geode.internal.cache.GemFireCacheImpl;
import org.apache.geode.internal.cache.LocalRegion;
import org.apache.geode.internal.cache.RegionEntry;

public class MemoryIndexStoreJUnitTest {

  Region region;
  GemFireCacheImpl cache;
  InternalIndexStatistics mockStats;
  MemoryIndexStore store;
  RegionEntry[] mockEntries;
  int numMockEntries = 10;
  GemFireCacheImpl actualInstance;

  protected void subclassPreSetup() {}

  protected Region createRegion() {
    return mock(LocalRegion.class);
  }

  @Before
  public void setup() {
    subclassPreSetup();
    region = createRegion();
    cache = mock(GemFireCacheImpl.class);
    mockStats = mock(AbstractIndex.InternalIndexStatistics.class);

    store = new MemoryIndexStore(region, mockStats, cache);
    store.setIndexOnValues(true);
    mockEntries = new RegionEntry[numMockEntries];
    IntStream.range(0, numMockEntries).forEach(i -> {
      mockEntries[i] = createRegionEntry(i, new Object());
    });
  }

  @Test
  public void createIteratorWhenCacheNulledWhenShuttingDownShouldNotThrowNPE() {
    store.get("T");
  }

  @Test
  public void testSizeOfStoreReturnsNumberOfKeysAndNotActualNumberOfValues() {
    IntStream.range(0, 150).forEach(i -> {
      try {
        store.addMapping(i % 3, createRegionEntry(i, new Object()));
      } catch (Exception e) {
        throw new AssertionError(e);
      }
    });
    assertEquals(150, numObjectsInStore(store));
  }

  @Test
  public void testAddEnoughEntriesToCreateAConcurrentHashSet() {
    IntStream.range(0, 150).forEach(i -> {
      try {
        store.addMapping(1, createRegionEntry(i, new Object()));
      } catch (Exception e) {
        throw new AssertionError(e);
      }
    });
    assertEquals(150, numObjectsInStore(store));
  }

  @Test
  public void testUpdateAgainstAConcurrentHashSet() throws Exception {
    IntStream.range(0, 150).forEach(i -> {
      try {
        store.addMapping(1, createRegionEntry(1, new Object()));
      } catch (Exception e) {
        throw new AssertionError(e);
      }
    });
    RegionEntry entry = createRegionEntry(1, new Object());
    store.addMapping(1, entry);
    store.updateMapping(2, 1, entry, entry.getValue(null));
    assertEquals(151, numObjectsInStore(store));
  }

  @Test
  public void testCanAddObjectWithUndefinedKey() throws Exception {
    store.addMapping(QueryService.UNDEFINED, mockEntries[0]);
    assertEquals(1, numObjectsIterated(store.get(QueryService.UNDEFINED)));
    assertEquals(0, numObjectsInStore(store));
  }

  @Test
  public void testCanAddManyObjectsWithUndefinedKey() throws Exception {
    for (final RegionEntry mockEntry : mockEntries) {
      store.addMapping(QueryService.UNDEFINED, mockEntry);
    }
    assertEquals(mockEntries.length, numObjectsIterated(store.get(QueryService.UNDEFINED)));
    // Undefined will not return without an explicit get for UNDEFINED);
    assertEquals(0, numObjectsInStore(store));
  }

  @Test
  public void testIteratorWithStartInclusiveAndNoKeysToRemoveReturnsCorrectNumberOfResults()
      throws Exception {
    addMockedEntries(numMockEntries);
    assertEquals(2, numObjectsIterated(store.iterator(numMockEntries - 2, true, null)));
  }

  @Test
  public void testIteratorWithStartExclusiveAndNoKeysToRemoveReturnsCorrectNumberOfResults()
      throws Exception {
    addMockedEntries(numMockEntries);
    assertEquals(1, numObjectsIterated(store.iterator(numMockEntries - 2, false, null)));
  }

  @Test
  public void testIteratorWithStartInclusiveAndKeyToRemoveReturnsCorrectNumberOfResults()
      throws Exception {
    addMockedEntries(numMockEntries);
    Set keysToRemove = new HashSet();
    keysToRemove.add("1");
    assertEquals(9, numObjectsIterated(store.iterator(1, true, keysToRemove)));
  }

  @Test
  public void testIteratorWithStartExclusiveAndKeyToRemoveReturnsCorrectNumberOfResults()
      throws Exception {
    addMockedEntries(numMockEntries);
    Set keysToRemove = new HashSet();
    keysToRemove.add("1");
    assertEquals(8, numObjectsIterated(store.iterator(1, false, keysToRemove)));
  }

  @Test
  public void testStartAndEndInclusiveReturnsCorrectResults() throws Exception {
    addMockedEntries(numMockEntries);
    assertEquals(6, numObjectsIterated(store.iterator(1, true, 6, true, null)));
  }

  @Test
  public void testStartInclusiveAndEndExclusiveReturnsCorrectResults() throws Exception {
    addMockedEntries(numMockEntries);
    assertEquals(5, numObjectsIterated(store.iterator(1, true, 6, false, null)));
  }

  @Test
  public void testStartExclusiveAndEndExclusiveReturnsCorrectResults() throws Exception {
    addMockedEntries(numMockEntries);
    assertEquals(4, numObjectsIterated(store.iterator(1, false, 6, false, null)));
  }

  @Test
  public void testStartExclusiveAndEndInclusiveReturnsCorrectResults() throws Exception {
    addMockedEntries(numMockEntries);
    assertEquals(5, numObjectsIterated(store.iterator(1, false, 6, true, null)));
  }

  @Test
  public void testStartIsNull() throws Exception {
    addMockedEntries(numMockEntries);
    assertEquals(6, numObjectsIterated(store.iterator(null, false, 6, false, null)));
  }

  @Test
  public void testDescendingIteratorReturnsExpectedOrderOfEntries() throws Exception {
    RegionEntry mockEntry1 = mockEntries[0];
    RegionEntry mockEntry2 = mockEntries[1];
    store.addMapping("1", mockEntry1);
    store.addMapping("2", mockEntry2);
    Iterator iteratorFirst = store.descendingIterator(null);
    assertEquals(2, numObjectsIterated(iteratorFirst));

    Iterator iterator = store.descendingIterator(null);
    iterator.hasNext();
    assertEquals(mockEntry2,
        ((MemoryIndexStore.MemoryIndexStoreEntry) iterator.next()).getRegionEntry());
    iterator.hasNext();
    assertEquals(mockEntry1,
        ((MemoryIndexStore.MemoryIndexStoreEntry) iterator.next()).getRegionEntry());
  }

  @Test
  public void testDescendingIteratorWithRemovedKeysReturnsExpectedOrderOfEntries()
      throws Exception {
    RegionEntry mockEntry1 = mockEntries[0];
    RegionEntry mockEntry2 = mockEntries[1];
    RegionEntry mockEntry3 = mockEntries[2];
    store.addMapping("1", mockEntry1);
    store.addMapping("2", mockEntry2);
    store.addMapping("3", mockEntry3);
    Set keysToRemove = new HashSet();
    keysToRemove.add("2");
    Iterator iteratorFirst = store.descendingIterator(keysToRemove);
    assertEquals(2, numObjectsIterated(iteratorFirst));

    // keysToRemove has been modified by the store, we need to readd the key to remove
    keysToRemove.add("2");
    Iterator iterator = store.descendingIterator(keysToRemove);
    iterator.hasNext();
    assertEquals(mockEntry3,
        ((MemoryIndexStore.MemoryIndexStoreEntry) iterator.next()).getRegionEntry());
    iterator.hasNext();
    assertEquals(mockEntry1,
        ((MemoryIndexStore.MemoryIndexStoreEntry) iterator.next()).getRegionEntry());
    assertFalse(iterator.hasNext());
  }

  @Test
  public void testDescendingIteratorWithMultipleRemovedKeysReturnsExpectedOrderOfEntries()
      throws Exception {
    RegionEntry mockEntry1 = mockEntries[0];
    RegionEntry mockEntry2 = mockEntries[1];
    RegionEntry mockEntry3 = mockEntries[2];
    store.addMapping("1", mockEntry1);
    store.addMapping("2", mockEntry2);
    store.addMapping("3", mockEntry3);
    Set keysToRemove = new HashSet();
    keysToRemove.add("2");
    keysToRemove.add("1");
    Iterator iteratorFirst = store.descendingIterator(keysToRemove);
    assertEquals(1, numObjectsIterated(iteratorFirst));

    // keysToRemove has been modified by the store, we need to readd the key to remove
    keysToRemove.add("2");
    keysToRemove.add("1");
    Iterator iterator = store.descendingIterator(keysToRemove);
    iterator.hasNext();
    assertEquals(mockEntry3,
        ((MemoryIndexStore.MemoryIndexStoreEntry) iterator.next()).getRegionEntry());
    assertFalse(iterator.hasNext());
  }

  @Test
  public void testSizeWithKeyArgumentReturnsCorrectSize() throws Exception {
    RegionEntry mockEntry1 = mockEntries[0];
    RegionEntry mockEntry2 = mockEntries[1];
    store.addMapping("1", mockEntry1);
    store.addMapping("2", mockEntry2);
    assertEquals(1, store.size("1"));
  }

  @Test
  public void testGetReturnsExpectedIteratorValue() throws Exception {
    RegionEntry mockEntry1 = mockEntries[0];
    RegionEntry mockEntry2 = mockEntries[1];
    store.addMapping("1", mockEntry1);
    store.addMapping("2", mockEntry2);
    assertEquals(1, numObjectsIterated(store.get("1")));
  }

  @Test
  public void testGetReturnsExpectedIteratorWithMultipleValues() throws Exception {
    RegionEntry mockEntry1 = mockEntries[0];
    RegionEntry mockEntry2 = mockEntries[1];
    RegionEntry mockEntry3 = mockEntries[2];
    RegionEntry mockEntry4 = mockEntries[3];
    store.addMapping("1", mockEntry1);
    store.addMapping("1", mockEntry2);
    store.addMapping("1", mockEntry3);
    store.addMapping("2", mockEntry4);
    assertEquals(3, numObjectsIterated(store.get("1")));
    assertEquals(4, numObjectsInStore(store));
  }

  @Test
  public void testGetWithIndexOnKeysReturnsExpectedIteratorValues() throws Exception {
    RegionEntry mockEntry1 = mockEntries[0];
    RegionEntry mockEntry2 = mockEntries[1];
    store.setIndexOnValues(false);
    store.setIndexOnRegionKeys(true);
    store.addMapping("1", mockEntry1);
    store.addMapping("2", mockEntry2);
    assertEquals(1, numObjectsIterated(store.get("1")));
  }

  @Test
  public void testCorrectlyRemovesEntryProvidedTheWrongKey() throws Exception {
    RegionEntry mockEntry1 = mockEntries[0];
    RegionEntry mockEntry2 = mockEntries[1];
    store.addMapping("1", mockEntry1);
    store.addMapping("2", mockEntry2);
    store.removeMapping("1", mockEntry2);
    assertEquals(1, numObjectsInStore(store));
    assertTrue(objectContainedIn(store, mockEntry1));
  }

  @Test
  public void testRemoveMappingRemovesFromBackingMap() throws Exception {
    RegionEntry mockEntry1 = mockEntries[0];
    RegionEntry mockEntry2 = mockEntries[1];
    store.addMapping("1", mockEntry1);
    store.addMapping("2", mockEntry2);
    store.removeMapping("1", mockEntry1);
    assertEquals(1, numObjectsInStore(store));
    assertTrue(objectContainedIn(store, mockEntry2));
  }

  @Test
  public void testAddMappingAddsToBackingMap() throws Exception {
    RegionEntry mockEntry1 = mockEntries[0];
    RegionEntry mockEntry2 = mockEntries[1];
    store.addMapping("1", mockEntry1);
    store.addMapping("2", mockEntry2);
    assertEquals(2, numObjectsInStore(store));
    assertTrue(objectContainedIn(store, mockEntry1));
    assertTrue(objectContainedIn(store, mockEntry2));
  }

  @Test
  public void testClear() throws Exception {
    RegionEntry mockEntry1 = mockEntries[0];
    RegionEntry mockEntry2 = mockEntries[1];
    store.addMapping("1", mockEntry1);
    store.addMapping("1", mockEntry2);
    store.clear();
    assertEquals(0, numObjectsInStore(store));
  }

  private int numObjectsInStore(MemoryIndexStore store) {
    Iterator iterator = store.iterator(null);
    return numObjectsIterated(iterator);
  }

  private int numObjectsIterated(Iterator iterator) {
    int count = 0;
    while (iterator.hasNext()) {
      iterator.next();
      count++;
    }
    return count;
  }

  private boolean objectContainedIn(MemoryIndexStore store, Object o) {
    Iterator iterator = store.valueToEntriesMap.values().iterator();
    return objectContainedIn(iterator, o);
  }

  private boolean objectContainedIn(Iterator iterator, Object o) {
    while (iterator.hasNext()) {
      if (iterator.next().equals(o)) {
        return true;
      }
    }
    return false;
  }

  private void addMockedEntries(int numEntriesToAdd) {
    IntStream.range(0, numEntriesToAdd).forEach(i -> {
      try {
        store.addMapping(mockEntries[i].getKey(), mockEntries[i]);
      } catch (Exception e) {
        throw new AssertionError(e);
      }
    });
  }

  private RegionEntry createRegionEntry(Object key, Object value) {
    RegionEntry mockEntry = mock(RegionEntry.class);
    when(mockEntry.getValue(any())).thenReturn(value);
    when(mockEntry.getKey()).thenReturn(key);
    return mockEntry;
  }
}
