/*
 * 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.dunit;

import static org.junit.Assert.*;

import java.util.Properties;

import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheException;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.PartitionAttributesFactory;
import org.apache.geode.cache.PartitionResolver;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionShortcut;
import org.apache.geode.cache.client.ClientCache;
import org.apache.geode.cache.client.ClientCacheFactory;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.apache.geode.cache.query.Index;
import org.apache.geode.cache.query.QueryService;
import org.apache.geode.cache.query.SelectResults;
import org.apache.geode.cache.query.data.Portfolio;
import org.apache.geode.cache.query.data.Position;
import org.apache.geode.cache.query.internal.QueryObserverHolder;
import org.apache.geode.cache.query.internal.index.IndexManager;
import org.apache.geode.cache30.CacheSerializableRunnable;
import org.apache.geode.internal.cache.execute.PRClientServerTestBase;
import org.apache.geode.test.dunit.Assert;
import org.apache.geode.test.dunit.AsyncInvocation;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.Invoke;
import org.apache.geode.test.dunit.LogWriterUtils;
import org.apache.geode.test.dunit.SerializableRunnable;
import org.apache.geode.test.dunit.ThreadUtils;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.Wait;
import org.apache.geode.test.dunit.cache.internal.JUnit4CacheTestCase;
import org.apache.geode.test.dunit.internal.JUnit4DistributedTestCase;
import org.apache.geode.test.junit.categories.DistributedTest;
import org.apache.geode.test.junit.categories.FlakyTest;

/**
 * This tests the data inconsistency during update on an index and querying the same UNLOCKED index.
 */
@Category(DistributedTest.class)
public class QueryDataInconsistencyDUnitTest extends JUnit4CacheTestCase {

  private static final int cnt = 0;

  private static final int cntDest = 10;

  static VM server = null;

  static VM client = null;

  static Cache cache = null;

  static String PartitionedRegionName1 = "TestPartitionedRegion1"; // default
                                                                   // name
  static String repRegionName = "TestRepRegion"; // default name

  static Integer serverPort1 = null;

  public static int numOfBuckets = 20;

  public static String[] queries =
      new String[] {"select * from /" + PartitionedRegionName1 + " where ID=1",};

  public static String[] queriesForRR =
      new String[] {"<trace> select * from /" + repRegionName + " where ID=1"};

  public static volatile boolean hooked = false;

  public QueryDataInconsistencyDUnitTest() {
    super();
  }

  @Override
  public final void postTearDownCacheTestCase() throws Exception {
    Invoke.invokeInEveryVM(JUnit4DistributedTestCase::disconnectFromDS);
    Invoke.invokeInEveryVM(QueryObserverHolder::reset);
  }

  @Override
  public final void postSetUp() throws Exception {
    Host host = Host.getHost(0);
    server = host.getVM(0);
  }

  @Test
  public void testCompactRangeIndex() {
    // Create caches
    Properties props = new Properties();
    server.invoke(() -> PRClientServerTestBase.createCacheInVm(props));

    server.invoke(new CacheSerializableRunnable("create indexes") {

      @Override
      public void run2() throws CacheException {
        cache = CacheFactory.getAnyInstance();
        Region region = cache.createRegionFactory(RegionShortcut.REPLICATE).create(repRegionName);

        // Create common Portflios and NewPortfolios
        for (int j = cnt; j < cntDest; j++) {
          region.put(new Integer(j), new Portfolio(j));
        }

        QueryService qs = CacheFactory.getAnyInstance().getQueryService();
        try {
          Index index = qs.createIndex("idIndex", "ID", "/" + repRegionName);
          assertEquals(10, index.getStatistics().getNumberOfKeys());
        } catch (Exception e) {
          fail("Index creation failed");
        }
      }
    });
    // Invoke update from client and stop in updateIndex
    // first before updating the RegionEntry and second after updating
    // the RegionEntry.
    AsyncInvocation putThread =
        server.invokeAsync(new CacheSerializableRunnable("update a Region Entry") {

          @Override
          public void run2() throws CacheException {
            Region repRegion = CacheFactory.getAnyInstance().getRegion(repRegionName);
            IndexManager.testHook = new IndexManagerTestHook();
            repRegion.put(new Integer("1"), new Portfolio(cntDest + 1));
            // above call must be hooked in BEFORE_UPDATE_OP call.
          }
        });
    server.invoke(new CacheSerializableRunnable("query on server") {

      @Override
      public void run2() throws CacheException {
        QueryService qs = CacheFactory.getAnyInstance().getQueryService();
        while (!hooked) {
          Wait.pause(100);
        }
        Object rs = null;
        try {
          rs = qs.newQuery("<trace> select * from /" + repRegionName + " where ID = 1").execute();
        } catch (Exception e) {
          e.printStackTrace();
          fail("Query execution failed on server.");
          IndexManager.testHook = null;
        }
        assertTrue(rs instanceof SelectResults);
        assertEquals(1, ((SelectResults) rs).size());
        Portfolio p1 = (Portfolio) ((SelectResults) rs).asList().get(0);
        if (p1.getID() != 1) {
          fail("Query thread did not verify index results even when RE is under update");
          IndexManager.testHook = null;
        }
        hooked = false;// Let client put go further.
      }
    });

    // Client put is again hooked in AFTER_UPDATE_OP call in updateIndex.
    server.invoke(new CacheSerializableRunnable("query on server") {

      @Override
      public void run2() throws CacheException {
        QueryService qs = CacheFactory.getAnyInstance().getQueryService();
        while (!hooked) {
          Wait.pause(100);
        }
        Object rs = null;
        try {
          rs = qs.newQuery("<trace> select * from /" + repRegionName + " where ID = 1").execute();
        } catch (Exception e) {
          e.printStackTrace();
          fail("Query execution failed on server." + e.getMessage());
        } finally {
          IndexManager.testHook = null;
        }
        assertTrue(rs instanceof SelectResults);
        if (((SelectResults) rs).size() > 0) {
          Portfolio p1 = (Portfolio) ((SelectResults) rs).iterator().next();
          if (p1.getID() != 1) {
            fail("Query thread did not verify index results even when RE is under update and "
                + "RegionEntry value has been modified before releasing the lock");
            IndexManager.testHook = null;
          }
        }
        hooked = false;// Let client put go further.
      }
    });
    ThreadUtils.join(putThread, 200);
  }

  @Test
  public void testRangeIndex() {
    // Create caches
    Properties props = new Properties();
    server.invoke(() -> PRClientServerTestBase.createCacheInVm(props));

    server.invoke(new CacheSerializableRunnable("create indexes") {

      @Override
      public void run2() throws CacheException {
        cache = CacheFactory.getAnyInstance();
        Region region = cache.createRegionFactory(RegionShortcut.REPLICATE).create(repRegionName);
        IndexManager.testHook = null;
        // Create common Portfolios and NewPortfolios
        Position.cnt = 0;
        for (int j = cnt; j < cntDest; j++) {
          Portfolio p = new Portfolio(j);
          CacheFactory.getAnyInstance().getLogger().fine("Shobhit: portfolio " + j + " : " + p);
          region.put(new Integer(j), p);
        }

        QueryService qs = CacheFactory.getAnyInstance().getQueryService();
        try {
          Index index = qs.createIndex("posIndex", "pos.secId",
              "/" + repRegionName + " p, p.positions.values pos");
          assertEquals(12, index.getStatistics().getNumberOfKeys());
        } catch (Exception e) {
          fail("Index creation failed");
        }
      }
    });
    // Invoke update from client and stop in updateIndex
    // first before updating the RegionEntry and second after updating
    // the RegionEntry.
    AsyncInvocation putThread =
        server.invokeAsync(new CacheSerializableRunnable("update a Region Entry") {

          @Override
          public void run2() throws CacheException {
            Region repRegion = CacheFactory.getAnyInstance().getRegion(repRegionName);
            IndexManager.testHook = new IndexManagerTestHook();
            Portfolio newPort = new Portfolio(cntDest + 1);
            CacheFactory.getAnyInstance().getLogger().fine("Shobhit: New Portfolio" + newPort);
            repRegion.put(new Integer("1"), newPort);
            // above call must be hooked in BEFORE_UPDATE_OP call.
          }
        });

    server.invoke(new CacheSerializableRunnable("query on server") {

      @Override
      public void run2() throws CacheException {
        QueryService qs = CacheFactory.getAnyInstance().getQueryService();
        Position pos1 = null;
        while (!hooked) {
          Wait.pause(100);
        }
        try {
          Object rs = qs.newQuery("<trace> select pos from /" + repRegionName
              + " p, p.positions.values pos where pos.secId = 'APPL' AND p.ID = 1").execute();
          CacheFactory.getAnyInstance().getLogger().fine("Shobhit: " + rs);
          assertTrue(rs instanceof SelectResults);
          pos1 = (Position) ((SelectResults) rs).iterator().next();
          if (!pos1.secId.equals("APPL")) {
            fail("Query thread did not verify index results even when RE is under update");
            IndexManager.testHook = null;
          }
        } catch (Exception e) {
          e.printStackTrace();
          Assert.fail("Query execution failed on server.", e);
          IndexManager.testHook = null;
        } finally {
          hooked = false;// Let client put go further.
        }
        while (!hooked) {
          Wait.pause(100);
        }
        try {
          Object rs = qs.newQuery("<trace> select pos from /" + repRegionName
              + " p, p.positions.values pos where pos.secId = 'APPL' AND p.ID = 1").execute();
          CacheFactory.getAnyInstance().getLogger().fine("Shobhit: " + rs);
          assertTrue(rs instanceof SelectResults);
          if (((SelectResults) rs).size() > 0) {
            Position pos2 = (Position) ((SelectResults) rs).iterator().next();
            if (pos2.equals(pos1)) {
              fail("Query thread did not verify index results even when RE is under update and "
                  + "RegionEntry value has been modified before releasing the lock");
            }
          }
        } catch (Exception e) {
          e.printStackTrace();
          fail("Query execution failed on server.");
        } finally {
          hooked = false;// Let client put go further.
          IndexManager.testHook = null;
        }
      }
    });
    ThreadUtils.join(putThread, 200);
  }

  @Category(FlakyTest.class) // GEODE-925: time sensitive, async actions, short timeouts
  @Test
  public void testRangeIndexWithIndexAndQueryFromCluaseMisMatch() { // TODO: fix misspelling
    // Create caches
    Properties props = new Properties();
    server.invoke(() -> PRClientServerTestBase.createCacheInVm(props));

    server.invoke(new CacheSerializableRunnable("create indexes") {

      @Override
      public void run2() throws CacheException {
        cache = CacheFactory.getAnyInstance();
        Region region = cache.createRegionFactory(RegionShortcut.REPLICATE).create(repRegionName);
        IndexManager.testHook = null;
        // Create common Portfolios and NewPortfolios
        Position.cnt = 0;
        for (int j = cnt; j < cntDest; j++) {
          region.put(new Integer(j), new Portfolio(j));
        }

        QueryService qs = CacheFactory.getAnyInstance().getQueryService();
        try {
          Index index = qs.createIndex("posIndex", "pos.secId", "/" + repRegionName
              + " p, p.collectionHolderMap.values coll, p.positions.values pos");
          assertEquals(12, index.getStatistics().getNumberOfKeys());
        } catch (Exception e) {
          fail("Index creation failed");
        }
      }
    });
    // Invoke update from client and stop in updateIndex
    // first before updating the RegionEntry and second after updating
    // the RegionEntry.
    AsyncInvocation putThread =
        server.invokeAsync(new CacheSerializableRunnable("update a Region Entry") {

          @Override
          public void run2() throws CacheException {
            Region repRegion = CacheFactory.getAnyInstance().getRegion(repRegionName);
            IndexManager.testHook = new IndexManagerTestHook();
            // This portfolio with same ID must have different positions.
            repRegion.put(new Integer("1"), new Portfolio(1));
            // above call must be hooked in BEFORE_UPDATE_OP call.
          }
        });

    server.invoke(new CacheSerializableRunnable("query on server") {

      @Override
      public void run2() throws CacheException {
        QueryService qs = CacheFactory.getAnyInstance().getQueryService();
        Position pos1 = null;
        while (!hooked) {
          Wait.pause(100);
        }
        try {
          Object rs = qs.newQuery("<trace> select pos from /" + repRegionName
              + " p, p.positions.values pos where pos.secId = 'APPL' AND p.ID = 1").execute();
          CacheFactory.getAnyInstance().getLogger().fine("Shobhit: " + rs);
          assertTrue(rs instanceof SelectResults);
          pos1 = (Position) ((SelectResults) rs).iterator().next();
          if (!pos1.secId.equals("APPL")) {
            fail("Query thread did not verify index results even when RE is under update");
            IndexManager.testHook = null;
          }
        } catch (Exception e) {
          e.printStackTrace();
          Assert.fail("Query execution failed on server.", e);
          IndexManager.testHook = null;
        } finally {
          hooked = false;// Let client put go further.
        }
        while (!hooked) {
          Wait.pause(100);
        }
        try {
          Object rs = qs.newQuery("select pos from /" + repRegionName
              + " p, p.positions.values pos where pos.secId = 'APPL' AND p.ID = 1").execute();
          assertTrue(rs instanceof SelectResults);
          if (((SelectResults) rs).size() > 0) {
            Position pos2 = (Position) ((SelectResults) rs).iterator().next();
            if (pos2.equals(pos1)) {
              fail("Query thread did not verify index results even when RE is under update and "
                  + "RegionEntry value has been modified before releasing the lock");
            }
          }
        } catch (Exception e) {
          e.printStackTrace();
          fail("Query execution failed on server.");
        } finally {
          hooked = false;// Let client put go further.
          IndexManager.testHook = null;
        }
      }
    });
    ThreadUtils.join(putThread, 200); // GEODE-925 occurs here and this is very short join 200
                                      // millis
  }

  @Test
  public void testRangeIndexWithIndexAndQueryFromCluaseMisMatch2() {
    // Create caches
    Properties props = new Properties();
    server.invoke(() -> PRClientServerTestBase.createCacheInVm(props));

    server.invoke(new CacheSerializableRunnable("create indexes") {

      @Override
      public void run2() throws CacheException {
        cache = CacheFactory.getAnyInstance();
        Region region = cache.createRegionFactory(RegionShortcut.REPLICATE).create(repRegionName);
        IndexManager.testHook = null;
        // Create common Portfolios and NewPortfolios
        Position.cnt = 0;
        for (int j = cnt; j < cntDest; j++) {
          region.put(new Integer(j), new Portfolio(j));
        }

        QueryService qs = CacheFactory.getAnyInstance().getQueryService();
        try {
          Index index = qs.createIndex("posIndex", "pos.secId",
              "/" + repRegionName + " p, p.positions.values pos");
          assertEquals(12, index.getStatistics().getNumberOfKeys());
        } catch (Exception e) {
          fail("Index creation failed");
        }
      }
    });
    // Invoke update from client and stop in updateIndex
    // first before updating the RegionEntry and second after updating
    // the RegionEntry.
    AsyncInvocation putThread =
        server.invokeAsync(new CacheSerializableRunnable("update a Region Entry") {

          @Override
          public void run2() throws CacheException {
            Region repRegion = CacheFactory.getAnyInstance().getRegion(repRegionName);
            IndexManager.testHook = new IndexManagerTestHook();
            // This portfolio with same ID must have different positions.
            repRegion.put(new Integer("1"), new Portfolio(1));
            // above call must be hooked in BEFORE_UPDATE_OP call.
          }
        });

    server.invoke(new CacheSerializableRunnable("query on server") {

      @Override
      public void run2() throws CacheException {
        QueryService qs = CacheFactory.getAnyInstance().getQueryService();
        Position pos1 = null;
        while (!hooked) {
          Wait.pause(100);
        }
        try {
          Object rs = qs
              .newQuery("<trace> select pos from /" + repRegionName
                  + " p, p.collectionHolderMap.values coll, p.positions.values pos where pos.secId = 'APPL' AND p.ID = 1")
              .execute();
          CacheFactory.getAnyInstance().getLogger().fine("Shobhit: " + rs);
          assertTrue(rs instanceof SelectResults);
          pos1 = (Position) ((SelectResults) rs).iterator().next();
          if (!pos1.secId.equals("APPL")) {
            fail("Query thread did not verify index results even when RE is under update");
            IndexManager.testHook = null;
          }
        } catch (Exception e) {
          e.printStackTrace();
          Assert.fail("Query execution failed on server.", e);
          IndexManager.testHook = null;
        } finally {
          hooked = false;// Let client put go further.
        }
        while (!hooked) {
          Wait.pause(100);
        }
        try {
          Object rs = qs
              .newQuery("select pos from /" + repRegionName
                  + " p, p.collectionHolderMap.values coll, p.positions.values pos where pos.secId = 'APPL' AND p.ID = 1")
              .execute();
          assertTrue(rs instanceof SelectResults);
          if (((SelectResults) rs).size() > 0) {
            Position pos2 = (Position) ((SelectResults) rs).iterator().next();
            if (pos2.equals(pos1)) {
              fail("Query thread did not verify index results even when RE is under update and "
                  + "RegionEntry value has been modified before releasing the lock");
            }
          }
        } catch (Exception e) {
          e.printStackTrace();
          fail("Query execution failed on server.");
        } finally {
          IndexManager.testHook = null;
          hooked = false;// Let client put go further.
        }
      }
    });
    ThreadUtils.join(putThread, 200);
  }

  public static void createProxyRegions() {
    new QueryDataInconsistencyDUnitTest().createProxyRegs();
  }

  private void createProxyRegs() {
    ClientCache cache = (ClientCache) CacheFactory.getAnyInstance();
    cache.createClientRegionFactory(ClientRegionShortcut.PROXY).create(repRegionName);

    /*
     * cache.createClientRegionFactory(ClientRegionShortcut.PROXY).create( PartitionedRegionName1);
     */
  }

  public static void createNewPR() {
    new QueryDataInconsistencyDUnitTest().createPR();
  }

  public void createPR() {
    PartitionResolver testKeyBasedResolver = new QueryAPITestPartitionResolver();
    cache = CacheFactory.getAnyInstance();
    cache.createRegionFactory(RegionShortcut.PARTITION_REDUNDANT)
        .setPartitionAttributes(new PartitionAttributesFactory().setTotalNumBuckets(numOfBuckets)
            .setPartitionResolver(testKeyBasedResolver).create())
        .create(PartitionedRegionName1);
  }

  public static void createCacheClientWithoutRegion(String host, Integer port1) {
    new QueryDataInconsistencyDUnitTest().createCacheClientWithoutReg(host, port1);
  }

  private void createCacheClientWithoutReg(String host, Integer port1) {
    disconnectFromDS();
    new ClientCacheFactory().addPoolServer(host, port1).create();
  }

  /**
   * This function puts portfolio objects into the created Region (PR or Local) *
   *
   * @param regionName
   * @param portfolio
   * @param to
   * @param from
   * @return cacheSerializable object
   */
  public CacheSerializableRunnable getCacheSerializableRunnableForPRPuts(final String regionName,
      final Object[] portfolio, final int from, final int to) {
    SerializableRunnable puts = new CacheSerializableRunnable("Region Puts") {
      @Override
      public void run2() throws CacheException {
        Cache cache = CacheFactory.getAnyInstance();
        Region region = cache.getRegion(repRegionName);
        for (int j = from; j < to; j++)
          region.put(new Integer(j), portfolio[j]);
        LogWriterUtils.getLogWriter().info(
            "PRQueryDUnitHelper#getCacheSerializableRunnableForPRPuts: Inserted Portfolio data on Region "
                + regionName);
      }
    };
    return (CacheSerializableRunnable) puts;
  }

  public class IndexManagerTestHook
      implements org.apache.geode.cache.query.internal.index.IndexManager.TestHook {
    public void hook(final int spot) throws RuntimeException {
      switch (spot) {
        case 9: // Before Index update and after region entry lock.
          hooked = true;
          LogWriterUtils.getLogWriter()
              .info("QueryDataInconsistency.IndexManagerTestHook is hooked in Update Index Entry.");
          while (hooked) {
            Wait.pause(100);
          }
          assertEquals(hooked, false);
          break;
        case 10: // Before Region update and after Index Remove call.
          hooked = true;
          LogWriterUtils.getLogWriter()
              .info("QueryDataInconsistency.IndexManagerTestHook is hooked in Remove Index Entry.");
          while (hooked) {
            Wait.pause(100);
          }
          assertEquals(hooked, false);
          break;
        default:
          break;
      }
    }
  }
}
