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

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

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

import org.apache.geode.cache.EntryDestroyedException;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.client.ServerConnectivityException;
import org.apache.geode.test.dunit.IgnoredException;
import org.apache.geode.test.dunit.NetworkUtils;
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.junit.categories.ClientSubscriptionTest;

@SuppressWarnings({"rawtypes", "serial"})
@Category({ClientSubscriptionTest.class})
public class HAInterestPart2DUnitTest extends HAInterestTestCase {

  public HAInterestPart2DUnitTest() {
    super();
  }

  /**
   * Tests if Primary fails during interest un registration should initiate failover should pick new
   * primary
   */
  @Test
  public void testPrimaryFailureInUNregisterInterest() throws Exception {
    createClientPoolCache(this.getName(), NetworkUtils.getServerHostName(server1.getHost()));
    createEntriesK1andK2();
    server1.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    server2.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    server3.invoke(() -> HAInterestTestCase.createEntriesK1andK2());

    registerK1AndK2();

    VM oldPrimary = getPrimaryVM();
    stopPrimaryAndUnregisterRegisterK1();

    verifyDeadAndLiveServers(1, 2);

    VM newPrimary = getPrimaryVM(oldPrimary);
    newPrimary.invoke(() -> HAInterestTestCase.verifyDispatcherIsAlive());
    // primary
    newPrimary.invoke(() -> HAInterestTestCase.verifyInterestUNRegistration());
    // secondary
    getBackupVM().invoke(() -> HAInterestTestCase.verifyInterestUNRegistration());
  }

  /**
   * Tests if Secondary fails during interest un registration should add to dead Ep list
   */
  @Test
  public void testSecondaryFailureInUNRegisterInterest() throws Exception {
    createClientPoolCache(this.getName(), NetworkUtils.getServerHostName(server1.getHost()));
    createEntriesK1andK2();
    server1.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    server2.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    server3.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    registerK1AndK2();
    VM stoppedBackup = stopSecondaryAndUNregisterK1();
    verifyDeadAndLiveServers(1, 2);
    // still primary
    getPrimaryVM().invoke(() -> HAInterestTestCase.verifyDispatcherIsAlive());
    // primary
    getPrimaryVM().invoke(() -> HAInterestTestCase.verifyInterestUNRegistration());
    // secondary
    getBackupVM(stoppedBackup).invoke(() -> HAInterestTestCase.verifyInterestUNRegistration());
  }

  /**
   * Tests a scenario in which Dead Server Monitor detects Server Live Just before interest
   * registration then interest should be registered on the newly detected live server as well
   */
  @Test
  public void testDSMDetectsServerLiveJustBeforeInterestRegistration() throws Exception {
    createClientPoolCache(this.getName(), NetworkUtils.getServerHostName(server1.getHost()));
    createEntriesK1andK2();
    server1.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    server2.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    server3.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    VM backup = getBackupVM();
    backup.invoke(() -> HAInterestTestCase.stopServer());
    verifyDeadAndLiveServers(1, 2);
    setClientServerObserverForBeforeRegistration(backup);
    try {
      registerK1AndK2();
      waitForBeforeRegistrationCallback();
    } finally {
      unSetClientServerObserverForRegistrationCallback();
    }
    server1.invoke(() -> HAInterestTestCase.verifyInterestRegistration());
    server2.invoke(() -> HAInterestTestCase.verifyInterestRegistration());
    server3.invoke(() -> HAInterestTestCase.verifyInterestRegistration());
  }

  /**
   * Tests a scenario in which Dead Server Monitor detects Server Live Just After interest
   * registration then interest should be registered on the newly detected live server as well
   */
  @Test
  public void testDSMDetectsServerLiveJustAfterInterestRegistration() throws Exception {
    createClientPoolCache(this.getName(), NetworkUtils.getServerHostName(server1.getHost()));

    createEntriesK1andK2();
    server1.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    server2.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    server3.invoke(() -> HAInterestTestCase.createEntriesK1andK2());

    VM backup = getBackupVM();
    backup.invoke(() -> HAInterestTestCase.stopServer());
    verifyDeadAndLiveServers(1, 2);

    setClientServerObserverForAfterRegistration(backup);
    try {
      registerK1AndK2();
      waitForAfterRegistrationCallback();
    } finally {
      unSetClientServerObserverForRegistrationCallback();
    }

    server1.invoke(() -> HAInterestTestCase.verifyInterestRegistration());
    server2.invoke(() -> HAInterestTestCase.verifyInterestRegistration());
    server3.invoke(() -> HAInterestTestCase.verifyInterestRegistration());
  }

  /**
   * Tests a Scenario: Only one server, register interest on the server stop server , and update the
   * registered entries on the server start the server , DSM will recover interest list on this live
   * server and verify that as a part of recovery it refreshes registered entries from the server,
   * because it is primary
   */
  @Test
  public void testRefreshEntriesFromPrimaryWhenDSMDetectsServerLive() throws Exception {
    IgnoredException.addIgnoredException(ServerConnectivityException.class.getName());

    PORT1 = ((Integer) server1.invoke(() -> HAInterestTestCase.createServerCache())).intValue();
    server1.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    createClientPoolCacheConnectionToSingleServer(this.getName(),
        NetworkUtils.getServerHostName(server1.getHost()));
    registerK1AndK2();
    verifyRefreshedEntriesFromServer();

    server1.invoke(() -> HAInterestTestCase.stopServer());
    verifyDeadAndLiveServers(1, 0);
    server1.invoke(() -> HAInterestTestCase.putK1andK2());
    server1.invoke(() -> HAInterestTestCase.startServer());
    verifyDeadAndLiveServers(0, 1);
    final Region r1 = cache.getRegion(Region.SEPARATOR + REGION_NAME);
    assertNotNull(r1);
    // Verify for interest registration after cache-server is started.
    server1.invoke(() -> HAInterestTestCase.verifyInterestRegistration());

    WaitCriterion wc = new WaitCriterion() {
      private String excuse;

      @Override
      public boolean done() {
        Region.Entry e1;
        Region.Entry e2;

        try {
          e1 = r1.getEntry(k1);
          if (e1 == null) {
            excuse = "Entry for k1 is null";
            return false;
          }
        } catch (EntryDestroyedException e) {
          excuse = "Entry destroyed";
          return false;
        }
        if (!server_k1.equals(e1.getValue())) {
          excuse = "e1 value is not server_k1";
          return false;
        }
        try {
          e2 = r1.getEntry(k2);
          if (e2 == null) {
            excuse = "Entry for k2 is null";
            return false;
          }
        } catch (EntryDestroyedException e) {
          excuse = "Entry destroyed";
          return false;
        }
        if (!server_k2.equals(e2.getValue())) {
          excuse = "e2 value is not server_k2";
          return false;
        }
        return true;
      }

      @Override
      public String description() {
        return excuse;
      }
    };
    Wait.waitForCriterion(wc, TIMEOUT_MILLIS, INTERVAL_MILLIS, true);
  }

  /**
   * Tests a Scenario: stop a secondary server and update the registered entries on the stopped
   * server start the server , DSM will recover interest list on this live server and verify that as
   * a part of recovery it does not refreshes registered entries from the server, because it is
   * secondary
   */
  @Test
  public void testGIIFromSecondaryWhenDSMDetectsServerLive() throws Exception {
    server1.invoke(() -> HAInterestTestCase.closeCache());
    server2.invoke(() -> HAInterestTestCase.closeCache());
    server3.invoke(() -> HAInterestTestCase.closeCache());

    PORT1 = ((Integer) server1.invoke(() -> HAInterestTestCase.createServerCacheWithLocalRegion()))
        .intValue();
    PORT2 = ((Integer) server2.invoke(() -> HAInterestTestCase.createServerCacheWithLocalRegion()))
        .intValue();
    PORT3 = ((Integer) server3.invoke(() -> HAInterestTestCase.createServerCacheWithLocalRegion()))
        .intValue();

    server1.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    server2.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    server3.invoke(() -> HAInterestTestCase.createEntriesK1andK2());

    createClientPoolCache(this.getName(), NetworkUtils.getServerHostName(server1.getHost()));

    VM backup1 = getBackupVM();
    VM backup2 = getBackupVM(backup1);
    backup1.invoke(() -> HAInterestTestCase.stopServer());
    backup2.invoke(() -> HAInterestTestCase.stopServer());
    verifyDeadAndLiveServers(2, 1);
    registerK1AndK2();
    verifyRefreshedEntriesFromServer();
    backup1.invoke(() -> HAInterestTestCase.putK1andK2());
    backup1.invoke(() -> HAInterestTestCase.startServer());
    verifyDeadAndLiveServers(1, 2);
    verifyRefreshedEntriesFromServer();
  }

  /**
   * Bug Test for Bug # 35945 A java level Deadlock between acquireConnection and RegionEntry during
   * processRecoveredEndpoint by Dead Server Monitor Thread.
   *
   */
  @Test
  public void testBug35945() throws Exception {
    PORT1 = ((Integer) server1.invoke(() -> HAInterestTestCase.createServerCache())).intValue();
    server1.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    createClientPoolCacheConnectionToSingleServer(this.getName(),
        NetworkUtils.getServerHostName(server1.getHost()));
    registerK1AndK2();
    verifyRefreshedEntriesFromServer();

    server1.invoke(() -> HAInterestTestCase.stopServer());
    verifyDeadAndLiveServers(1, 0);
    // put on stopped server
    server1.invoke(() -> HAInterestTestCase.putK1andK2());
    // spawn a thread to put on server , which will acquire a lock on entry
    setClientServerObserverForBeforeInterestRecovery();
    server1.invoke(() -> HAInterestTestCase.startServer());
    verifyDeadAndLiveServers(0, 1);
    waitForBeforeInterestRecoveryCallBack();
    // verify updated value of k1 as a refreshEntriesFromServer
    final Region r1 = cache.getRegion(Region.SEPARATOR + REGION_NAME);
    assertNotNull(r1);

    WaitCriterion wc = new WaitCriterion() {
      private String excuse;

      @Override
      public boolean done() {
        Region.Entry e1 = r1.getEntry(k1);
        Region.Entry e2 = r1.getEntry(k2);
        Object v1 = null;
        if (e1 != null) {
          try {
            v1 = e1.getValue();
          } catch (EntryDestroyedException ignore) {
            // handled to fix GEODE-296
          }
        }
        if (e1 == null || !server_k1_updated.equals(v1)) {
          excuse = "v1=" + v1;
          return false;
        }
        Object v2 = null;
        if (e2 != null) {
          try {
            v2 = e2.getValue();
          } catch (EntryDestroyedException ignore) {
            // handled to fix GEODE-296
          }
        }
        if (e2 == null || !server_k2.equals(v2)) {
          excuse = "v2=" + v2;
          return false;
        }
        return true;
      }

      @Override
      public String description() {
        return excuse;
      }
    };
    Wait.waitForCriterion(wc, TIMEOUT_MILLIS, INTERVAL_MILLIS, true);
  }

  /**
   * Tests if failure occurred in Interest recovery thread, then it should select new endpoint to
   * register interest
   */
  @Test
  public void testInterestRecoveryFailure() throws Exception {
    IgnoredException.addIgnoredException("Server unreachable");

    PORT1 = ((Integer) server1.invoke(() -> HAInterestTestCase.createServerCache())).intValue();
    server1.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    PORT2 = ((Integer) server2.invoke(() -> HAInterestTestCase.createServerCache())).intValue();
    server2.invoke(() -> HAInterestTestCase.createEntriesK1andK2());
    createClientPoolCacheWithSmallRetryInterval(this.getName(),
        NetworkUtils.getServerHostName(server1.getHost()));
    registerK1AndK2();
    verifyRefreshedEntriesFromServer();
    VM backup = getBackupVM();
    VM primary = getPrimaryVM();

    backup.invoke(() -> HAInterestTestCase.stopServer());
    primary.invoke(() -> HAInterestTestCase.stopServer());
    verifyDeadAndLiveServers(2, 0);

    primary.invoke(() -> HAInterestTestCase.putK1andK2());
    setClientServerObserverForBeforeInterestRecoveryFailure();
    primary.invoke(() -> HAInterestTestCase.startServer());
    waitForBeforeInterestRecoveryCallBack();
    if (exceptionOccurred) {
      fail("The DSM could not ensure that server 1 is started & serevr 2 is stopped");
    }
    final Region r1 = cache.getRegion(Region.SEPARATOR + REGION_NAME);
    assertNotNull(r1);

    WaitCriterion wc = new WaitCriterion() {
      private String excuse;

      @Override
      public boolean done() {
        Region.Entry e1 = r1.getEntry(k1);
        Region.Entry e2 = r1.getEntry(k2);
        if (e1 == null) {
          excuse = "Entry for k1 still null";
          return false;
        }
        if (e2 == null) {
          excuse = "Entry for k2 still null";
          return false;
        }
        if (!(server_k1.equals(e1.getValue()))) {
          excuse = "Value for k1 wrong";
          return false;
        }
        if (!(server_k2.equals(e2.getValue()))) {
          excuse = "Value for k2 wrong";
          return false;
        }
        return true;
      }

      @Override
      public String description() {
        return excuse;
      }
    };
    Wait.waitForCriterion(wc, TIMEOUT_MILLIS, INTERVAL_MILLIS, true);
  }
}
