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

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.stream.Collectors.toList;
import static org.apache.geode.distributed.ConfigurationProperties.LOCATORS;
import static org.apache.geode.distributed.ConfigurationProperties.MCAST_PORT;
import static org.apache.geode.distributed.ConfigurationProperties.NAME;
import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_CLIENT_AUTHENTICATOR;
import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_CLIENT_AUTH_INIT;
import static org.apache.geode.distributed.ConfigurationProperties.SERIALIZABLE_OBJECT_FILTER;
import static org.apache.geode.test.awaitility.GeodeAwaitility.getTimeout;
import static org.apache.geode.test.dunit.IgnoredException.addIgnoredException;
import static org.apache.geode.test.dunit.VM.getController;
import static org.apache.geode.test.dunit.VM.getVM;
import static org.apache.geode.test.dunit.VM.toArray;
import static org.apache.geode.util.internal.GeodeGlossary.GEMFIRE_PREFIX;
import static org.apache.geode.util.internal.UncheckedUtils.uncheckedCast;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;

import java.io.IOException;
import java.io.Serializable;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.IntStream;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.cache.CacheClosedException;
import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.Scope;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.apache.geode.cache.client.Pool;
import org.apache.geode.cache.client.PoolFactory;
import org.apache.geode.cache.client.PoolManager;
import org.apache.geode.cache.client.internal.InternalClientCache;
import org.apache.geode.cache.execute.Function;
import org.apache.geode.cache.execute.FunctionContext;
import org.apache.geode.cache.execute.FunctionException;
import org.apache.geode.cache.execute.FunctionInvocationTargetException;
import org.apache.geode.cache.execute.FunctionService;
import org.apache.geode.cache.execute.RegionFunctionContext;
import org.apache.geode.cache.execute.ResultCollector;
import org.apache.geode.cache.server.CacheServer;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.cache.execute.util.TypedFunctionService;
import org.apache.geode.security.templates.DummyAuthenticator;
import org.apache.geode.security.templates.UserPasswordAuthInit;
import org.apache.geode.test.dunit.AsyncInvocation;
import org.apache.geode.test.dunit.IgnoredException;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.rules.CacheRule;
import org.apache.geode.test.dunit.rules.ClientCacheRule;
import org.apache.geode.test.dunit.rules.DistributedRestoreSystemProperties;
import org.apache.geode.test.dunit.rules.DistributedRule;
import org.apache.geode.test.junit.categories.FunctionServiceTest;

@Category(FunctionServiceTest.class)
@SuppressWarnings("serial")
public class DistributedRegionFunctionExecutionDUnitTest implements Serializable {

  private static final AtomicReference<Region<?, ?>> REGION = new AtomicReference<>();
  private static final AtomicReference<CountDownLatch> LATCH = new AtomicReference<>();

  private final Set<String> filter = new HashSet<>();

  private String regionName;
  private String poolName;

  private VM empty;
  private VM normal;
  private VM replicate1;
  private VM replicate2;
  private VM replicate3;

  @Rule
  public CacheRule cacheRule = new CacheRule();

  @Rule
  public ClientCacheRule clientCacheRule = new ClientCacheRule();

  @Rule
  public DistributedRestoreSystemProperties restoreSystemProperties =
      new DistributedRestoreSystemProperties();

  @Before
  public void setUp() throws Exception {
    empty = getController();
    replicate1 = getVM(0);
    replicate2 = getVM(1);
    replicate3 = getVM(2);
    normal = getVM(3);

    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      // zero is no-op unless test replaces the reference
      vm.invoke(() -> LATCH.set(new CountDownLatch(0)));
    }

    regionName = getClass().getSimpleName() + "_region";
    poolName = getClass().getSimpleName() + "_pool";

    for (int i = 100; i < 120; i++) {
      filter.add("execKey-" + i);
    }
  }

  @After
  public void tearDown() {
    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> LATCH.get().countDown());
    }
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyEmpty() {
    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    empty.invoke(() -> createRegion(DataPolicy.EMPTY));
    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    for (VM vm : toArray(replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    empty.invoke(() -> {
      populateRegion(200);

      executeDistributedRegionFunction();
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyEmpty_SendException() {
    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    empty.invoke(() -> createRegion(DataPolicy.EMPTY));
    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    for (VM vm : toArray(replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    empty.invoke(() -> {
      populateRegion(200);

      executeResultWithExceptionFunction();
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyEmpty_NoLastResult() {
    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    empty.invoke(() -> createRegion(DataPolicy.EMPTY));
    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    for (VM vm : toArray(replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    empty.invoke(() -> {
      populateRegion(200);

      executeNoLastResultFunction();
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyNormal() {
    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    empty.invoke(() -> createRegion(DataPolicy.EMPTY));
    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    for (VM vm : toArray(replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    empty.invoke(() -> populateRegion(200));

    normal.invoke(() -> {
      Throwable thrown = catchThrowable(this::executeDistributedRegionFunction);

      assertThat(thrown)
          .isInstanceOf(FunctionException.class)
          .hasMessage("Function execution on region with DataPolicy.NORMAL is not supported");
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyReplicate() {
    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    empty.invoke(() -> createRegion(DataPolicy.EMPTY));
    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    for (VM vm : toArray(replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    empty.invoke(() -> populateRegion(200));

    replicate1.invoke(this::executeDistributedRegionFunction);
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyReplicateNotTimedOut()
      throws Exception {
    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> {
        createCache(getDistributedSystemProperties());

        LATCH.set(new CountDownLatch(1));

        FunctionService.registerFunction(new DistributedRegionFunction());
        FunctionService.registerFunction(new LongRunningFunction(LATCH.get()));
      });
    }

    empty.invoke(() -> createRegion(DataPolicy.EMPTY));
    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    for (VM vm : toArray(replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    empty.invoke(() -> populateRegion(200));

    AsyncInvocation<Void> executeFunctionInReplicate1 = replicate1.invokeAsync(() -> {
      ResultCollector<String, List<String>> resultCollector = TypedFunctionService
          .<Void, String, List<String>>onRegion(getRegion())
          .withFilter(filter)
          .execute(LongRunningFunction.class.getSimpleName(), getTimeout().toMillis(),
              MILLISECONDS);

      assertThat(resultCollector.getResult().get(0))
          .isEqualTo("LongRunningFunction completed");
    });

    // how long LongRunningFunction runs for is now controlled here:
    Thread.sleep(2000);

    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> {
        LATCH.get().countDown();
      });
    }

    executeFunctionInReplicate1.await();
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyReplicateTimedOut() {
    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> {
        createCache(getDistributedSystemProperties());

        LATCH.set(new CountDownLatch(1));

        FunctionService.registerFunction(new DistributedRegionFunction());
        FunctionService.registerFunction(new LongRunningFunction(LATCH.get()));
      });
    }

    empty.invoke(() -> createRegion(DataPolicy.EMPTY));
    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    for (VM vm : toArray(replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    empty.invoke(() -> populateRegion(200));

    replicate1.invoke(() -> {
      Throwable thrown = catchThrowable(() -> {
        TypedFunctionService
            .<Void, String, List<String>>onRegion(getRegion())
            .withFilter(filter)
            .execute(LongRunningFunction.class.getSimpleName(), 1000, MILLISECONDS);
      });

      assertThat(thrown)
          .hasCauseInstanceOf(FunctionException.class)
          .hasMessageContaining("All results not received in time provided");
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyReplicate_SendException() {
    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    empty.invoke(() -> createRegion(DataPolicy.EMPTY));
    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    for (VM vm : toArray(replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    empty.invoke(() -> populateRegion(200));

    replicate1.invoke(this::executeResultWithExceptionFunction);
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyReplicate_NoLastResult() {
    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    empty.invoke(() -> createRegion(DataPolicy.EMPTY));
    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    for (VM vm : toArray(replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    empty.invoke(() -> populateRegion(200));

    replicate1.invoke(this::executeNoLastResultFunction);
  }

  @Test
  public void testDistributedRegionFunctionExecutionWithFunctionInvocationTargetException() {
    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    empty.invoke(() -> createRegion(DataPolicy.EMPTY));
    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    for (VM vm : toArray(replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> registerThrowsFunctionInvocationTargetExceptionFunction(true, 5));
    }

    empty.invoke(() -> populateRegion(200));

    replicate1.invoke(() -> {
      try (IgnoredException ie = addIgnoredException("I have been thrown")) {
        executeFunctionFunctionInvocationTargetException();
      }
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionWithFunctionInvocationTargetException_WithoutHA() {
    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    empty.invoke(() -> createRegion(DataPolicy.EMPTY));
    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    for (VM vm : toArray(replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> registerThrowsFunctionInvocationTargetExceptionFunction(false, 0));
    }

    empty.invoke(() -> populateRegion(200));

    replicate1.invoke(() -> {
      try (IgnoredException ie = addIgnoredException("I have been thrown")) {
        executeFunctionFunctionInvocationTargetExceptionWithoutHA();
      }
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionWithFunctionInvocationTargetExceptionForEmptyDataPolicy() {
    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    empty.invoke(() -> createRegion(DataPolicy.EMPTY));
    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    for (VM vm : toArray(replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> registerThrowsFunctionInvocationTargetExceptionFunction(true, 5));
    }

    empty.invoke(() -> populateRegion(200));

    empty.invoke(() -> {
      try (IgnoredException ie = addIgnoredException("I have been thrown")) {
        executeFunctionFunctionInvocationTargetException();
      }
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionWithFunctionInvocationTargetExceptionForEmptyDataPolicy_WithoutHA() {
    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    empty.invoke(() -> createRegion(DataPolicy.EMPTY));
    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    for (VM vm : toArray(replicate1, replicate2, replicate3)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    for (VM vm : toArray(empty, normal, replicate1, replicate2, replicate3)) {
      vm.invoke(() -> registerThrowsFunctionInvocationTargetExceptionFunction(false, 0));
    }

    empty.invoke(() -> populateRegion(200));

    empty.invoke(() -> {
      try (IgnoredException ie = addIgnoredException("I have been thrown")) {
        executeFunctionFunctionInvocationTargetExceptionWithoutHA();
      }
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionHACacheClosedException() {
    for (VM vm : toArray(empty, replicate1)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    empty.invoke(() -> createRegion(DataPolicy.EMPTY));
    replicate1.invoke(() -> createRegion(DataPolicy.REPLICATE));

    empty.invoke(() -> populateRegion(200));

    replicate2.invoke(() -> {
      createCache(getDistributedSystemProperties());
      createRegion(DataPolicy.REPLICATE);
    });

    empty.invoke(() -> {
      List<Boolean> result = executeDistributedRegionFunction();

      assertThat(result)
          .hasSize(5001)
          .containsOnly(true);
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionHANodeFailure() throws Exception {
    for (VM vm : toArray(empty, replicate1)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    empty.invoke(() -> createRegion(DataPolicy.EMPTY));
    replicate1.invoke(() -> createRegion(DataPolicy.REPLICATE));

    empty.invoke(() -> populateRegion(200));

    AsyncInvocation<List<Boolean>> executeFunctionHaInEmptyVM =
        empty.invokeAsync(this::executeDistributedRegionFunction);

    replicate2.invoke(() -> {
      createCache(getDistributedSystemProperties());
      createRegion(DataPolicy.REPLICATE);
    });

    replicate1.invoke(() -> getCache().close());

    List<Boolean> result = executeFunctionHaInEmptyVM.get();

    assertThat(result)
        .hasSize(5001)
        .containsOnly(true);
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyEmpty_ClientServer() {
    VM empty1 = replicate3;
    VM empty2 = normal;
    VM client = empty;

    for (VM vm : toArray(empty2, replicate1, replicate2, empty1)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    client.invoke(() -> createClientCache());

    int port1 = empty1.invoke(() -> {
      createRegion(DataPolicy.EMPTY);
      return createCacheServer();
    });
    int port2 = empty2.invoke(() -> {
      createRegion(DataPolicy.EMPTY);
      return createCacheServer();
    });

    for (VM vm : toArray(replicate1, replicate2)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    client.invoke(() -> {
      createClientRegion(port1, port2);
      populateClientRegion(200);

      executeDistributedRegionFunction();
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyEmpty_ClientServer_SendException() {
    VM empty1 = replicate3;
    VM empty2 = normal;
    VM client = empty;

    for (VM vm : toArray(empty2, replicate1, replicate2, empty1)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    client.invoke(() -> createClientCache());

    int port1 = empty1.invoke(() -> {
      createRegion(DataPolicy.EMPTY);
      return createCacheServer();
    });
    int port2 = empty2.invoke(() -> {
      createRegion(DataPolicy.EMPTY);
      return createCacheServer();
    });

    for (VM vm : toArray(replicate1, replicate2)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    client.invoke(() -> {
      createClientRegion(port1, port2);
      populateClientRegion(200);

      executeResultWithExceptionFunction();
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyEmpty_ClientServer_NoLastResult() {
    VM empty1 = replicate3;
    VM empty2 = normal;
    VM client = empty;

    for (VM vm : toArray(empty2, replicate1, replicate2, empty1)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    client.invoke(() -> createClientCache());

    int port1 = empty1.invoke(() -> {
      createRegion(DataPolicy.EMPTY);
      return createCacheServer();
    });
    int port2 = empty2.invoke(() -> {
      createRegion(DataPolicy.EMPTY);
      return createCacheServer();
    });

    for (VM vm : toArray(replicate1, replicate2)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    client.invoke(() -> {
      createClientRegion(port1, port2);
      populateClientRegion(200);

      try (IgnoredException ie1 = addIgnoredException("did not send last result", empty1);
          IgnoredException ie2 = addIgnoredException("did not send last result", empty2)) {
        executeNoLastResultFunction();
      }
    });
  }

  /**
   * If one server goes down while executing a function, that function should failover to other
   * available server.
   */
  @Test
  public void testServerFailoverWithTwoServerAliveHA() throws InterruptedException {
    VM emptyServer1 = replicate1;
    VM client = normal;

    for (VM vm : toArray(emptyServer1, replicate2, replicate3)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    int port1 = emptyServer1.invoke(() -> {
      createRegion(DataPolicy.EMPTY);
      return createCacheServer();
    });
    int port2 = replicate2.invoke(() -> {
      createRegion(DataPolicy.REPLICATE);
      return createCacheServer();
    });

    replicate3.invoke(() -> createRegion(DataPolicy.REPLICATE));

    client.invoke(() -> {
      LATCH.set(new CountDownLatch(1));
      createClientCache();
      createClientRegion(port1, port2);
      populateClientRegion(200);
    });

    replicate2.invoke(this::stopServerHA);

    AsyncInvocation<List<Boolean>> executeFunctionHaInClientVm =
        client.invokeAsync(this::executeDistributedRegionFunction);

    replicate2.invoke(() -> {
      startServerHA();
      client.invoke(() -> LATCH.get().countDown());
    });

    client.invoke(() -> {
      LATCH.get().await(getTimeout().toMillis(), MILLISECONDS);
    });

    emptyServer1.invoke(this::closeCacheHA);

    List<Boolean> result = executeFunctionHaInClientVm.get();

    assertThat(result)
        .hasSize(5001)
        .containsOnly(true);
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyNormal_ClientServer() {
    VM client = empty;
    VM normal1 = normal;
    VM normal2 = replicate3;
    VM empty = replicate2;

    for (VM vm : toArray(normal1, replicate1, empty, normal2)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    client.invoke(() -> createClientCache());

    int port1 = normal1.invoke(() -> {
      createRegion(DataPolicy.NORMAL);
      return createCacheServer();
    });
    int port2 = normal2.invoke(() -> {
      createRegion(DataPolicy.NORMAL);
      return createCacheServer();
    });

    for (VM vm : toArray(empty, replicate1)) {
      vm.invoke(() -> createRegion(DataPolicy.EMPTY));
    }

    client.invoke(() -> {
      createClientRegion(port1, port2);
      populateClientRegion(200);

      // add expected exception
      try (IgnoredException ie = addIgnoredException(FunctionException.class)) {
        Throwable thrown = catchThrowable(this::executeDistributedRegionFunction);

        assertThat(thrown)
            .isInstanceOf(FunctionException.class)
            .hasMessageContaining(
                "Function execution on region with DataPolicy.NORMAL is not supported");
      }
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyReplicate_ClientServer() {
    VM client = empty;
    VM empty = replicate3;

    for (VM vm : toArray(normal, replicate1, replicate2, empty)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    client.invoke(() -> createClientCache());

    int port1 = replicate1.invoke(() -> {
      createRegion(DataPolicy.REPLICATE);
      return createCacheServer();
    });
    int port2 = replicate2.invoke(() -> {
      createRegion(DataPolicy.REPLICATE);
      return createCacheServer();
    });

    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    empty.invoke(() -> createRegion(DataPolicy.EMPTY));

    client.invoke(() -> {
      createClientRegion(port1, port2);
      populateClientRegion(200);

      executeUnregisteredFunction();
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyReplicate_ClientServer_WithoutRegister() {
    VM client = empty;
    VM empty = replicate3;

    for (VM vm : toArray(normal, replicate1, replicate2, empty)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    client.invoke(() -> createClientCache());

    int port1 = replicate1.invoke(() -> {
      createRegion(DataPolicy.REPLICATE);
      return createCacheServer();
    });
    int port2 = replicate2.invoke(() -> {
      createRegion(DataPolicy.REPLICATE);
      return createCacheServer();
    });

    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    empty.invoke(() -> createRegion(DataPolicy.EMPTY));

    client.invoke(() -> {
      createClientRegion(port1, port2);
      populateClientRegion(200);

      executeDistributedRegionFunction();
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyReplicate_ClientServer_FunctionInvocationTargetException() {
    VM client = empty;
    VM empty = replicate3;

    for (VM vm : toArray(normal, replicate1, replicate2, empty)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    int port1 = replicate1.invoke(() -> {
      createRegion(DataPolicy.REPLICATE);
      return createCacheServer();
    });
    int port2 = replicate2.invoke(() -> {
      createRegion(DataPolicy.REPLICATE);
      return createCacheServer();
    });

    client.invoke(() -> createClientCache());
    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    empty.invoke(() -> createRegion(DataPolicy.EMPTY));

    for (VM vm : toArray(client, replicate1, replicate2, normal, empty)) {
      vm.invoke(() -> registerThrowsFunctionInvocationTargetExceptionFunction(true, 5));
    }

    client.invoke(() -> {
      createClientRegion(port1, port2);
      populateClientRegion(200);

      // add expected exception to avoid suspect strings
      try (IgnoredException ie = addIgnoredException("I have been thrown")) {
        executeFunctionFunctionInvocationTargetException_ClientServer();
      }
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyReplicate_ClientServer_FunctionInvocationTargetException_WithoutHA() {
    VM client = empty;
    VM empty = replicate3;

    for (VM vm : toArray(normal, replicate1, replicate2, empty)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    int port1 = replicate1.invoke(() -> {
      createRegion(DataPolicy.REPLICATE);
      return createCacheServer();
    });
    int port2 = replicate2.invoke(() -> {
      createRegion(DataPolicy.REPLICATE);
      return createCacheServer();
    });

    client.invoke(() -> createClientCache());
    normal.invoke(() -> createRegion(DataPolicy.NORMAL));
    empty.invoke(() -> createRegion(DataPolicy.EMPTY));

    for (VM vm : toArray(client, replicate1, replicate2, normal, empty)) {
      vm.invoke(() -> registerThrowsFunctionInvocationTargetExceptionFunction(false, 0));
    }

    client.invoke(() -> {
      createClientRegion(port1, port2);
      populateClientRegion(200);

      // add expected exception to avoid suspect strings
      try (IgnoredException ie = addIgnoredException("I have been thrown")) {
        executeFunctionFunctionInvocationTargetException_ClientServer_WithoutHA();
      }
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyEmpty_ClientServer_FunctionInvocationTargetException() {
    VM client = empty;
    VM empty1 = replicate3;
    VM empty2 = normal;

    for (VM vm : toArray(empty2, replicate1, replicate2, empty1)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    int port1 = empty1.invoke(() -> {
      createRegion(DataPolicy.EMPTY);
      return createCacheServer();
    });
    int port2 = empty2.invoke(() -> {
      createRegion(DataPolicy.EMPTY);
      return createCacheServer();
    });

    client.invoke(() -> createClientCache());
    for (VM vm : toArray(replicate1, replicate2)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    client.invoke(() -> {
      createClientRegion(port1, port2);
      populateClientRegion(200);
    });

    for (VM vm : toArray(client, empty1, empty2, replicate1, replicate2)) {
      vm.invoke(() -> registerThrowsFunctionInvocationTargetExceptionFunction(true, 5));
    }

    client.invoke(() -> {
      // add expected exception to avoid suspect strings
      try (IgnoredException ie = addIgnoredException("I have been thrown")) {
        executeFunctionFunctionInvocationTargetException_ClientServer();
      }
    });
  }

  @Test
  public void testDistributedRegionFunctionExecutionOnDataPolicyEmpty_ClientServer_FunctionInvocationTargetException_WithoutHA() {
    VM client = empty;
    VM empty1 = replicate3;
    VM empty2 = normal;

    for (VM vm : toArray(empty2, replicate1, replicate2, empty1)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }

    client.invoke(() -> createClientCache());

    int port1 = empty1.invoke(() -> {
      createRegion(DataPolicy.EMPTY);
      return createCacheServer();
    });
    int port2 = empty2.invoke(() -> {
      createRegion(DataPolicy.EMPTY);
      return createCacheServer();
    });

    for (VM vm : toArray(replicate1, replicate2)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }

    client.invoke(() -> {
      createClientRegion(port1, port2);
      populateClientRegion(200);
    });

    for (VM vm : toArray(client, empty1, empty2, replicate1, replicate2)) {
      vm.invoke(() -> registerThrowsFunctionInvocationTargetExceptionFunction(false, 0));
    }

    client.invoke(() -> {
      // add expected exception to avoid suspect strings
      try (IgnoredException ie = addIgnoredException("I have been thrown")) {
        executeFunctionFunctionInvocationTargetException_ClientServer_WithoutHA();
      }
    });
  }

  @Test
  public void inlineFunctionIsUsedOnClientInsteadOfLookingUpFunctionById() {
    VM client = empty;
    VM empty = replicate3;

    for (VM vm : toArray(normal, empty, replicate1, replicate2)) {
      vm.invoke(() -> createCache(getDistributedSystemProperties()));
    }
    client.invoke(() -> createClientCache(getClientProperties()));

    int port1 = replicate1.invoke(() -> {
      createRegion(DataPolicy.REPLICATE);
      return createCacheServer();
    });
    int port2 = replicate2.invoke(() -> {
      createRegion(DataPolicy.REPLICATE);
      return createCacheServer();
    });

    for (VM vm : toArray(normal, empty)) {
      vm.invoke(() -> createRegion(DataPolicy.REPLICATE));
    }
    client.invoke(() -> {
      createClientRegion(port1, port2);
      populateClientRegion(200);
    });

    for (VM vm : toArray(normal, empty, replicate1, replicate2, client)) {
      vm.invoke(() -> FunctionService.registerFunction(inlineFunction("Failure", false)));
    }

    client.invoke(() -> {
      ResultCollector<Boolean, List<Boolean>> resultCollector = TypedFunctionService
          .<Boolean, Boolean, List<Boolean>>onRegion(getRegion())
          .setArguments(true)
          .execute(inlineFunction("Success", true));

      assertThat(resultCollector.getResult())
          .hasSize(1)
          .containsOnly(true);
    });
  }

  /**
   * Verify that AuthenticationRequiredException is not thrown when security-* properties are NOT
   * provided. We have to grep for this exception in logs for any occurrence.
   */
  @Test
  public void authenticationRequiredExceptionIsNotThrownWhenSecurityIsNotConfigured() {
    VM client = replicate1;
    VM server = replicate2;

    int port = server.invoke(() -> {
      createCacheWithSecurity();
      createRegion(DataPolicy.REPLICATE);
      return createCacheServer();
    });

    client.invoke(() -> {
      createClientCacheWithSecurity();
      createClientRegion(port);

      executeNoResultFunction();
      doPut();
    });
  }

  @Test
  public void testFunctionWithNoResultThrowsException() {
    VM client = empty;

    int port1 = replicate1.invoke(() -> {
      createCache(getDistributedSystemProperties());
      createRegion(DataPolicy.REPLICATE);
      return createCacheServer();
    });
    int port2 = replicate2.invoke(() -> {
      createCache(getDistributedSystemProperties());
      createRegion(DataPolicy.REPLICATE);
      return createCacheServer();
    });

    client.invoke(() -> {
      createClientCache();
      createClientRegion(port1, port2);
      populateClientRegion(200);

      try (IgnoredException ie = addIgnoredException(RuntimeException.class)) {
        executeThrowsRuntimeExceptionFunction();
      }
    });
  }

  private Properties getDistributedSystemProperties() {
    Properties props = DistributedRule.getDistributedSystemProperties();
    props.setProperty(SERIALIZABLE_OBJECT_FILTER,
        "org.apache.geode.internal.cache.functions.**;org.apache.geode.internal.cache.execute.**;org.apache.geode.test.dunit.**");
    return props;
  }

  private Properties getClientProperties() {
    Properties props = new Properties();
    props.setProperty(SERIALIZABLE_OBJECT_FILTER,
        "org.apache.geode.internal.cache.functions.**;org.apache.geode.internal.cache.execute.**;org.apache.geode.test.dunit.**");
    return props;
  }

  private void createCache(Properties props) {
    cacheRule.createCache(props);
  }

  private void createClientCache(Properties props) {
    clientCacheRule.createClientCache(props);
  }

  private InternalCache getCache() {
    return cacheRule.getCache();
  }

  private InternalClientCache getClientCache() {
    return clientCacheRule.getClientCache();
  }

  private void createCacheWithSecurity() {
    Properties props = getDistributedSystemProperties();
    props.setProperty(NAME, "SecurityServer");
    props.setProperty(SECURITY_CLIENT_AUTHENTICATOR,
        DummyAuthenticator.class.getName() + ".create");

    createCache(props);
  }

  private void createClientCache() {
    Properties props = getDistributedSystemProperties();
    props.setProperty(MCAST_PORT, "0");
    props.setProperty(LOCATORS, "");

    createClientCache(props);
  }

  private void createClientCacheWithSecurity() {
    Properties props = getDistributedSystemProperties();
    props.setProperty(MCAST_PORT, "0");
    props.setProperty(LOCATORS, "");
    props.setProperty(NAME, "SecurityClient");
    props.setProperty(SECURITY_CLIENT_AUTH_INIT, UserPasswordAuthInit.class.getName() + ".create");
    props.setProperty("security-username", "reader1");
    props.setProperty("security-password", "reader1");

    createClientCache(props);
  }

  private void createClientRegion(int... ports) {
    System.setProperty(GEMFIRE_PREFIX + "bridge.disableShufflingOfEndpoints", "true");

    PoolFactory poolFactory = PoolManager.createFactory();
    for (int port : ports) {
      poolFactory.addServer("localhost", port);
    }
    Pool pool = poolFactory
        .setMaxConnections(10)
        .setMinConnections(6)
        .setPingInterval(3000)
        .setReadTimeout(2000)
        .setRetryAttempts(2)
        .setSocketBufferSize(1000)
        .setSubscriptionEnabled(false)
        .setSubscriptionRedundancy(-1)
        .create(poolName);

    Region<?, ?> region = getClientCache()
        .createClientRegionFactory(ClientRegionShortcut.PROXY)
        .setPoolName(pool.getName())
        .create(regionName);

    setRegion(region);
  }

  private void createRegion(DataPolicy dataPolicy) {
    Region<?, ?> region = getCache().createRegionFactory()
        .setDataPolicy(dataPolicy)
        .setScope(Scope.DISTRIBUTED_ACK)
        .create(regionName);

    setRegion(region);
  }

  private int createCacheServer() throws IOException {
    CacheServer cacheServer = getCache().addCacheServer();
    cacheServer.setPort(0);
    cacheServer.start();
    return cacheServer.getPort();
  }

  private void doPut() {
    Region<String, String> region = getRegion();
    region.put("K1", "B1");
  }

  private void populateRegion(int count) {
    Region<String, Integer> region = getRegion();
    for (int i = 1; i <= count; i++) {
      region.put("execKey-" + i, i);
    }
  }

  private void populateClientRegion(int count) {
    Region<String, Integer> region = getRegion();
    for (int i = 1; i <= count; i++) {
      region.put("execKey-" + i, i);
    }
  }

  private void startServerHA() throws IOException {
    for (CacheServer cacheServer : getCache().getCacheServers()) {
      cacheServer.start();
    }
  }

  private void stopServerHA() {
    for (CacheServer cacheServer : getCache().getCacheServers()) {
      cacheServer.stop();
    }
  }

  private void closeCacheHA() {
    for (CacheServer cacheServer : getCache().getCacheServers()) {
      cacheServer.stop();
    }

    getCache().close();
  }

  private void registerThrowsFunctionInvocationTargetExceptionFunction(boolean isHA,
      int retryCount) {
    FunctionService.registerFunction(
        new ThrowsFunctionInvocationTargetExceptionFunction(isHA, retryCount));
  }

  private void executeNoResultFunction() {
    TypedFunctionService
        .onRegion(getRegion())
        .execute(new NoResultFunction());
  }

  private List<Boolean> executeDistributedRegionFunction() {
    return TypedFunctionService
        .<Boolean, Boolean, List<Boolean>>onRegion(getRegion())
        .withFilter(filter)
        .setArguments(false)
        .execute(new DistributedRegionFunction())
        .getResult();
  }

  private void executeThrowsRuntimeExceptionFunction() {
    TypedFunctionService
        .<Void, Void, Void>onRegion(getRegion())
        .withFilter(filter)
        .execute(new ThrowsRuntimeExceptionFunction());
  }

  private void executeResultWithExceptionFunction() {
    Function function = new ResultWithExceptionFunction();

    Set<String> filter = new HashSet<>();
    for (int i = 0; i <= 19; i++) {
      filter.add("execKey-" + 100 + i);
    }

    ResultCollector<Object, List<Object>> resultCollector = TypedFunctionService
        .<Boolean, Object, List<Object>>onRegion(getRegion())
        .withFilter(filter)
        .setArguments(true)
        .execute(function);

    List<Object> result = resultCollector.getResult();
    result.sort(new NumericComparator());

    assertThat(result.get(0))
        .as("First element of " + resultCollector.getResult())
        .isInstanceOf(CustomRuntimeException.class);

    resultCollector = TypedFunctionService
        .<Set<String>, Object, List<Object>>onRegion(getRegion())
        .withFilter(filter)
        .setArguments(filter)
        .execute(function);

    result = resultCollector.getResult();
    result.sort(new NumericComparator());

    assertThat(result)
        .hasSize(filter.size() + 1)
        .containsAll(IntStream.rangeClosed(0, 19).boxed().collect(toList()));

    assertThat(result.get(result.size() - 1))
        .as("Last element of " + result)
        .isInstanceOf(CustomRuntimeException.class);
  }

  private void executeNoLastResultFunction() {
    Throwable thrown = catchThrowable(() -> {
      TypedFunctionService
          .onRegion(getRegion())
          .withFilter(filter)
          .execute(new NoLastResultFunction())
          .getResult();
    });

    assertThat(thrown)
        .hasMessageContaining("did not send last result");
  }

  private void executeUnregisteredFunction() {
    FunctionService.unregisterFunction(new DistributedRegionFunction().getId());

    TypedFunctionService
        .<Void, Boolean, List<Boolean>>onRegion(getRegion())
        .withFilter(filter)
        .execute(new DistributedRegionFunction())
        .getResult();
  }

  private void executeFunctionFunctionInvocationTargetException() {
    ResultCollector<Integer, List<Integer>> resultCollector = TypedFunctionService
        .<Boolean, Integer, List<Integer>>onRegion(getRegion())
        .setArguments(true)
        .execute(ThrowsFunctionInvocationTargetExceptionFunction.class.getSimpleName());

    assertThat(resultCollector.getResult())
        .containsOnly(5);
  }

  private void executeFunctionFunctionInvocationTargetExceptionWithoutHA() {
    Throwable thrown = catchThrowable(() -> {
      TypedFunctionService
          .<Boolean, Integer, List<Integer>>onRegion(getRegion())
          .setArguments(true)
          .execute(ThrowsFunctionInvocationTargetExceptionFunction.class.getSimpleName())
          .getResult();
    });

    assertThat(thrown)
        .isInstanceOf(FunctionException.class);

    assertThat(thrown.getCause())
        .isInstanceOf(FunctionInvocationTargetException.class);
  }

  private void executeFunctionFunctionInvocationTargetException_ClientServer() {
    ResultCollector<Integer, List<Integer>> resultCollector = TypedFunctionService
        .<Boolean, Integer, List<Integer>>onRegion(getRegion())
        .setArguments(true)
        .execute(ThrowsFunctionInvocationTargetExceptionFunction.class.getSimpleName());

    assertThat(resultCollector.getResult())
        .containsOnly(5);
  }

  private void executeFunctionFunctionInvocationTargetException_ClientServer_WithoutHA() {
    Throwable thrown = catchThrowable(() -> {
      TypedFunctionService
          .<Boolean, Integer, List<Integer>>onRegion(getRegion())
          .setArguments(true)
          .execute(ThrowsFunctionInvocationTargetExceptionFunction.class.getSimpleName())
          .getResult();
    });

    assertThat(thrown)
        .isInstanceOf(FunctionException.class);

    assertThat(thrown.getCause())
        .isInstanceOf(FunctionInvocationTargetException.class);
  }

  private static <K, V> Region<K, V> getRegion() {
    return uncheckedCast(REGION.get());
  }

  private static void setRegion(Region<?, ?> region) {
    REGION.set(region);
  }

  private static Function<Object> inlineFunction(String stringResult, boolean booleanResult) {
    return new Function<Object>() {
      @Override
      public void execute(FunctionContext<Object> context) {
        if (context.getArguments() instanceof String) {
          context.getResultSender().lastResult(stringResult);
        } else if (context.getArguments() instanceof Boolean) {
          context.getResultSender().lastResult(booleanResult);
        }
      }

      @Override
      public String getId() {
        return "Function";
      }
    };
  }

  /**
   * Return Integers provided as Arguments and a last result of CustomRuntimeException.
   */
  private static class ResultWithExceptionFunction implements Function<Object> {

    @Override
    public void execute(FunctionContext<Object> context) {
      if (context.getArguments() instanceof Set) {
        Set<Integer> arguments =
            uncheckedCast(context.getArguments());
        for (int i = 0; i < arguments.size(); i++) {
          context.getResultSender().sendResult(i);
        }
      }
      context.getResultSender().sendException(
          new CustomRuntimeException("I have been thrown from TestFunction with set"));
    }

    @Override
    public String getId() {
      return getClass().getName();
    }

    @Override
    public boolean isHA() {
      return false;
    }
  }

  /**
   * Throw RuntimeException without sending any results.
   */
  private static class ThrowsRuntimeExceptionFunction implements Function<Void> {

    @Override
    public void execute(FunctionContext<Void> context) {
      throw new RuntimeException("failure");
    }

    @Override
    public String getId() {
      return getClass().getName();
    }

    @Override
    public boolean hasResult() {
      return false;
    }

    @Override
    public boolean isHA() {
      return false;
    }
  }

  /**
   * Return the Integer 5 or throw FunctionInvocationTargetException depending on object state.
   */
  private static class ThrowsFunctionInvocationTargetExceptionFunction implements Function<Void> {

    private final AtomicInteger count = new AtomicInteger();
    private final int retryCount;
    private final boolean isHA;

    private ThrowsFunctionInvocationTargetExceptionFunction(boolean isHA, int retryCount) {
      this.isHA = isHA;
      this.retryCount = retryCount;
    }

    @Override
    public void execute(FunctionContext<Void> context) {
      count.incrementAndGet();
      if (retryCount != 0 && count.get() >= retryCount) {
        context.getResultSender().lastResult(5);
      } else {
        throw new FunctionInvocationTargetException("I have been thrown from " + getId());
      }
    }

    @Override
    public String getId() {
      return getClass().getSimpleName();
    }

    @Override
    public boolean isHA() {
      return isHA;
    }
  }

  /**
   * Validate DataPolicy and Filter state, perform Region put operations, and then return 5000
   * boolean true values and a last result of boolean false.
   */
  private static class DistributedRegionFunction implements Function<Boolean> {

    @Override
    public void execute(FunctionContext<Boolean> context) {
      RegionFunctionContext regionFunctionContext = (RegionFunctionContext) context;
      Region<Object, Object> region = regionFunctionContext.getDataSet();
      InternalDistributedSystem sys = InternalDistributedSystem.getConnectedInstance();

      assertThat(region.getAttributes().getDataPolicy().withStorage()).isTrue();
      assertThat(region.getAttributes().getDataPolicy()).isNotEqualTo(DataPolicy.NORMAL);
      assertThat(regionFunctionContext.getFilter()).hasSize(20);

      // argument true indicates that CacheClose has to be done from the body itself
      if (context.getArguments() != null && context.getArguments()) {
        // do not close cache in retry
        if (!regionFunctionContext.isPossibleDuplicate()) {
          sys.disconnect();
          throw new CacheClosedException(
              "Throwing CacheClosedException to simulate failover during function exception");
        }
      }

      // intentionally doing region operation to cause cacheClosedException
      region.put("execKey-201", 201);

      if (regionFunctionContext.isPossibleDuplicate()) {
        // Below operation is done when the function is reexecuted
        region.put("execKey-202", 202);
        region.put("execKey-203", 203);
      }

      for (int i = 0; i < 5000; i++) {
        context.getResultSender().sendResult(true);
      }
      context.getResultSender().lastResult(true);
    }

    @Override
    public String getId() {
      return getClass().getSimpleName();
    }
  }

  /**
   * Execute until the provided CountDownLatch counts down to zero.
   */
  private static class LongRunningFunction implements Function<Void> {

    private final CountDownLatch latch;

    private LongRunningFunction(CountDownLatch latch) {
      this.latch = latch;
    }

    @Override
    public void execute(FunctionContext<Void> context) {
      try {
        latch.await(getTimeout().toMillis(), MILLISECONDS);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }

      context.getResultSender().lastResult("LongRunningFunction completed");
    }

    @Override
    public String getId() {
      return getClass().getSimpleName();
    }
  }

  /**
   * Complete immediately with no results.
   */
  private static class NoResultFunction implements Function<Void> {

    @Override
    public void execute(FunctionContext<Void> context) {
      // no result
    }

    @Override
    public String getId() {
      return getClass().getSimpleName();
    }

    @Override
    public boolean hasResult() {
      return false;
    }

    @Override
    public boolean isHA() {
      return false;
    }
  }

  /**
   * Return Arguments as results without a last result.
   */
  private static class NoLastResultFunction implements Function<Object> {

    @Override
    public void execute(FunctionContext<Object> context) {
      context.getResultSender().sendResult(context.getArguments());
    }

    @Override
    public String getId() {
      return getClass().getSimpleName();
    }
  }

  /**
   * Custom RuntimeException.
   */
  private static class CustomRuntimeException extends RuntimeException {

    private CustomRuntimeException(String message) {
      super(message);
    }
  }

  /**
   * Use natural ordering for Integers and moves optional Exception to end of Collection.
   */
  private static class NumericComparator implements Comparator<Object>, Serializable {

    @Override
    public int compare(Object o1, Object o2) {
      if (o1 == o2) {
        return 0;
      }
      if (!(o1 instanceof Integer)) {
        return 1;
      }
      if (!(o2 instanceof Integer)) {
        return -1;
      }
      if ((int) o1 > (int) o2) {
        return 1;
      }
      return -1;
    }
  }
}
