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

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.geode.distributed.ConfigurationProperties.CLUSTER_SSL_CIPHERS;
import static org.apache.geode.distributed.ConfigurationProperties.CLUSTER_SSL_ENABLED;
import static org.apache.geode.distributed.ConfigurationProperties.CLUSTER_SSL_PROTOCOLS;
import static org.apache.geode.distributed.ConfigurationProperties.CLUSTER_SSL_REQUIRE_AUTHENTICATION;
import static org.apache.geode.distributed.ConfigurationProperties.ENABLE_NETWORK_PARTITION_DETECTION;
import static org.apache.geode.distributed.ConfigurationProperties.LOCATORS;
import static org.apache.geode.distributed.ConfigurationProperties.MCAST_PORT;
import static org.apache.geode.distributed.internal.DistributionConfig.RESTRICT_MEMBERSHIP_PORT_RANGE;
import static org.apache.geode.internal.AvailablePort.SOCKET;
import static org.apache.geode.internal.AvailablePort.getRandomAvailablePort;
import static org.apache.geode.test.dunit.IgnoredException.addIgnoredException;
import static org.apache.geode.test.dunit.NetworkUtils.getServerHostName;
import static org.apache.geode.test.dunit.Wait.pause;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

import java.io.IOException;
import java.io.Serializable;
import java.net.ConnectException;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import org.awaitility.core.ConditionTimeoutException;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;

import org.apache.geode.InternalGemFireException;
import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.Scope;
import org.apache.geode.cache.client.PoolManager;
import org.apache.geode.cache.client.ServerConnectivityException;
import org.apache.geode.cache.client.internal.PoolImpl;
import org.apache.geode.cache30.ClientServerTestCase;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.DurableClientAttributes;
import org.apache.geode.distributed.Role;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.cache.tier.InternalClientMembership;
import org.apache.geode.internal.cache.tier.sockets.ServerConnection;
import org.apache.geode.management.membership.ClientMembership;
import org.apache.geode.management.membership.ClientMembershipEvent;
import org.apache.geode.management.membership.ClientMembershipListener;
import org.apache.geode.management.membership.MembershipEvent;
import org.apache.geode.management.membership.MembershipListener;
import org.apache.geode.management.membership.UniversalMembershipListenerAdapter;
import org.apache.geode.management.membership.UniversalMembershipListenerAdapter.AdaptedMembershipEvent;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.SerializableCallable;
import org.apache.geode.test.dunit.SerializableRunnable;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.rules.DistributedRestoreSystemProperties;

/**
 * Distributed tests for {@link UniversalMembershipListenerAdapter}.
 *
 * @since GemFire 4.2.1
 */

public class UniversalMembershipListenerAdapterDUnitTest extends ClientServerTestCase {

  private static final int JOINED = 0;
  private static final int LEFT = 1;
  private static final int CRASHED = 2;

  /** Maximum millis allowed for client to fully connect before test fails */
  private static final int JOIN_FAIL_MILLIS = 30 * 1000;

  /**
   * Millis to wait for notification of all three event listeners to be notified
   * <p>
   *
   * Use Integer.MAX_VALUE for debugging
   */
  private static final int ASYNC_EVENT_WAIT_MILLIS = 30 * 1000;

  private static int serverPort;

  private final MembershipNotification joinSystemNotification = new MembershipNotification();
  private final MembershipNotification leftSystemNotification = new MembershipNotification();
  private final MembershipNotification crashedSystemNotification = new MembershipNotification();

  private final MembershipNotification joinAdapterNotification = new MembershipNotification();
  private final MembershipNotification leftAdapterNotification = new MembershipNotification();
  private final MembershipNotification crashedAdapterNotification = new MembershipNotification();

  private final MembershipNotification joinClientNotification = new MembershipNotification();
  private final MembershipNotification leftClientNotification = new MembershipNotification();
  private final MembershipNotification crashedClientNotification = new MembershipNotification();

  @Rule
  public DistributedRestoreSystemProperties distributedRestoreSystemProperties =
      new DistributedRestoreSystemProperties();

  @After
  public void tearDown() throws Exception {
    disconnectAllFromDS();
    InternalClientMembership.unregisterAllListeners();
    ServerConnection.setForceClientCrashEvent(false);
  }

  @Override
  public Properties getDistributedSystemProperties() {
    Properties config = new Properties();
    config.put(ENABLE_NETWORK_PARTITION_DETECTION, "false");
    return config;
  }

  /**
   * Tests wrapping of ClientMembershipEvent fired as MembershipEvent.
   */
  @Test
  public void testAdaptedBridgeEvents() throws Exception {
    getSystem();

    // apparently construction registers with ClientMembership
    new UniversalMembershipListenerAdapter() {
      @Override
      public void memberJoined(final MembershipEvent event) {
        joinAdapterNotification.notify(event);
      }
    };

    DistributedMember member = new FakeDistributedMember("member");
    InternalClientMembership.notifyClientJoined(member);

    joinAdapterNotification.awaitNotification(30, SECONDS);
    joinAdapterNotification.validate(member);
  }

  /**
   * Tests use of history to prevent duplicate events.
   */
  @Test
  public void testNoDuplicates() throws Exception {
    getSystem();

    // apparently construction registers with ClientMembership
    new UniversalMembershipListenerAdapter() {
      @Override
      public void memberJoined(final MembershipEvent event) {
        joinAdapterNotification.notify(event);
      }

      @Override
      public void memberLeft(final MembershipEvent event) {
        joinAdapterNotification.notify(event);
      }

      @Override
      public void memberCrashed(final MembershipEvent event) {
        joinAdapterNotification.notify(event);
      }
    };

    DistributedMember member = new FakeDistributedMember("member");

    // first join
    InternalClientMembership.notifyClientJoined(member);

    joinAdapterNotification.awaitNotification(30, SECONDS);
    joinAdapterNotification.validate(member);
    joinAdapterNotification.reset();

    // duplicate join
    InternalClientMembership.notifyClientJoined(member);

    joinAdapterNotification.awaitWithoutNotification(5, SECONDS);
    joinAdapterNotification.validateNotNotified();

    // first left
    InternalClientMembership.notifyClientLeft(member);

    joinAdapterNotification.awaitNotification(30, SECONDS);
    joinAdapterNotification.validate(member);
    joinAdapterNotification.reset();

    // duplicate left
    InternalClientMembership.notifyClientLeft(member);

    joinAdapterNotification.awaitWithoutNotification(5, SECONDS);
    joinAdapterNotification.validateNotNotified();

    // rejoin
    InternalClientMembership.notifyClientJoined(member);

    joinAdapterNotification.awaitNotification(30, SECONDS);
    joinAdapterNotification.validate(member);
  }

  /**
   * Tests notification of events for loner bridge clients in server process.
   */
  @Test
  public void testLonerClientEventsInServer() throws Exception {
    MembershipListener systemListener = new MembershipListener() {
      @Override
      public void memberJoined(final MembershipEvent event) {
        joinSystemNotification.notify(event);
      }

      @Override
      public void memberLeft(final MembershipEvent event) {
        leftSystemNotification.notify(event);
      }

      @Override
      public void memberCrashed(final MembershipEvent event) {
        crashedSystemNotification.notify(event);
      }
    };

    UniversalMembershipListenerAdapter adapter = new UniversalMembershipListenerAdapter() {
      @Override
      public void memberJoined(final MembershipEvent event) {
        joinAdapterNotification.notify((AdaptedMembershipEvent) event);
      }

      @Override
      public void memberLeft(final MembershipEvent event) {
        leftAdapterNotification.notify((AdaptedMembershipEvent) event);
      }

      @Override
      public synchronized void memberCrashed(final MembershipEvent event) {
        crashedAdapterNotification.notify((AdaptedMembershipEvent) event);
      }
    };

    ClientMembershipListener bridgeListener = new ClientMembershipListener() {
      @Override
      public void memberJoined(final ClientMembershipEvent event) {
        joinClientNotification.notify(event);
      }

      @Override
      public void memberLeft(final ClientMembershipEvent event) {
        leftClientNotification.notify(event);
      }

      @Override
      public void memberCrashed(final ClientMembershipEvent event) {
        crashedClientNotification.notify(event);
      }
    };

    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    String name = this.getUniqueName();
    int[] ports = new int[1];

    // create CacheServer in controller vm...
    getSystem();

    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.LOCAL);
    Region region = createRegion(name, factory.create());

    assertThat(region).isNotNull();
    assertThat(getRootRegion().getSubregion(name)).isNotNull();

    ports[0] = startBridgeServer(0);
    assertThat(ports[0] != 0).isTrue();

    DistributedMember serverMember = getMemberId();
    String serverMemberId = serverMember.getId();

    // register the bridge listener
    ClientMembership.registerClientMembershipListener(bridgeListener);

    InternalCache cache = getInternalCache();
    ManagementService service = ManagementService.getExistingManagementService(cache);
    // register the system listener
    service.addMembershipListener(systemListener);

    // register the universal adapter. Not required as this test is only for BridgeClient test
    adapter.registerMembershipListener(service);

    SerializableCallable createBridgeClient = new SerializableCallable("Create bridge client") {
      @Override
      public Object call() {
        Properties config = new Properties();
        config.setProperty(MCAST_PORT, "0");
        config.setProperty(LOCATORS, "");
        config.setProperty(ENABLE_NETWORK_PARTITION_DETECTION, "false");
        getSystem(config);
        AttributesFactory factory = new AttributesFactory();
        factory.setScope(Scope.LOCAL);
        ClientServerTestCase.configureConnectionPool(factory, getServerHostName(host), ports, false,
            -1, -1, null);
        createRegion(name, factory.create());
        assertThat(getRootRegion().getSubregion(name)).isNotNull();
        return getMemberId();
      }
    };

    // create bridge client in vm0...
    DistributedMember clientMember = (DistributedMember) vm0.invoke(createBridgeClient);
    String clientMemberId = clientMember.getId();

    // should trigger both adapter and bridge listener but not system listener
    joinAdapterNotification.awaitNotification(30, SECONDS);
    joinClientNotification.awaitNotification(30, SECONDS);

    joinSystemNotification.validateNotNotified();
    joinAdapterNotification.validate(clientMember);
    joinClientNotification.validate(clientMember);

    joinSystemNotification.reset();
    joinAdapterNotification.reset();
    joinClientNotification.reset();

    vm0.invoke(new SerializableRunnable("Wait for client to fully connect") {
      @Override
      public void run() {
        String poolName = getRootRegion().getSubregion(name).getAttributes().getPoolName();
        PoolImpl pool = (PoolImpl) PoolManager.find(poolName);
        waitForClientToFullyConnect(pool);
      }
    });

    vm0.invoke(new SerializableRunnable("Close bridge client region") {
      @Override
      public void run() {
        getRootRegion().getSubregion(name).close();
        PoolManager.close();
      }
    });

    leftAdapterNotification.awaitNotification(30, SECONDS);
    leftClientNotification.awaitNotification(30, SECONDS);

    leftSystemNotification.validateNotNotified();
    leftAdapterNotification.validate(clientMember);
    leftClientNotification.validate(clientMember);

    leftSystemNotification.reset();
    leftAdapterNotification.reset();
    leftClientNotification.reset();

    // reconnect bridge client to test for crashed event
    clientMember = (DistributedMember) vm0.invoke(createBridgeClient);

    joinAdapterNotification.awaitNotification(30, SECONDS);
    joinClientNotification.awaitNotification(30, SECONDS);

    joinSystemNotification.validateNotNotified();
    joinAdapterNotification.validate(clientMember);
    joinClientNotification.validate(clientMember);

    joinSystemNotification.reset();
    joinAdapterNotification.reset();
    joinClientNotification.reset();

    vm0.invoke(new SerializableRunnable("Wait for client to fully connect") {
      @Override
      public void run() {
        String poolName = getRootRegion().getSubregion(name).getAttributes().getPoolName();
        PoolImpl pool = (PoolImpl) PoolManager.find(poolName);
        waitForClientToFullyConnect(pool);
      }
    });

    ServerConnection.setForceClientCrashEvent(true);

    vm0.invoke(new SerializableRunnable("Stop bridge client") {
      @Override
      public void run() {
        getRootRegion().getSubregion(name).close();
        PoolManager.close();
      }
    });

    crashedAdapterNotification.awaitNotification(30, SECONDS);
    crashedClientNotification.awaitNotification(30, SECONDS);

    crashedSystemNotification.validateNotNotified();
    crashedAdapterNotification.validate(clientMember);
    crashedClientNotification.validate(clientMember);
  }

  /**
   * Tests notification of events for loner bridge clients in server process.
   */
  @Test
  public void testSystemClientEventsInServer() throws Exception {
    boolean[] firedSystem = new boolean[3];
    DistributedMember[] memberSystem = new DistributedMember[3];
    String[] memberIdSystem = new String[3];
    boolean[] isClientSystem = new boolean[3];

    boolean[] firedAdapter = new boolean[3];
    DistributedMember[] memberAdapter = new DistributedMember[3];
    String[] memberIdAdapter = new String[3];
    boolean[] isClientAdapter = new boolean[3];

    boolean[] firedBridge = new boolean[3];
    DistributedMember[] memberBridge = new DistributedMember[3];
    String[] memberIdBridge = new String[3];
    boolean[] isClientBridge = new boolean[3];

    boolean[] firedSystemDuplicate = new boolean[3];
    boolean[] firedAdapterDuplicate = new boolean[3];
    boolean[] firedBridgeDuplicate = new boolean[3];

    MembershipListener systemListener = new MembershipListener() {
      @Override
      public synchronized void memberJoined(MembershipEvent event) {
        firedSystemDuplicate[JOINED] = firedSystem[JOINED];
        firedSystem[JOINED] = true;
        memberSystem[JOINED] = event.getDistributedMember();
        memberIdSystem[JOINED] = event.getMemberId();
        notifyAll();
      }

      @Override
      public synchronized void memberLeft(MembershipEvent event) {
        firedSystemDuplicate[LEFT] = firedSystem[LEFT];
        firedSystem[LEFT] = true;
        memberSystem[LEFT] = event.getDistributedMember();
        memberIdSystem[LEFT] = event.getMemberId();
        notifyAll();
      }

      @Override
      public synchronized void memberCrashed(MembershipEvent event) {
        firedSystemDuplicate[CRASHED] = firedSystem[CRASHED];
        firedSystem[CRASHED] = true;
        memberSystem[CRASHED] = event.getDistributedMember();
        memberIdSystem[CRASHED] = event.getMemberId();
        notifyAll();
      }
    };

    UniversalMembershipListenerAdapter adapter = new UniversalMembershipListenerAdapter() {
      @Override
      public synchronized void memberJoined(MembershipEvent event) {
        firedAdapterDuplicate[JOINED] = firedAdapter[JOINED];
        firedAdapter[JOINED] = true;
        memberAdapter[JOINED] = event.getDistributedMember();
        memberIdAdapter[JOINED] = event.getMemberId();
        if (event instanceof AdaptedMembershipEvent) {
          isClientAdapter[JOINED] = ((AdaptedMembershipEvent) event).isClient();
        }
        notifyAll();
      }

      @Override
      public synchronized void memberLeft(MembershipEvent event) {
        firedAdapterDuplicate[LEFT] = firedAdapter[LEFT];
        firedAdapter[LEFT] = true;
        memberAdapter[LEFT] = event.getDistributedMember();
        memberIdAdapter[LEFT] = event.getMemberId();
        if (event instanceof AdaptedMembershipEvent) {
          isClientAdapter[LEFT] = ((AdaptedMembershipEvent) event).isClient();
        }
        notifyAll();
      }

      @Override
      public synchronized void memberCrashed(MembershipEvent event) {
        firedAdapterDuplicate[CRASHED] = firedAdapter[CRASHED];
        firedAdapter[CRASHED] = true;
        memberAdapter[CRASHED] = event.getDistributedMember();
        memberIdAdapter[CRASHED] = event.getMemberId();
        if (event instanceof AdaptedMembershipEvent) {
          isClientAdapter[CRASHED] = ((AdaptedMembershipEvent) event).isClient();
        }
        notifyAll();
      }
    };

    ClientMembershipListener bridgeListener = new ClientMembershipListener() {
      @Override
      public synchronized void memberJoined(ClientMembershipEvent event) {
        firedBridgeDuplicate[JOINED] = firedBridge[JOINED];
        firedBridge[JOINED] = true;
        memberBridge[JOINED] = event.getMember();
        memberIdBridge[JOINED] = event.getMemberId();
        isClientBridge[JOINED] = event.isClient();
        notifyAll();
      }

      @Override
      public synchronized void memberLeft(ClientMembershipEvent event) {
        firedBridgeDuplicate[LEFT] = firedBridge[LEFT];
        firedBridge[LEFT] = true;
        memberBridge[LEFT] = event.getMember();
        memberIdBridge[LEFT] = event.getMemberId();
        isClientBridge[LEFT] = event.isClient();
        notifyAll();
      }

      @Override
      public synchronized void memberCrashed(ClientMembershipEvent event) {
        firedBridgeDuplicate[CRASHED] = firedBridge[CRASHED];
        firedBridge[CRASHED] = true;
        memberBridge[CRASHED] = event.getMember();
        memberIdBridge[CRASHED] = event.getMemberId();
        isClientBridge[CRASHED] = event.isClient();
        notifyAll();
      }
    };

    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    String name = this.getUniqueName();
    int[] ports = new int[1];

    // create CacheServer in controller vm...
    getSystem();
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.LOCAL);
    Region region = createRegion(name, factory.create());
    assertThat(region).isNotNull();
    assertThat(getRootRegion().getSubregion(name)).isNotNull();

    ports[0] = startBridgeServer(0);
    assertThat(ports[0] != 0).isTrue();
    DistributedMember serverMember = getMemberId();
    String serverMemberId = serverMember.getId();
    Properties serverProperties = getSystem().getProperties();

    // Below removed properties are already got copied as cluster SSL properties
    serverProperties.remove(CLUSTER_SSL_ENABLED);
    serverProperties.remove(CLUSTER_SSL_CIPHERS);
    serverProperties.remove(CLUSTER_SSL_PROTOCOLS);
    serverProperties.remove(CLUSTER_SSL_REQUIRE_AUTHENTICATION);

    // register the bridge listener
    ClientMembership.registerClientMembershipListener(bridgeListener);

    InternalCache cache = getInternalCache();
    ManagementService service = ManagementService.getExistingManagementService(cache);
    // register the system listener
    service.addMembershipListener(systemListener);

    // register the universal adapter.
    adapter.registerMembershipListener(service);

    SerializableCallable createBridgeClient = new SerializableCallable("Create bridge client") {
      @Override
      public Object call() {
        System.setProperty(RESTRICT_MEMBERSHIP_PORT_RANGE, "false");
        assertThat(getSystem(serverProperties).isConnected()).isTrue();
        assertThat(getCache().isClosed()).isFalse();
        AttributesFactory factory = new AttributesFactory();
        factory.setScope(Scope.LOCAL);
        ClientServerTestCase.configureConnectionPool(factory, getServerHostName(host), ports, false,
            -1, -1, null);
        createRegion(name, factory.create());
        assertThat(getRootRegion().getSubregion(name)).isNotNull();
        return getMemberId();
      }
    };

    // create bridge client in vm0...
    DistributedMember clientMember = (DistributedMember) vm0.invoke(createBridgeClient);
    String clientMemberId = clientMember.getId();

    // should trigger both adapter and bridge listener but not system listener
    synchronized (adapter) {
      while (!firedAdapter[JOINED]) {
        adapter.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }
    synchronized (bridgeListener) {
      while (!firedBridge[JOINED]) {
        bridgeListener.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }
    synchronized (systemListener) {
      while (!firedSystem[JOINED]) {
        systemListener.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }

    assertArrayFalse(firedSystemDuplicate);
    assertArrayFalse(firedAdapterDuplicate);
    assertArrayFalse(firedBridgeDuplicate);

    assertThat(firedBridge[JOINED]).isTrue();
    assertThat(memberBridge[JOINED]).isEqualTo(clientMember);
    assertThat(memberIdBridge[JOINED]).isEqualTo(clientMemberId);
    assertThat(isClientBridge[JOINED]).isTrue();
    assertThat(firedBridge[LEFT]).isFalse();
    assertThat(memberBridge[LEFT]).isNull();
    assertThat(memberIdBridge[LEFT]).isNull();
    assertThat(isClientBridge[LEFT]).isFalse();
    assertThat(firedBridge[CRASHED]).isFalse();
    assertThat(memberBridge[CRASHED]).isNull();
    assertThat(memberIdBridge[CRASHED]).isNull();
    assertThat(isClientBridge[CRASHED]).isFalse();
    resetArraysForTesting(firedBridge, memberBridge, memberIdBridge, isClientBridge);

    assertThat(firedSystem[JOINED]).isTrue();
    assertThat(memberSystem[JOINED]).isEqualTo(clientMember);
    assertThat(memberIdSystem[JOINED]).isEqualTo(clientMemberId);
    assertThat(isClientSystem[JOINED]).isFalse();
    assertThat(firedSystem[LEFT]).isFalse();
    assertThat(memberSystem[LEFT]).isNull();
    assertThat(memberIdSystem[LEFT]).isNull();
    assertThat(isClientSystem[LEFT]).isFalse();
    assertThat(firedSystem[CRASHED]).isFalse();
    assertThat(memberSystem[CRASHED]).isNull();
    assertThat(memberIdSystem[CRASHED]).isNull();
    assertThat(isClientSystem[CRASHED]).isFalse();
    resetArraysForTesting(firedSystem, memberSystem, memberIdSystem, isClientSystem);

    assertThat(firedAdapter[JOINED]).isTrue();
    assertThat(memberAdapter[JOINED]).isEqualTo(clientMember);
    assertThat(memberIdAdapter[JOINED]).isEqualTo(clientMemberId);
    // assertThat(isClientAdapter[JOINED]).isTrue();
    assertThat(firedAdapter[LEFT]).isFalse();
    assertThat(memberAdapter[LEFT]).isNull();
    assertThat(memberIdAdapter[LEFT]).isNull();
    assertThat(isClientAdapter[LEFT]).isFalse();
    assertThat(firedAdapter[CRASHED]).isFalse();
    assertThat(memberAdapter[CRASHED]).isNull();
    assertThat(memberIdAdapter[CRASHED]).isNull();
    assertThat(isClientAdapter[CRASHED]).isFalse();
    resetArraysForTesting(firedAdapter, memberAdapter, memberIdAdapter, isClientAdapter);

    vm0.invoke(new SerializableRunnable("Wait for client to fully connect") {
      @Override
      public void run() {
        String pl = getRootRegion().getSubregion(name).getAttributes().getPoolName();
        PoolImpl pi = (PoolImpl) PoolManager.find(pl);
        waitForClientToFullyConnect(pi);
      }
    });

    // close bridge client region
    vm0.invoke(new SerializableRunnable("Close bridge client region") {
      @Override
      public void run() {
        getRootRegion().getSubregion(name).close();
        PoolManager.close();
      }
    });

    synchronized (adapter) {
      while (!firedAdapter[LEFT]) {
        adapter.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }
    synchronized (bridgeListener) {
      while (!firedBridge[LEFT]) {
        bridgeListener.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }

    assertArrayFalse(firedSystemDuplicate);
    assertArrayFalse(firedAdapterDuplicate);
    assertArrayFalse(firedBridgeDuplicate);

    assertThat(firedBridge[JOINED]).isFalse();
    assertThat(memberIdBridge[JOINED]).isNull();
    assertThat(memberBridge[JOINED]).isNull();
    assertThat(isClientBridge[JOINED]).isFalse();
    assertThat(firedBridge[LEFT]).isTrue();
    assertThat(memberBridge[LEFT]).isEqualTo(clientMember);
    assertThat(memberIdBridge[LEFT]).isEqualTo(clientMemberId);
    assertThat(isClientBridge[LEFT]).isTrue();
    assertThat(firedBridge[CRASHED]).isFalse();
    assertThat(memberBridge[CRASHED]).isNull();
    assertThat(memberIdBridge[CRASHED]).isNull();
    assertThat(isClientBridge[CRASHED]).isFalse();
    resetArraysForTesting(firedBridge, memberBridge, memberIdBridge, isClientBridge);

    assertThat(firedSystem[JOINED]).isFalse();
    assertThat(memberSystem[JOINED]).isNull();
    assertThat(memberIdSystem[JOINED]).isNull();
    assertThat(isClientSystem[JOINED]).isFalse();
    assertThat(firedSystem[LEFT]).isFalse();
    assertThat(memberSystem[LEFT]).isNull();
    assertThat(memberIdSystem[LEFT]).isNull();
    assertThat(isClientSystem[LEFT]).isFalse();
    assertThat(firedSystem[CRASHED]).isFalse();
    assertThat(memberSystem[CRASHED]).isNull();
    assertThat(memberIdSystem[CRASHED]).isNull();
    assertThat(isClientSystem[CRASHED]).isFalse();
    resetArraysForTesting(firedSystem, memberSystem, memberIdSystem, isClientSystem);

    assertThat(firedAdapter[JOINED]).isFalse();
    assertThat(memberAdapter[JOINED]).isNull();
    assertThat(memberIdAdapter[JOINED]).isNull();
    assertThat(isClientAdapter[JOINED]).isFalse();
    assertThat(firedAdapter[LEFT]).isTrue();
    assertThat(memberAdapter[LEFT]).isEqualTo(clientMember);
    assertThat(memberIdAdapter[LEFT]).isEqualTo(clientMemberId);
    assertThat(isClientAdapter[LEFT]).isTrue();
    assertThat(firedAdapter[CRASHED]).isFalse();
    assertThat(memberAdapter[CRASHED]).isNull();
    assertThat(memberIdAdapter[CRASHED]).isNull();
    assertThat(isClientAdapter[CRASHED]).isFalse();
    resetArraysForTesting(firedAdapter, memberAdapter, memberIdAdapter, isClientAdapter);

    // reconnect bridge client
    clientMember = (DistributedMember) vm0.invoke(createBridgeClient);
    clientMemberId = clientMember.getId();

    synchronized (adapter) {
      while (!firedAdapter[JOINED]) {
        adapter.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }
    synchronized (bridgeListener) {
      while (!firedBridge[JOINED]) {
        bridgeListener.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }

    assertArrayFalse(firedSystemDuplicate);
    assertArrayFalse(firedAdapterDuplicate);
    assertArrayFalse(firedBridgeDuplicate);

    assertThat(firedBridge[JOINED]).isTrue();
    assertThat(memberBridge[JOINED]).isEqualTo(clientMember);
    assertThat(memberIdBridge[JOINED]).isEqualTo(clientMemberId);
    assertThat(isClientBridge[JOINED]).isTrue();
    assertThat(firedBridge[LEFT]).isFalse();
    assertThat(memberBridge[LEFT]).isNull();
    assertThat(memberIdBridge[LEFT]).isNull();
    assertThat(isClientBridge[LEFT]).isFalse();
    assertThat(firedBridge[CRASHED]).isFalse();
    assertThat(memberBridge[CRASHED]).isNull();
    assertThat(memberIdBridge[CRASHED]).isNull();
    assertThat(isClientBridge[CRASHED]).isFalse();
    resetArraysForTesting(firedBridge, memberBridge, memberIdBridge, isClientBridge);

    assertThat(firedSystem[JOINED]).isFalse();
    assertThat(memberSystem[JOINED]).isNull();
    assertThat(memberIdSystem[JOINED]).isNull();
    assertThat(isClientSystem[JOINED]).isFalse();
    assertThat(firedSystem[LEFT]).isFalse();
    assertThat(memberSystem[LEFT]).isNull();
    assertThat(memberIdSystem[LEFT]).isNull();
    assertThat(isClientSystem[LEFT]).isFalse();
    assertThat(firedSystem[CRASHED]).isFalse();
    assertThat(memberSystem[CRASHED]).isNull();
    assertThat(memberIdSystem[CRASHED]).isNull();
    assertThat(isClientSystem[CRASHED]).isFalse();
    resetArraysForTesting(firedSystem, memberSystem, memberIdSystem, isClientSystem);

    assertThat(firedAdapter[JOINED]).isTrue();
    assertThat(memberAdapter[JOINED]).isEqualTo(clientMember);
    assertThat(memberIdAdapter[JOINED]).isEqualTo(clientMemberId);
    // assertThat(isClientAdapter[JOINED]).isTrue();
    assertThat(firedAdapter[LEFT]).isFalse();
    assertThat(memberAdapter[LEFT]).isNull();
    assertThat(memberIdAdapter[LEFT]).isNull();
    assertThat(isClientAdapter[LEFT]).isFalse();
    assertThat(firedAdapter[CRASHED]).isFalse();
    assertThat(memberAdapter[CRASHED]).isNull();
    assertThat(memberIdAdapter[CRASHED]).isNull();
    assertThat(isClientAdapter[CRASHED]).isFalse();
    resetArraysForTesting(firedAdapter, memberAdapter, memberIdAdapter, isClientAdapter);

    vm0.invoke(new SerializableRunnable("Wait for client to fully connect") {
      @Override
      public void run() {
        String poolName = getRootRegion().getSubregion(name).getAttributes().getPoolName();
        PoolImpl pool = (PoolImpl) PoolManager.find(poolName);
        waitForClientToFullyConnect(pool);
      }
    });

    // have bridge client disconnect from system
    vm0.invoke(new SerializableRunnable("Disconnect bridge client") {
      @Override
      public void run() {
        closeCache();
        disconnectFromDS();
      }
    });

    synchronized (adapter) {
      while (!firedAdapter[LEFT]) {
        adapter.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }
    synchronized (systemListener) {
      while (!firedSystem[LEFT]) {
        systemListener.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }
    synchronized (bridgeListener) {
      while (!firedBridge[LEFT]) {
        bridgeListener.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }

    assertArrayFalse(firedSystemDuplicate);
    assertArrayFalse(firedAdapterDuplicate);
    assertArrayFalse(firedBridgeDuplicate);

    assertThat(firedBridge[JOINED]).isFalse();
    assertThat(memberBridge[JOINED]).isNull();
    assertThat(memberIdBridge[JOINED]).isNull();
    assertThat(isClientBridge[JOINED]).isFalse();
    assertThat(firedBridge[LEFT]).isTrue();
    assertThat(memberBridge[LEFT]).isEqualTo(clientMember);
    assertThat(memberIdBridge[LEFT]).isEqualTo(clientMemberId);
    assertThat(isClientBridge[LEFT]).isTrue();
    assertThat(firedBridge[CRASHED]).isFalse();
    assertThat(memberBridge[CRASHED]).isNull();
    assertThat(memberIdBridge[CRASHED]).isNull();
    assertThat(isClientBridge[CRASHED]).isFalse();
    resetArraysForTesting(firedBridge, memberBridge, memberIdBridge, isClientBridge);

    assertThat(firedSystem[JOINED]).isFalse();
    assertThat(memberSystem[JOINED]).isNull();
    assertThat(memberIdSystem[JOINED]).isNull();
    assertThat(isClientSystem[JOINED]).isFalse();
    assertThat(firedSystem[LEFT]).isTrue();
    assertThat(memberSystem[LEFT]).isEqualTo(clientMember);
    assertThat(memberIdSystem[LEFT]).isEqualTo(clientMemberId);
    assertThat(isClientSystem[LEFT]).isFalse();
    assertThat(firedSystem[CRASHED]).isFalse();
    assertThat(memberSystem[CRASHED]).isNull();
    assertThat(memberIdSystem[CRASHED]).isNull();
    assertThat(isClientSystem[CRASHED]).isFalse();
    resetArraysForTesting(firedSystem, memberSystem, memberIdSystem, isClientSystem);

    assertThat(firedAdapter[JOINED]).isFalse();
    assertThat(memberAdapter[JOINED]).isNull();
    assertThat(memberIdAdapter[JOINED]).isNull();
    assertThat(isClientAdapter[JOINED]).isFalse();
    assertThat(firedAdapter[LEFT]).isTrue();
    assertThat(memberAdapter[LEFT]).isEqualTo(clientMember);
    assertThat(memberIdAdapter[LEFT]).isEqualTo(clientMemberId);
    // assertThat(isClientAdapter[LEFT]).isTrue();
    assertThat(firedAdapter[CRASHED]).isFalse();
    assertThat(memberAdapter[CRASHED]).isNull();
    assertThat(memberIdAdapter[CRASHED]).isNull();
    assertThat(isClientAdapter[CRASHED]).isFalse();
    resetArraysForTesting(firedAdapter, memberAdapter, memberIdAdapter, isClientAdapter);

    // reconnect bridge client
    clientMember = (DistributedMember) vm0.invoke(createBridgeClient);
    clientMemberId = clientMember.getId();

    synchronized (adapter) {
      while (!firedAdapter[JOINED]) {
        adapter.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }
    synchronized (systemListener) {
      while (!firedSystem[JOINED]) {
        systemListener.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }
    synchronized (bridgeListener) {
      while (!firedBridge[JOINED]) {
        bridgeListener.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }

    assertArrayFalse(firedSystemDuplicate);
    assertArrayFalse(firedAdapterDuplicate);
    assertArrayFalse(firedBridgeDuplicate);

    assertThat(firedBridge[JOINED]).isTrue();
    assertThat(memberBridge[JOINED]).isEqualTo(clientMember);
    assertThat(memberIdBridge[JOINED]).isEqualTo(clientMemberId);
    assertThat(isClientBridge[JOINED]).isTrue();
    assertThat(firedBridge[LEFT]).isFalse();
    assertThat(memberBridge[LEFT]).isNull();
    assertThat(memberIdBridge[LEFT]).isNull();
    assertThat(isClientBridge[LEFT]).isFalse();
    assertThat(firedBridge[CRASHED]).isFalse();
    assertThat(memberBridge[CRASHED]).isNull();
    assertThat(memberIdBridge[CRASHED]).isNull();
    assertThat(isClientBridge[CRASHED]).isFalse();
    resetArraysForTesting(firedBridge, memberBridge, memberIdBridge, isClientBridge);

    assertThat(firedSystem[JOINED]).isTrue();
    assertThat(memberSystem[JOINED]).isEqualTo(clientMember);
    assertThat(memberIdSystem[JOINED]).isEqualTo(clientMemberId);
    assertThat(isClientSystem[JOINED]).isFalse();
    assertThat(firedSystem[LEFT]).isFalse();
    assertThat(memberSystem[LEFT]).isNull();
    assertThat(memberIdSystem[LEFT]).isNull();
    assertThat(isClientSystem[LEFT]).isFalse();
    assertThat(firedSystem[CRASHED]).isFalse();
    assertThat(memberSystem[CRASHED]).isNull();
    assertThat(memberIdSystem[CRASHED]).isNull();
    assertThat(isClientSystem[CRASHED]).isFalse();
    resetArraysForTesting(firedSystem, memberSystem, memberIdSystem, isClientSystem);

    assertThat(firedAdapter[JOINED]).isTrue();
    assertThat(memberAdapter[JOINED]).isEqualTo(clientMember);
    assertThat(memberIdAdapter[JOINED]).isEqualTo(clientMemberId);
    // assertThat(isClientAdapter[JOINED]).isTrue();
    assertThat(firedAdapter[LEFT]).isFalse();
    assertThat(memberAdapter[LEFT]).isNull();
    assertThat(memberIdAdapter[LEFT]).isNull();
    assertThat(isClientAdapter[LEFT]).isFalse();
    assertThat(firedAdapter[CRASHED]).isFalse();
    assertThat(memberAdapter[CRASHED]).isNull();
    assertThat(memberIdAdapter[CRASHED]).isNull();
    assertThat(isClientAdapter[CRASHED]).isFalse();
    resetArraysForTesting(firedAdapter, memberAdapter, memberIdAdapter, isClientAdapter);

    vm0.invoke(new SerializableRunnable("Wait for client to fully connect") {
      @Override
      public void run() {
        String poolName = getRootRegion().getSubregion(name).getAttributes().getPoolName();
        PoolImpl pool = (PoolImpl) PoolManager.find(poolName);
        waitForClientToFullyConnect(pool);
      }
    });

    // close bridge client region with test hook for crash
    ServerConnection.setForceClientCrashEvent(true);

    vm0.invoke(new SerializableRunnable("Close bridge client region") {
      @Override
      public void run() {
        getRootRegion().getSubregion(name).close();
        PoolManager.close();
      }
    });

    synchronized (adapter) {
      while (!firedAdapter[CRASHED]) {
        adapter.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }
    synchronized (bridgeListener) {
      while (!firedBridge[CRASHED]) {
        bridgeListener.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }

    assertArrayFalse(firedSystemDuplicate);
    assertArrayFalse(firedAdapterDuplicate);
    assertArrayFalse(firedBridgeDuplicate);

    assertThat(firedBridge[JOINED]).isFalse();
    assertThat(memberBridge[JOINED]).isNull();
    assertThat(memberIdBridge[JOINED]).isNull();
    assertThat(isClientBridge[JOINED]).isFalse();
    assertThat(firedBridge[LEFT]).isFalse();
    assertThat(memberBridge[LEFT]).isNull();
    assertThat(memberIdBridge[LEFT]).isNull();
    assertThat(isClientBridge[LEFT]).isFalse();
    assertThat(firedBridge[CRASHED]).isTrue();
    assertThat(memberBridge[CRASHED]).isEqualTo(clientMember);
    assertThat(memberIdBridge[CRASHED]).isEqualTo(clientMemberId);
    assertThat(isClientBridge[CRASHED]).isTrue();

    assertThat(firedSystem[JOINED]).isFalse();
    assertThat(memberSystem[JOINED]).isNull();
    assertThat(memberIdSystem[JOINED]).isNull();
    assertThat(isClientSystem[JOINED]).isFalse();
    assertThat(firedSystem[LEFT]).isFalse();
    assertThat(memberSystem[LEFT]).isNull();
    assertThat(memberIdSystem[LEFT]).isNull();
    assertThat(isClientSystem[LEFT]).isFalse();
    assertThat(firedSystem[CRASHED]).isFalse();
    assertThat(memberSystem[CRASHED]).isNull();
    assertThat(memberIdSystem[CRASHED]).isNull();
    assertThat(isClientSystem[CRASHED]).isFalse();

    assertThat(firedAdapter[JOINED]).isFalse();
    assertThat(memberAdapter[JOINED]).isNull();
    assertThat(memberIdAdapter[JOINED]).isNull();
    assertThat(isClientAdapter[JOINED]).isFalse();
    assertThat(firedAdapter[LEFT]).isFalse();
    assertThat(memberAdapter[LEFT]).isNull();
    assertThat(memberIdAdapter[LEFT]).isNull();
    assertThat(isClientAdapter[LEFT]).isFalse();
    assertThat(firedAdapter[CRASHED]).isTrue();
    assertThat(memberAdapter[CRASHED]).isEqualTo(clientMember);
    assertThat(memberIdAdapter[CRASHED]).isEqualTo(clientMemberId);
    assertThat(isClientAdapter[CRASHED]).isTrue();
  }

  /**
   * Waits for client to create an expected number of connections. Note: This probably won't work if
   * the pool has more than one Endpoint.
   */
  private void waitForClientToFullyConnect(PoolImpl pool) {
    long failMillis = System.currentTimeMillis() + JOIN_FAIL_MILLIS;
    boolean fullyConnected = false;
    while (!fullyConnected) {
      pause(100);
      fullyConnected = pool.getConnectionCount() >= pool.getMinConnections();
      assertThat(System.currentTimeMillis()).isLessThan(failMillis);
    }
  }

  /**
   * Resets all elements of arrays used for listener testing. Boolean values are reset to false.
   * String values are reset to null.
   */
  private void resetArraysForTesting(boolean[] fired, DistributedMember[] member, String[] memberId,
      boolean[] isClient) {
    for (int i = 0; i < fired.length; i++) {
      fired[i] = false;
      member[i] = null;
      memberId[i] = null;
      isClient[i] = false;
    }
  }

  /**
   * Asserts all elements in the array are false.
   */
  private void assertArrayFalse(boolean[] array) {
    assertArrayFalse(null, array);
  }

  private void assertArrayFalse(String msg, boolean[] array) {
    for (boolean value : array) {
      if (msg == null) {
        assertThat(value).isFalse();
      } else {
        assertThat(value).as(msg).isFalse();
      }
    }
  }

  private void assertArrayTrue(String msg, boolean[] array) {
    for (int i = 0; i < array.length; i++) {
      if (msg == null) {
        assertThat(array[i]).isTrue();
      } else {
        assertThat(array[i]).as(msg).isTrue();
      }
    }
  }

  /**
   * Tests notification of events for bridge server in system bridge client process.
   */
  @Test
  public void testServerEventsInPeerSystem() throws Exception {
    boolean[] firedSystem = new boolean[3];
    DistributedMember[] memberSystem = new DistributedMember[3];
    String[] memberIdSystem = new String[3];
    boolean[] isClientSystem = new boolean[3];

    boolean[] firedAdapter = new boolean[3];
    DistributedMember[] memberAdapter = new DistributedMember[3];
    String[] memberIdAdapter = new String[3];
    boolean[] isClientAdapter = new boolean[3];

    boolean[] firedBridge = new boolean[3];
    DistributedMember[] memberBridge = new DistributedMember[3];
    String[] memberIdBridge = new String[3];
    boolean[] isClientBridge = new boolean[3];

    boolean[] firedSystemDuplicate = new boolean[3];
    boolean[] firedAdapterDuplicate = new boolean[3];
    boolean[] firedBridgeDuplicate = new boolean[3];

    MembershipListener systemListener = new MembershipListener() {
      @Override
      public synchronized void memberJoined(MembershipEvent event) {
        firedSystemDuplicate[JOINED] = firedSystem[JOINED];
        firedSystem[JOINED] = true;
        memberSystem[JOINED] = event.getDistributedMember();
        memberIdSystem[JOINED] = event.getMemberId();
        notifyAll();
      }

      @Override
      public synchronized void memberLeft(MembershipEvent event) {
        firedSystemDuplicate[LEFT] = firedSystem[LEFT];
        firedSystem[LEFT] = true;
        memberSystem[LEFT] = event.getDistributedMember();
        memberIdSystem[LEFT] = event.getMemberId();
        notifyAll();
      }

      @Override
      public synchronized void memberCrashed(MembershipEvent event) {
        firedSystemDuplicate[CRASHED] = firedSystem[CRASHED];
        firedSystem[CRASHED] = true;
        memberSystem[CRASHED] = event.getDistributedMember();
        memberIdSystem[CRASHED] = event.getMemberId();
        notifyAll();
      }
    };

    UniversalMembershipListenerAdapter adapter = new UniversalMembershipListenerAdapter() {
      @Override
      public synchronized void memberJoined(MembershipEvent event) {
        firedAdapterDuplicate[JOINED] = firedAdapter[JOINED];
        firedAdapter[JOINED] = true;
        memberAdapter[JOINED] = event.getDistributedMember();
        memberIdAdapter[JOINED] = event.getMemberId();
        if (event instanceof AdaptedMembershipEvent) {
          isClientAdapter[JOINED] = ((AdaptedMembershipEvent) event).isClient();
        }
        notifyAll();
      }

      @Override
      public synchronized void memberLeft(MembershipEvent event) {
        firedAdapterDuplicate[LEFT] = firedAdapter[LEFT];
        firedAdapter[LEFT] = true;
        memberAdapter[LEFT] = event.getDistributedMember();
        memberIdAdapter[LEFT] = event.getMemberId();
        if (event instanceof AdaptedMembershipEvent) {
          isClientAdapter[LEFT] = ((AdaptedMembershipEvent) event).isClient();
        }
        notifyAll();
      }

      @Override
      public synchronized void memberCrashed(MembershipEvent event) {
        firedAdapterDuplicate[CRASHED] = firedAdapter[CRASHED];
        firedAdapter[CRASHED] = true;
        memberAdapter[CRASHED] = event.getDistributedMember();
        memberIdAdapter[CRASHED] = event.getMemberId();
        if (event instanceof AdaptedMembershipEvent) {
          isClientAdapter[CRASHED] = ((AdaptedMembershipEvent) event).isClient();
        }
        notifyAll();
      }
    };

    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    String name = this.getUniqueName();
    int[] ports = new int[] {getRandomAvailablePort(SOCKET)};
    assertThat(ports[0] != 0).isTrue();

    // create BridgeServer in controller vm...
    getSystem();
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.LOCAL);
    Region region = createRegion(name, factory.create());
    assertThat(region).isNotNull();
    assertThat(getRootRegion().getSubregion(name)).isNotNull();

    ports[0] = startBridgeServer(0);
    assertThat(ports[0] != 0).isTrue();

    DistributedMember serverMember = getMemberId();
    String serverMemberId = serverMember.getId();
    Properties serverProperties = getSystem().getProperties();

    serverProperties.remove(CLUSTER_SSL_ENABLED);
    serverProperties.remove(CLUSTER_SSL_CIPHERS);
    serverProperties.remove(CLUSTER_SSL_PROTOCOLS);
    serverProperties.remove(CLUSTER_SSL_REQUIRE_AUTHENTICATION);

    InternalCache cache = getInternalCache();
    ManagementService service = ManagementService.getExistingManagementService(cache);
    // register the system listener
    service.addMembershipListener(systemListener);

    // register the universal adapter.
    adapter.registerMembershipListener(service);

    // create BridgeServer in vm0...
    SerializableCallable createPeerCache = new SerializableCallable("Create Peer Cache") {
      @Override
      public Object call() {
        getSystem(serverProperties);
        AttributesFactory factory = new AttributesFactory();
        factory.setScope(Scope.LOCAL);
        Region region = createRegion(name, factory.create());
        assertThat(region).isNotNull();
        assertThat(getRootRegion().getSubregion(name)).isNotNull();
        return basicGetSystem().getDistributedMember();
      }
    };

    DistributedMember peerMember = (DistributedMember) vm0.invoke(createPeerCache);
    String peerMemberId = peerMember.getId();

    synchronized (systemListener) {
      if (!firedSystem[JOINED]) {
        systemListener.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }
    synchronized (adapter) {
      if (!firedAdapter[JOINED]) {
        adapter.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }

    assertArrayFalse(firedSystemDuplicate);

    assertThat(firedSystem[JOINED]).isTrue();
    assertThat(memberSystem[JOINED]).isEqualTo(peerMember);
    assertThat(memberIdSystem[JOINED]).isEqualTo(peerMemberId);
    assertThat(isClientSystem[JOINED]).isFalse();
    assertThat(firedSystem[LEFT]).isFalse();
    assertThat(memberSystem[LEFT]).isNull();
    assertThat(memberIdSystem[LEFT]).isNull();
    assertThat(isClientSystem[LEFT]).isFalse();
    assertThat(firedSystem[CRASHED]).isFalse();
    assertThat(memberSystem[CRASHED]).isNull();
    assertThat(memberIdSystem[CRASHED]).isNull();
    assertThat(isClientSystem[CRASHED]).isFalse();

    resetArraysForTesting(firedSystem, memberSystem, memberIdSystem, isClientSystem);

    assertThat(firedAdapter[JOINED]).isTrue();
    assertThat(memberAdapter[JOINED]).isNotNull();
    assertThat(memberIdAdapter[JOINED]).isNotNull();
    assertThat(memberAdapter[JOINED]).isEqualTo(peerMember);
    assertThat(memberIdAdapter[JOINED]).isEqualTo(peerMemberId);
    assertThat(isClientAdapter[JOINED]).isFalse();
    assertThat(firedAdapter[LEFT]).isFalse();
    assertThat(memberAdapter[LEFT]).isNull();
    assertThat(memberIdAdapter[LEFT]).isNull();
    assertThat(isClientAdapter[LEFT]).isFalse();
    assertThat(firedAdapter[CRASHED]).isFalse();
    assertThat(memberAdapter[CRASHED]).isNull();
    assertThat(memberIdAdapter[CRASHED]).isNull();
    assertThat(isClientAdapter[CRASHED]).isFalse();

    resetArraysForTesting(firedAdapter, memberAdapter, memberIdAdapter, isClientAdapter);

    addIgnoredException(ServerConnectivityException.class.getName());
    addIgnoredException(IOException.class.getName());

    vm0.invoke(new SerializableRunnable("Disconnect Peer server") {
      @Override
      public void run() {
        disconnectFromDS();
      }
    });

    synchronized (systemListener) {
      if (!firedSystem[LEFT]) {
        systemListener.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }
    synchronized (adapter) {
      if (!firedAdapter[LEFT]) {
        adapter.wait(ASYNC_EVENT_WAIT_MILLIS);
      }
    }

    // done with IgnoredExceptions

    assertArrayFalse(firedSystemDuplicate);
    assertArrayFalse(firedAdapterDuplicate);

    assertThat(firedSystem[JOINED]).isFalse();
    assertThat(memberSystem[JOINED]).isNull();
    assertThat(memberIdSystem[JOINED]).isNull();
    assertThat(isClientSystem[JOINED]).isFalse();
    assertThat(firedSystem[LEFT]).isTrue();
    assertThat(memberSystem[LEFT]).isEqualTo(peerMember);
    assertThat(memberIdSystem[LEFT]).isEqualTo(peerMemberId);
    assertThat(isClientSystem[LEFT]).isFalse();
    assertThat(firedSystem[CRASHED]).isFalse();
    assertThat(memberSystem[CRASHED]).isNull();
    assertThat(memberIdSystem[CRASHED]).isNull();
    assertThat(isClientSystem[CRASHED]).isFalse();

    resetArraysForTesting(firedSystem, memberSystem, memberIdSystem, isClientSystem);

    assertThat(firedAdapter[JOINED]).isFalse();
    assertThat(memberIdAdapter[JOINED]).isNull();
    assertThat(isClientAdapter[JOINED]).isFalse();
    // LEFT fired by System listener
    assertThat(firedAdapter[LEFT]).isTrue();
    assertThat(memberAdapter[LEFT]).isEqualTo(peerMember);
    assertThat(memberIdAdapter[LEFT]).isEqualTo(peerMemberId);
    assertThat(isClientAdapter[LEFT]).isFalse();

    // There won't be an adapter crashed event because since the two VMs
    // are in the same distributed system, and the server's real member
    // id is used now. In this case, two events are sent - one from
    // jgroups (memberDeparted), and one from the server (a memberCrashed).
    // The memberCrashed event is deemed a duplicate and not sent - see
    // UniversalMembershipListenerAdapter.MembershipListener.isDuplicate

    assertThat(firedAdapter[CRASHED]).isFalse();
    assertThat(memberAdapter[CRASHED]).isNull();
    assertThat(memberIdAdapter[CRASHED]).isNull();
    assertThat(isClientAdapter[CRASHED]).isFalse();

    resetArraysForTesting(firedAdapter, memberAdapter, memberIdAdapter, isClientAdapter);
  }

  /**
   * Tests notification of events for bridge server in system bridge client process.
   */
  @Test
  public void testServerEventsInLonerClient() throws Exception {
    boolean[] firedAdapter = new boolean[3];
    DistributedMember[] memberAdapter = new DistributedMember[3];
    String[] memberIdAdapter = new String[3];
    boolean[] isClientAdapter = new boolean[3];

    boolean[] firedBridge = new boolean[3];
    DistributedMember[] memberBridge = new DistributedMember[3];
    String[] memberIdBridge = new String[3];
    boolean[] isClientBridge = new boolean[3];

    boolean[] firedAdapterDuplicate = new boolean[3];
    boolean[] firedBridgeDuplicate = new boolean[3];

    UniversalMembershipListenerAdapter adapter = new UniversalMembershipListenerAdapter() {
      @Override
      public synchronized void memberJoined(MembershipEvent event) {
        firedAdapterDuplicate[JOINED] = firedAdapter[JOINED];
        firedAdapter[JOINED] = true;
        memberAdapter[JOINED] = event.getDistributedMember();
        memberIdAdapter[JOINED] = event.getMemberId();
        if (event instanceof AdaptedMembershipEvent) {
          isClientAdapter[JOINED] = ((AdaptedMembershipEvent) event).isClient();
        }
        notifyAll();
      }

      @Override
      public synchronized void memberLeft(MembershipEvent event) {
        firedAdapterDuplicate[LEFT] = firedAdapter[LEFT];
        firedAdapter[LEFT] = true;
        memberAdapter[LEFT] = event.getDistributedMember();
        memberIdAdapter[LEFT] = event.getMemberId();
        if (event instanceof AdaptedMembershipEvent) {
          isClientAdapter[LEFT] = ((AdaptedMembershipEvent) event).isClient();
        }
        notifyAll();
      }

      @Override
      public synchronized void memberCrashed(MembershipEvent event) {
        firedAdapterDuplicate[CRASHED] = firedAdapter[CRASHED];
        firedAdapter[CRASHED] = true;
        memberAdapter[CRASHED] = event.getDistributedMember();
        memberIdAdapter[CRASHED] = event.getMemberId();
        if (event instanceof AdaptedMembershipEvent) {
          isClientAdapter[CRASHED] = ((AdaptedMembershipEvent) event).isClient();
        }
        notifyAll();
      }
    };

    ClientMembershipListener bridgeListener = new ClientMembershipListener() {
      @Override
      public synchronized void memberJoined(ClientMembershipEvent event) {
        firedBridgeDuplicate[JOINED] = firedBridge[JOINED];
        firedBridge[JOINED] = true;
        memberBridge[JOINED] = event.getMember();
        memberIdBridge[JOINED] = event.getMemberId();
        isClientBridge[JOINED] = event.isClient();
        notifyAll();
      }

      @Override
      public synchronized void memberLeft(ClientMembershipEvent event) {
        firedBridgeDuplicate[LEFT] = firedBridge[LEFT];
        firedBridge[LEFT] = true;
        memberBridge[LEFT] = event.getMember();
        memberIdBridge[LEFT] = event.getMemberId();
        isClientBridge[LEFT] = event.isClient();
        notifyAll();
      }

      @Override
      public synchronized void memberCrashed(ClientMembershipEvent event) {
        firedBridgeDuplicate[CRASHED] = firedBridge[CRASHED];
        firedBridge[CRASHED] = true;
        memberBridge[CRASHED] = event.getMember();
        memberIdBridge[CRASHED] = event.getMemberId();
        isClientBridge[CRASHED] = event.isClient();
        notifyAll();
      }
    };

    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    String name = getUniqueName();
    int[] ports = new int[] {getRandomAvailablePort(SOCKET)};
    assertThat(ports[0] != 0).isTrue();

    Properties config = new Properties();
    config.put(MCAST_PORT, "0");
    config.put(LOCATORS, "");
    config.setProperty(ENABLE_NETWORK_PARTITION_DETECTION, "false");
    getSystem(config);

    // register the bridge listener
    ClientMembership.registerClientMembershipListener(bridgeListener);

    // create CacheServer in vm0...
    SerializableCallable createBridgeServer = new SerializableCallable("Create BridgeServer") {
      @Override
      public Object call() {
        getSystem();
        AttributesFactory factory = new AttributesFactory();
        factory.setScope(Scope.LOCAL);
        Region region = createRegion(name, factory.create());
        assertThat(region).isNotNull();
        assertThat(getRootRegion().getSubregion(name)).isNotNull();

        try {
          serverPort = startBridgeServer(ports[0]);
        } catch (IOException e) {
          throw new AssertionError(e);
        }

        return basicGetSystem().getDistributedMember();
      }
    };

    vm0.invoke(createBridgeServer);

    // gather details for later creation of pool...
    assertThat((int) vm0.invoke("getServerPort", () -> serverPort)).isEqualTo(ports[0]);

    // create region which connects to bridge server
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.LOCAL);
    configureConnectionPool(factory, getServerHostName(host), ports, false, -1, -1, null);
    createRegion(name, factory.create());
    assertThat(getRootRegion().getSubregion(name)).isNotNull();

    await("wait for join").atMost(ASYNC_EVENT_WAIT_MILLIS, MILLISECONDS).until(() -> {
      synchronized (adapter) {
        return firedAdapter[JOINED];
      }
    });
    await("wait for join").atMost(ASYNC_EVENT_WAIT_MILLIS, MILLISECONDS).until(() -> {
      synchronized (bridgeListener) {
        return firedBridge[JOINED];
      }
    });

    assertArrayFalse(firedAdapterDuplicate);
    assertArrayFalse(firedBridgeDuplicate);

    assertThat(firedBridge[JOINED]).isTrue();
    assertThat(memberBridge[JOINED]).isNotNull();
    assertThat(memberIdBridge[JOINED]).isNotNull();
    assertThat(isClientBridge[JOINED]).isFalse();
    assertThat(firedBridge[LEFT]).isFalse();
    assertThat(memberBridge[LEFT]).isNull();
    assertThat(memberIdBridge[LEFT]).isNull();
    assertThat(isClientBridge[LEFT]).isFalse();
    assertThat(firedBridge[CRASHED]).isFalse();
    assertThat(memberBridge[CRASHED]).isNull();
    assertThat(memberIdBridge[CRASHED]).isNull();
    assertThat(isClientBridge[CRASHED]).isFalse();

    resetArraysForTesting(firedBridge, memberBridge, memberIdBridge, isClientBridge);

    assertThat(firedAdapter[JOINED]).isTrue();
    assertThat(memberIdAdapter[JOINED]).isNotNull();
    assertThat(isClientAdapter[JOINED]).isFalse();
    assertThat(firedAdapter[LEFT]).isFalse();
    assertThat(memberAdapter[LEFT]).isNull();
    assertThat(memberIdAdapter[LEFT]).isNull();
    assertThat(isClientAdapter[LEFT]).isFalse();
    assertThat(firedAdapter[CRASHED]).isFalse();
    assertThat(memberAdapter[CRASHED]).isNull();
    assertThat(memberIdAdapter[CRASHED]).isNull();
    assertThat(isClientAdapter[CRASHED]).isFalse();

    resetArraysForTesting(firedAdapter, memberAdapter, memberIdAdapter, isClientAdapter);

    String poolName = getRootRegion().getSubregion(name).getAttributes().getPoolName();
    PoolImpl pool = (PoolImpl) PoolManager.find(poolName);
    waitForClientToFullyConnect(pool);

    addIgnoredException(IOException.class.getName());
    addIgnoredException(ConnectException.class.getName());

    vm0.invoke(new SerializableRunnable("Disconnect bridge server") {
      @Override
      public void run() {
        closeCache();
      }
    });

    await("wait for server to leave").atMost(ASYNC_EVENT_WAIT_MILLIS, MILLISECONDS).until(() -> {
      synchronized (adapter) {
        return firedAdapter[LEFT] || firedAdapter[CRASHED];
      }
    });
    await("wait for server to leave").atMost(ASYNC_EVENT_WAIT_MILLIS, MILLISECONDS).until(() -> {
      synchronized (bridgeListener) {
        return firedBridge[LEFT] || firedBridge[CRASHED];
      }
    });

    // done with IgnoredExceptions

    assertArrayFalse(firedAdapterDuplicate);
    assertArrayFalse(firedBridgeDuplicate);

    assertThat(firedBridge[JOINED]).isFalse();
    assertThat(memberIdBridge[JOINED]).isNull();
    assertThat(memberBridge[JOINED]).isNull();
    assertThat(isClientBridge[JOINED]).isFalse();
    assertThat(firedBridge[LEFT]).isFalse();
    assertThat(memberBridge[LEFT]).isNull();
    assertThat(memberIdBridge[LEFT]).isNull();
    assertThat(isClientBridge[LEFT]).isFalse();
    assertThat(firedBridge[CRASHED]).isTrue();
    assertThat(memberBridge[CRASHED]).isNotNull();
    assertThat(memberIdBridge[CRASHED]).isNotNull();
    assertThat(isClientBridge[CRASHED]).isFalse();

    resetArraysForTesting(firedBridge, memberBridge, memberIdBridge, isClientBridge);

    assertThat(firedAdapter[JOINED]).isFalse();
    assertThat(memberAdapter[JOINED]).isNull();
    assertThat(memberIdAdapter[JOINED]).isNull();
    assertThat(isClientAdapter[JOINED]).isFalse();
    assertThat(firedAdapter[LEFT]).isFalse();
    assertThat(memberAdapter[LEFT]).isNull();
    assertThat(memberIdAdapter[LEFT]).isNull();
    assertThat(isClientAdapter[LEFT]).isFalse();
    // CRASHED fired by Bridge listener
    assertThat(firedAdapter[CRASHED]).isTrue();
    assertThat(memberAdapter[CRASHED]).isNotNull();
    assertThat(memberIdAdapter[CRASHED]).isNotNull();
    assertThat(isClientAdapter[CRASHED]).isFalse();

    resetArraysForTesting(firedAdapter, memberAdapter, memberIdAdapter, isClientAdapter);

    // reconnect bridge client to test for crashed event
    vm0.invoke(createBridgeServer);

    // gather details for later creation of pool...
    assertThat((int) vm0.invoke(() -> serverPort)).isEqualTo(ports[0]);

    await("wait for join").atMost(ASYNC_EVENT_WAIT_MILLIS, MILLISECONDS).until(() -> {
      synchronized (adapter) {
        return firedAdapter[JOINED];
      }
    });
    await("wait for join").atMost(ASYNC_EVENT_WAIT_MILLIS, MILLISECONDS).until(() -> {
      synchronized (bridgeListener) {
        return firedBridge[JOINED];
      }
    });

    assertArrayFalse(firedAdapterDuplicate);
    assertArrayFalse(firedBridgeDuplicate);

    assertThat(firedBridge[JOINED]).isTrue();
    assertThat(memberBridge[JOINED]).isNotNull();
    assertThat(memberIdBridge[JOINED]).isNotNull();
    assertThat(isClientBridge[JOINED]).isFalse();
    assertThat(firedBridge[LEFT]).isFalse();
    assertThat(memberBridge[LEFT]).isNull();
    assertThat(memberIdBridge[LEFT]).isNull();
    assertThat(isClientBridge[LEFT]).isFalse();
    assertThat(firedBridge[CRASHED]).isFalse();
    assertThat(memberBridge[CRASHED]).isNull();
    assertThat(memberIdBridge[CRASHED]).isNull();
    assertThat(isClientBridge[CRASHED]).isFalse();

    resetArraysForTesting(firedBridge, memberBridge, memberIdBridge, isClientBridge);

    assertThat(firedAdapter[JOINED]).isTrue();
    assertThat(memberAdapter[JOINED]).isNotNull();
    assertThat(memberIdAdapter[JOINED]).isNotNull();
    assertThat(isClientAdapter[JOINED]).isFalse();
    assertThat(firedAdapter[LEFT]).isFalse();
    assertThat(memberAdapter[LEFT]).isNull();
    assertThat(memberIdAdapter[LEFT]).isNull();
    assertThat(isClientAdapter[LEFT]).isFalse();
    assertThat(firedAdapter[CRASHED]).isFalse();
    assertThat(memberAdapter[CRASHED]).isNull();
    assertThat(memberIdAdapter[CRASHED]).isNull();
    assertThat(isClientAdapter[CRASHED]).isFalse();

    resetArraysForTesting(firedAdapter, memberAdapter, memberIdAdapter, isClientAdapter);
  }

  private static InternalCache getInternalCache() {
    InternalCache cache = (InternalCache) CacheFactory.getAnyInstance();
    assertThat(cache).isNotNull();
    return cache;
  }

  private static class MembershipNotification implements Serializable {

    private final AtomicBoolean notified = new AtomicBoolean();
    private final AtomicReference<DistributedMember> member = new AtomicReference<>();
    private final AtomicReference<String> memberId = new AtomicReference<>();
    private final AtomicBoolean client = new AtomicBoolean();

    void notify(final MembershipEvent event) {
      validateNotNotified();

      notified.set(true);
      member.set(event.getDistributedMember());
      memberId.set(event.getMemberId());
    }

    void notify(final AdaptedMembershipEvent event) {
      validateNotNotified();

      notified.set(true);
      member.set(event.getDistributedMember());
      memberId.set(event.getMemberId());
      client.set(event.isClient());
    }

    void notify(final ClientMembershipEvent event) {
      validateNotNotified();

      notified.set(true);
      member.set(event.getMember());
      memberId.set(event.getMemberId());
      client.set(event.isClient());
    }

    void reset() {
      notified.set(false);
      member.set(null);
      memberId.set(null);
      client.set(false);
    }

    void awaitNotification(final long timeout, final TimeUnit unit) {
      await().atMost(timeout, unit).until(() -> notified.get());
    }

    void awaitWithoutNotification(final long timeout, final TimeUnit unit) {
      try {
        awaitNotification(timeout, unit);
      } catch (ConditionTimeoutException expected) {
        // do nothing
      }
    }

    void validate(DistributedMember clientJoined) {
      assertThat(notified.get()).isTrue();
      assertThat(member.get()).isEqualTo(clientJoined);
      assertThat(memberId.get()).isEqualTo(clientJoined.getId());
    }

    void validateNotNotified() {
      assertThat(notified.get()).isFalse();
      assertThat(member.get()).isNull();
      assertThat(memberId.get()).isNull();
      assertThat(client.get()).isFalse();
    }
  }

  private static class FakeDistributedMember implements DistributedMember {

    private String host;

    FakeDistributedMember(String host) {
      this.host = host;
    }

    @Override
    public String getName() {
      return "";
    }

    @Override
    public String getHost() {
      return this.host;
    }

    @Override
    public Set<Role> getRoles() {
      return Collections.emptySet();
    }

    @Override
    public int getProcessId() {
      return 0;
    }

    @Override
    public String getId() {
      return this.host;
    }

    @Override
    public int compareTo(DistributedMember o) {
      if ((o == null) || !(o instanceof FakeDistributedMember)) {
        throw new InternalGemFireException("Invalidly comparing TestDistributedMember to " + o);
      }

      FakeDistributedMember fakeDistributedMember = (FakeDistributedMember) o;
      return getHost().compareTo(fakeDistributedMember.getHost());
    }

    @Override
    public boolean equals(Object obj) {
      return (obj != null) && obj instanceof FakeDistributedMember
          && compareTo((FakeDistributedMember) obj) == 0;
    }

    @Override
    public int hashCode() {
      return getHost().hashCode();
    }

    @Override
    public DurableClientAttributes getDurableClientAttributes() {
      return null;
    }

    @Override
    public List<String> getGroups() {
      return Collections.emptyList();
    }
  }
}
