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

import static org.apache.geode.cache.Region.SEPARATOR;
import static org.apache.geode.distributed.ConfigurationProperties.LOCATORS;
import static org.apache.geode.distributed.ConfigurationProperties.MCAST_PORT;
import static org.apache.geode.internal.AvailablePortHelper.getRandomAvailableTCPPort;
import static org.apache.geode.test.dunit.Assert.assertEquals;
import static org.apache.geode.test.dunit.Assert.assertNotNull;
import static org.apache.geode.test.dunit.Assert.assertTrue;
import static org.apache.geode.test.dunit.Assert.fail;
import static org.apache.geode.test.dunit.Host.getHost;
import static org.apache.geode.test.dunit.LogWriterUtils.getLogWriter;
import static org.apache.geode.test.dunit.NetworkUtils.getServerHostName;

import java.io.IOException;
import java.util.Iterator;
import java.util.Properties;

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

import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheException;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.CacheListener;
import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.Declarable;
import org.apache.geode.cache.EntryEvent;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.cache.Scope;
import org.apache.geode.cache.client.internal.PoolImpl;
import org.apache.geode.cache.query.CqAttributes;
import org.apache.geode.cache.query.CqAttributesFactory;
import org.apache.geode.cache.query.CqException;
import org.apache.geode.cache.query.CqExistsException;
import org.apache.geode.cache.query.CqListener;
import org.apache.geode.cache.query.CqQuery;
import org.apache.geode.cache.query.QueryService;
import org.apache.geode.cache.query.RegionNotFoundException;
import org.apache.geode.cache.query.cq.dunit.CqQueryTestListener;
import org.apache.geode.cache.util.CacheListenerAdapter;
import org.apache.geode.cache30.CacheSerializableRunnable;
import org.apache.geode.cache30.ClientServerTestCase;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.internal.cache.CacheServerImpl;
import org.apache.geode.internal.cache.Conflatable;
import org.apache.geode.internal.cache.HARegion;
import org.apache.geode.internal.cache.LocalRegion;
import org.apache.geode.internal.cache.tier.sockets.CacheClientProxy;
import org.apache.geode.internal.cache.tier.sockets.CacheServerTestUtil;
import org.apache.geode.internal.cache.tier.sockets.ConflationDistributedTestHelper;
import org.apache.geode.internal.cache.tier.sockets.HAEventWrapper;
import org.apache.geode.test.awaitility.GeodeAwaitility;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.WaitCriterion;
import org.apache.geode.test.dunit.internal.JUnit4DistributedTestCase;
import org.apache.geode.test.junit.categories.ClientSubscriptionTest;

/**
 * This Dunit test is to verify that when the dispatcher of CS dispatches the Event , the peer's
 * HARegionQueue should get the events removed from the HA RegionQueues assuming the QRM thread has
 * acted upon by that time This is done in the following steps 1. start server1 and server2 2. start
 * client1 and client2 3. perform put operation from client1 4. check the entry in the regionque of
 * client2 on server2.It should be present. 5. Wait till client2 receives event 6. Make sure that
 * QRM is envoked 7. Again the entry in the regionque of client2 on server2.It should not be
 * present. 8. close client1 and client2 9. close server1 and server2
 */
@Category({ClientSubscriptionTest.class})
public class HADispatcherDistributedTest extends JUnit4DistributedTestCase {

  private static final String REGION_NAME =
      HADispatcherDistributedTest.class.getSimpleName() + "_region";
  private static final Object dummyObj = "dummyObject";
  private static final String KEY1 = "KEY1";
  private static final String VALUE1 = "VALUE1";
  private static final String KEY2 = "KEY2";
  private static final String VALUE2 = "VALUE2";

  private static Cache cache = null;
  private static volatile boolean isObjectPresent = false;
  private static volatile boolean waitFlag = true;

  private VM server1 = null;
  private VM server2 = null;
  private VM client1 = null;
  private VM client2 = null;
  private int PORT1;
  private int PORT2;

  @Override
  public final void postSetUp() throws Exception {
    String serverHostName = getServerHostName(getHost(0));

    // Server1 VM
    server1 = getHost(0).getVM(0);

    // Server2 VM
    server2 = getHost(0).getVM(1);

    // Client 1 VM
    client1 = getHost(0).getVM(2);

    // client 2 VM
    client2 = getHost(0).getVM(3);

    PORT1 = server1.invoke(() -> createServerCache(Boolean.FALSE));

    server1.invoke(() -> ConflationDistributedTestHelper.setIsSlowStart());
    server1.invoke(this::makeDispatcherSlow);
    server1.invoke(this::setQRMslow);

    PORT2 = server2.invoke(() -> createServerCache(Boolean.TRUE));

    client1.invoke(CacheServerTestUtil::disableShufflingOfEndpoints);
    client2.invoke(CacheServerTestUtil::disableShufflingOfEndpoints);
    client1.invoke(() -> createClientCache(serverHostName, PORT1, PORT2,
        Boolean.FALSE));
    client2.invoke(() -> createClientCache(serverHostName, PORT1, PORT2,
        Boolean.TRUE));
  }

  @Override
  public final void preTearDown() throws Exception {
    client1.invoke(this::closeCache);
    client2.invoke(this::closeCache);
    // close server
    server1.invoke(this::resetQRMslow);
    server1.invoke(this::closeCache);
    server2.invoke(this::closeCache);
  }

  @Test
  public void testDispatcher() throws Exception {
    clientPut(client1, KEY1, VALUE1);
    // Waiting in the client2 till it receives the event for the key.
    checkFromClient(client2);

    // performing check in the regionqueue of the server2
    checkFromServer(server2, KEY1);

    // For CQ Only.
    // performing put from the client1
    clientPut(client1, KEY2, VALUE2);
    checkFromClient(client2);
    checkFromServer(server2, KEY2);

    getLogWriter().info("testDispatcher() completed successfully");
  }

  /**
   * This is to test the serialization mechanism of ClientUpdateMessage. Added after CQ support.
   * This could be done in different way, by overflowing the HARegion queue.
   */
  @Test
  public void testClientUpdateMessageSerialization() throws Exception {
    // Update Value.
    clientPut(client1, KEY1, VALUE1);
    getLogWriter().fine(">>>>>>>> after clientPut(c1, k1, v1)");
    // Waiting in the client2 till it receives the event for the key.
    checkFromClient(client2);
    getLogWriter().fine("after checkFromClient(c2)");

    // performing check in the regionqueue of the server2
    checkFromServer(server2, KEY1);
    getLogWriter().fine("after checkFromServer(s2, k1)");

    // UPDATE.
    clientPut(client1, KEY1, VALUE1);
    getLogWriter().fine("after clientPut 2 (c1, k1, v1)");
    // Waiting in the client2 till it receives the event for the key.
    checkFromClient(client2);
    getLogWriter().fine("after checkFromClient 2 (c2)");

    // performing check in the regionqueue of the server2
    checkFromServer(server2, KEY1);
    getLogWriter().fine("after checkFromServer 2 (s2, k1)");

    getLogWriter().info("testClientUpdateMessageSerialization() completed successfully");
  }

  private void closeCache() {
    if (cache != null && !cache.isClosed()) {
      cache.close();
      cache.getDistributedSystem().disconnect();
    }
  }

  private void setQRMslow() throws InterruptedException {
    int oldMessageSyncInterval = cache.getMessageSyncInterval();
    cache.setMessageSyncInterval(6);
    Thread.sleep((oldMessageSyncInterval + 1) * 1000);
  }

  private void resetQRMslow() {
    cache.setMessageSyncInterval(HARegionQueue.DEFAULT_MESSAGE_SYNC_INTERVAL);
  }

  private void makeDispatcherSlow() {
    System.setProperty("slowStartTimeForTesting", "5000");
  }

  private void clientPut(VM vm, final Object key, final Object value) {
    // performing put from the client1
    vm.invoke(new CacheSerializableRunnable("putFromClient") {
      @Override
      public void run2() throws CacheException {
        Region region = cache.getRegion(SEPARATOR + REGION_NAME);
        assertNotNull(region);
        region.put(key, value);
      }
    });
  }

  private void checkFromClient(VM vm) {
    // Waiting in the client till it receives the event for the key.
    vm.invoke(new CacheSerializableRunnable("checkFromClient") {
      @Override
      public void run2() throws CacheException {
        Region region = cache.getRegion(SEPARATOR + REGION_NAME);
        assertNotNull(region);
        cache.getLogger().fine("starting the wait");
        synchronized (dummyObj) {
          while (waitFlag) {
            try {
              dummyObj.wait(30000);
            } catch (InterruptedException e) {
              fail("interrupted", e);
            }
          }
        }
        cache.getLogger().fine("wait over...waitFlag=" + waitFlag);
        if (waitFlag) {
          fail("test failed");
        }
      }
    });
  }

  private void checkFromServer(VM vm, final Object key) {
    vm.invoke(new CacheSerializableRunnable("checkFromServer") {
      @Override
      public void run2() throws CacheException {
        Iterator iter = cache.getCacheServers().iterator();
        CacheServerImpl server = (CacheServerImpl) iter.next();
        Iterator iter_prox =
            server.getAcceptor().getCacheClientNotifier().getClientProxies().iterator();
        isObjectPresent = false;

        while (iter_prox.hasNext()) {
          final CacheClientProxy proxy = (CacheClientProxy) iter_prox.next();
          HARegion region = (HARegion) proxy.getHARegion();
          assertNotNull(region);
          final HARegionQueue regionQueue = region.getOwner();

          WaitCriterion wc = new WaitCriterion() {
            @Override
            public boolean done() {
              int sz = regionQueue.size();
              cache.getLogger().fine("regionQueue.size()::" + sz);
              return sz == 0 || !proxy.isConnected();
            }

            @Override
            public String description() {
              return "regionQueue not empty with size " + regionQueue.size() + " for proxy "
                  + proxy;
            }
          };
          GeodeAwaitility.await().untilAsserted(wc);

          cache.getLogger().fine("processed a proxy");
        }
      }
    });
  }

  private void createCache(Properties props) {
    DistributedSystem ds = getSystem(props);
    assertNotNull(ds);
    ds.disconnect();
    ds = getSystem(props);
    cache = CacheFactory.create(ds);
    assertNotNull(cache);
  }

  private Integer createServerCache(Boolean isListenerPresent) throws IOException {
    createCache(new Properties());
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.DISTRIBUTED_ACK);
    factory.setDataPolicy(DataPolicy.REPLICATE);
    if (isListenerPresent == true) {
      CacheListener serverListener = new HAServerListener();
      factory.setCacheListener(serverListener);
    }
    RegionAttributes attrs = factory.create();
    cache.createRegion(REGION_NAME, attrs);
    CacheServerImpl server = (CacheServerImpl) cache.addCacheServer();
    assertNotNull(server);
    int port = getRandomAvailableTCPPort();
    server.setPort(port);
    server.setNotifyBySubscription(true);
    server.start();
    return server.getPort();
  }

  private void createClientCache(String hostName, Integer port1, Integer port2,
      Boolean isListenerPresent) throws CqException, CqExistsException, RegionNotFoundException {
    int PORT1 = port1;
    int PORT2 = port2;
    Properties props = new Properties();
    props.setProperty(MCAST_PORT, "0");
    props.setProperty(LOCATORS, "");
    createCache(props);
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.DISTRIBUTED_ACK);
    ClientServerTestCase.configureConnectionPool(factory, hostName, new int[] {PORT1, PORT2}, true,
        -1, 2, null);
    if (isListenerPresent == true) {
      CacheListener clientListener = new HAClientListener();
      factory.setCacheListener(clientListener);
    }
    RegionAttributes attrs = factory.create();
    cache.createRegion(REGION_NAME, attrs);
    Region region = cache.getRegion(SEPARATOR + REGION_NAME);
    assertNotNull(region);

    {
      LocalRegion lr = (LocalRegion) region;
      final PoolImpl pool = (PoolImpl) (lr.getServerProxy().getPool());
      WaitCriterion ev = new WaitCriterion() {
        @Override
        public boolean done() {
          return pool.getPrimary() != null;
        }

        @Override
        public String description() {
          return null;
        }
      };
      GeodeAwaitility.await().untilAsserted(ev);
      ev = new WaitCriterion() {
        @Override
        public boolean done() {
          return pool.getRedundants().size() >= 1;
        }

        @Override
        public String description() {
          return null;
        }
      };
      GeodeAwaitility.await().untilAsserted(ev);

      assertNotNull(pool.getPrimary());
      assertTrue("backups=" + pool.getRedundants() + " expected=" + 1,
          pool.getRedundants().size() >= 1);
      assertEquals(PORT1, pool.getPrimaryPort());
    }

    region.registerInterest(KEY1);

    // Register CQ.
    createCQ();
  }

  private void createCQ() throws CqException, CqExistsException, RegionNotFoundException {
    QueryService cqService = null;
    try {
      cqService = cache.getQueryService();
    } catch (Exception cqe) {
      cqe.printStackTrace();
      fail("Failed to getCQService.");
    }

    // Create CQ Attributes.
    CqAttributesFactory cqf = new CqAttributesFactory();
    CqListener[] cqListeners = {new CqQueryTestListener(getLogWriter())};
    cqf.initCqListeners(cqListeners);
    CqAttributes cqa = cqf.create();

    String cqName = "CQForHARegionQueueTest";
    String queryStr = "Select * from " + SEPARATOR + REGION_NAME;

    // Create CQ.
    CqQuery cq1 = cqService.newCq(cqName, queryStr, cqa);
    cq1.execute();
  }

  /**
   * This is the client listener which notifies the waiting thread when it receives the event.
   */
  private static class HAClientListener extends CacheListenerAdapter implements Declarable {

    @Override
    public void afterCreate(EntryEvent event) {
      synchronized (dummyObj) {
        try {
          Object value = event.getNewValue();
          if (value.equals(VALUE1)) {
            waitFlag = false;
            dummyObj.notifyAll();
          }
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }

    @Override
    public void init(Properties props) {}
  }

  /**
   * This is the server listener which ensures that regionqueue is properly populated
   */
  private static class HAServerListener extends CacheListenerAdapter {
    @Override
    public void afterCreate(EntryEvent event) {
      Cache cache = event.getRegion().getCache();
      Iterator iter = cache.getCacheServers().iterator();
      CacheServerImpl server = (CacheServerImpl) iter.next();
      isObjectPresent = false;

      // The event not be there in the region first time; try couple of time.
      // This should have been replaced by listener on the HARegion and doing wait for event arrival
      // in that.
      while (true) {
        for (CacheClientProxy proxy : server.getAcceptor().getCacheClientNotifier()
            .getClientProxies()) {
          HARegion regionForQueue = (HARegion) proxy.getHARegion();

          for (Object obj : regionForQueue.values()) {
            if (obj instanceof HAEventWrapper) {
              Conflatable confObj = (Conflatable) obj;
              if (KEY1.equals(confObj.getKeyToConflate())
                  || KEY2.equals(confObj.getKeyToConflate())) {
                isObjectPresent = true;
              }
            }
          }
        }

        if (isObjectPresent) {
          break; // From while.
        }

        try {
          Thread.sleep(10);
        } catch (InterruptedException e) {
          fail("interrupted", e);
        }
      }
    }
  }
}
