/*
 * 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.internal.cache.partitioned;

import static org.apache.geode.distributed.ConfigurationProperties.*;

import java.util.Properties;

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

import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.EntryEvent;
import org.apache.geode.cache.ExpirationAction;
import org.apache.geode.cache.ExpirationAttributes;
import org.apache.geode.cache.PartitionAttributesFactory;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionFactory;
import org.apache.geode.cache.RegionShortcut;
import org.apache.geode.cache.client.ClientCacheFactory;
import org.apache.geode.cache.client.ClientRegionFactory;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.apache.geode.cache.server.CacheServer;
import org.apache.geode.cache.util.CacheListenerAdapter;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.internal.AvailablePort;
import org.apache.geode.internal.cache.GemFireCacheImpl;
import org.apache.geode.internal.cache.ha.HARegionQueueStats;
import org.apache.geode.internal.cache.tier.sockets.CacheClientNotifier;
import org.apache.geode.internal.cache.tier.sockets.CacheClientProxy;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.Wait;
import org.apache.geode.test.dunit.WaitCriterion;
import org.apache.geode.test.dunit.internal.JUnit4DistributedTestCase;
import org.apache.geode.test.junit.categories.DistributedTest;

/**
 * The test creates two datastores with a partitioned region, and also running a cache server each.
 * A publisher client is connected to one server while a subscriber client is connected to both the
 * servers. The partitioned region has entry expiry set with ttl of 3 seconds and action as DESTROY.
 * The test ensures that the EXPIRE_DESTROY events are propagated to the subscriber client and the
 * secondary server does process the QRMs for the EXPIRE_DESTROY events.
 */
@Category(DistributedTest.class)
@SuppressWarnings("serial")
public class Bug47388DUnitTest extends JUnit4DistributedTestCase {

  private static VM vm0 = null;
  private static VM vm1 = null;
  private static VM vm2 = null;
  private static VM vm3 = null;

  private static GemFireCacheImpl cache;

  private static volatile boolean lastKeyDestroyed = false;

  public static final String REGION_NAME = Bug47388DUnitTest.class.getSimpleName() + "_region";

  @Override
  public final void postSetUp() throws Exception {
    disconnectFromDS();
    Host host = Host.getHost(0);
    vm0 = host.getVM(0); // datastore and server
    vm1 = host.getVM(1); // datastore and server
    vm2 = host.getVM(2); // durable client with subscription
    vm3 = host.getVM(3); // durable client without subscription

    // int mcastPort = AvailablePort.getRandomAvailablePort(AvailablePort.JGROUPS);
    int port0 = (Integer) vm0.invoke(() -> Bug47388DUnitTest.createCacheServerWithPRDatastore());
    int port1 = (Integer) vm1.invoke(() -> Bug47388DUnitTest.createCacheServerWithPRDatastore());

    vm2.invoke(Bug47388DUnitTest.class, "createClientCache",
        new Object[] {vm2.getHost(), new Integer[] {port0, port1}, Boolean.TRUE});
    vm3.invoke(Bug47388DUnitTest.class, "createClientCache",
        new Object[] {vm3.getHost(), new Integer[] {port0}, Boolean.FALSE});

  }

  @Override
  public final void preTearDown() throws Exception {
    closeCache();

    vm2.invoke(() -> Bug47388DUnitTest.closeCache());
    vm3.invoke(() -> Bug47388DUnitTest.closeCache());

    vm0.invoke(() -> Bug47388DUnitTest.closeCache());
    vm1.invoke(() -> Bug47388DUnitTest.closeCache());
  }

  public static void closeCache() throws Exception {
    if (cache != null) {
      cache.close();
    }
    lastKeyDestroyed = false;
  }

  @SuppressWarnings("deprecation")
  public static Integer createCacheServerWithPRDatastore() throws Exception {
    Properties props = new Properties();
    Bug47388DUnitTest test = new Bug47388DUnitTest();
    DistributedSystem ds = test.getSystem(props);
    ds.disconnect();
    cache = (GemFireCacheImpl) CacheFactory.create(test.getSystem());

    RegionFactory<String, String> rf = cache.createRegionFactory(RegionShortcut.PARTITION);

    rf.setEntryTimeToLive(new ExpirationAttributes(3, ExpirationAction.DESTROY))
        .setPartitionAttributes(new PartitionAttributesFactory<String, String>()
            .setRedundantCopies(1).setTotalNumBuckets(4).create())
        .setConcurrencyChecksEnabled(false);

    rf.create(REGION_NAME);

    CacheServer server = cache.addCacheServer();
    server.setPort(AvailablePort.getRandomAvailablePort(AvailablePort.SOCKET));
    server.start();
    return server.getPort();
  }

  @SuppressWarnings("deprecation")
  public static void createClientCache(Host host, Integer[] ports, Boolean doRI) throws Exception {
    Properties props = new Properties();
    props.setProperty(DURABLE_CLIENT_ID, "my-durable-client-" + ports.length);
    props.setProperty(DURABLE_CLIENT_TIMEOUT, "300000");

    DistributedSystem ds = new Bug47388DUnitTest().getSystem(props);
    ds.disconnect();
    ClientCacheFactory ccf = new ClientCacheFactory(props);
    ccf.setPoolSubscriptionEnabled(doRI);
    ccf.setPoolSubscriptionAckInterval(50);
    ccf.setPoolSubscriptionRedundancy(1);
    for (int port : ports) {
      ccf.addPoolServer(host.getHostName(), port);
    }
    cache = (GemFireCacheImpl) ccf.create();

    ClientRegionFactory<String, String> crf =
        cache.createClientRegionFactory(ClientRegionShortcut.CACHING_PROXY);

    if (doRI) {
      crf.addCacheListener(new CacheListenerAdapter<String, String>() {
        @Override
        public void afterDestroy(EntryEvent<String, String> event) {
          if (event.getKey().equalsIgnoreCase("LAST_KEY")) {
            lastKeyDestroyed = true;
          }
        }
      });
    }

    Region<String, String> region = crf.create(REGION_NAME);

    if (doRI) {
      region.registerInterest("ALL_KEYS", true);
      cache.readyForEvents();
    }
  }

  @SuppressWarnings("unchecked")
  public static void doPuts(Integer numOfSets, Integer numOfPuts) throws Exception {
    Region<String, String> region = cache.getRegion(REGION_NAME);

    for (int i = 0; i < numOfSets; i++) {
      for (int j = 0; j < numOfPuts; j++) {
        region.put("KEY_" + i + "_" + j, "VALUE_" + j);
      }
    }
    region.put("LAST_KEY", "LAST_KEY");
  }

  public static Boolean isPrimaryServer() {
    return ((CacheClientProxy) CacheClientNotifier.getInstance().getClientProxies().toArray()[0])
        .isPrimary();
  }

  public static void verifyClientSubscriptionStats(final Boolean isPrimary, final Integer events)
      throws Exception {

    WaitCriterion wc = new WaitCriterion() {
      private long dispatched;
      private long qrmed;

      @Override
      public boolean done() {
        HARegionQueueStats stats =
            ((CacheClientProxy) CacheClientNotifier.getInstance().getClientProxies().toArray()[0])
                .getHARegionQueue().getStatistics();

        final int numOfEvents;
        if (!isPrimary) {
          numOfEvents = events - 1; // No marker
        } else {
          numOfEvents = events;
        }

        if (isPrimary) {
          this.dispatched = stats.getEventsDispatched();
          return numOfEvents == this.dispatched;
        } else {
          this.qrmed = stats.getEventsRemovedByQrm();
          return this.qrmed == numOfEvents || (this.qrmed + 1) == numOfEvents;
          // Why +1 above? Because sometimes(TODO: explain further) there may
          // not be any QRM sent to the secondary for the last event dispatched
          // at primary.
        }
      }

      @Override
      public String description() {
        return "Expected events: " + events + " but actual eventsDispatched: " + this.dispatched
            + " and actual eventsRemovedByQrm: " + this.qrmed;
      }
    };

    Wait.waitForCriterion(wc, 60 * 1000, 500, true);
  }

  public static void waitForLastKeyDestroyed() throws Exception {
    WaitCriterion wc = new WaitCriterion() {
      @Override
      public boolean done() {
        return lastKeyDestroyed;
      }

      @Override
      public String description() {
        return "Last key's destroy not received";
      }

    };

    Wait.waitForCriterion(wc, 60 * 1000, 500, true);
  }

  @Ignore("TODO: test is disabled due to bug51931")
  @Test
  public void testQRMOfExpiredEventsProcessedSuccessfully() throws Exception {
    int numOfSets = 2, numOfPuts = 5;
    int totalEvents = 23; // = (numOfSets * numOfPuts) * 2 [eviction-destroys] +
                          // 2 [last key's put and eviction-destroy] + 1 [marker
                          // message]
    vm3.invoke(() -> Bug47388DUnitTest.doPuts(numOfSets, numOfPuts));

    boolean isvm0Primary = (Boolean) vm0.invoke(() -> Bug47388DUnitTest.isPrimaryServer());

    vm2.invoke(() -> Bug47388DUnitTest.waitForLastKeyDestroyed());

    vm0.invoke(() -> Bug47388DUnitTest.verifyClientSubscriptionStats(isvm0Primary, totalEvents));
    vm1.invoke(() -> Bug47388DUnitTest.verifyClientSubscriptionStats(!isvm0Primary, totalEvents));
  }
}
