/*
 * 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.test.dunit.rules;

import static org.apache.geode.test.dunit.Disconnect.disconnectFromDS;
import static org.apache.geode.test.dunit.DistributedTestUtils.unregisterInstantiatorsInThisVM;
import static org.apache.geode.test.dunit.Invoke.invokeInEveryVM;
import static org.apache.geode.test.dunit.Invoke.invokeInLocator;
import static org.apache.geode.test.dunit.VM.DEFAULT_VM_COUNT;
import static org.apache.geode.test.dunit.VM.getVM;
import static org.assertj.core.api.Assertions.assertThat;

import org.apache.geode.cache.query.QueryTestUtils;
import org.apache.geode.cache.query.internal.QueryObserverHolder;
import org.apache.geode.cache30.ClientServerTestCase;
import org.apache.geode.cache30.RegionTestCase;
import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.distributed.internal.DistributionMessageObserver;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.distributed.internal.tcpserver.TcpClient;
import org.apache.geode.internal.admin.ClientStatsManager;
import org.apache.geode.internal.cache.DiskStoreObserver;
import org.apache.geode.internal.cache.InitialImageOperation;
import org.apache.geode.internal.cache.tier.InternalClientMembership;
import org.apache.geode.internal.cache.tier.sockets.CacheServerTestUtil;
import org.apache.geode.internal.cache.tier.sockets.ClientProxyMembershipID;
import org.apache.geode.internal.cache.tier.sockets.Message;
import org.apache.geode.internal.cache.xmlcache.CacheCreation;
import org.apache.geode.internal.net.SocketCreator;
import org.apache.geode.internal.net.SocketCreatorFactory;
import org.apache.geode.management.internal.cli.LogWrapper;
import org.apache.geode.pdx.internal.TypeRegistry;
import org.apache.geode.test.dunit.IgnoredException;
import org.apache.geode.test.dunit.standalone.DUnitLauncher;
import org.apache.geode.test.junit.rules.serializable.SerializableExternalResource;

/**
 * JUnit Rule that launches DistributedTest VMs and scans all log output for suspect strings without
 * {@code DistributedTestCase}. The test class may need to implement {@code Serializable} if it
 * uses lambdas to perform {@code RMI} invocations on {@code VM}s.
 *
 * <p>
 * {@code DistributedRule} can be used in DistributedTests as a {@code Rule}. This will ensure
 * that checking for suspect strings is performed after each test method.
 *
 * <pre>
 * {@literal @}Rule
 * public DistributedRule distributedRule = new DistributedRule();
 *
 * {@literal @}Test
 * public void shouldHaveFourVMsByDefault() {
 *   assertThat(getVMCount()).isEqualTo(4);
 * }
 * </pre>
 *
 * <p>
 * You may specify a non-default number of {@code VM}s for the test when constructing
 * {@code DistributedRule}.
 *
 * <p>
 * Example of specifying fewer that the default number of {@code VM}s (which is 4):
 *
 * <pre>
 * {@literal @}Rule
 * public DistributedRule distributedRule = new DistributedRule(1);
 *
 * {@literal @}Test
 * public void hasOneVM() {
 *   assertThat(getVMCount()).isEqualTo(1);
 * }
 * </pre>
 *
 * <p>
 * Example of specifying greater that the default number of {@code VM}s (which is 4):
 *
 * <pre>
 * {@literal @}Rule
 * public DistributedRule distributedRule = new DistributedRule(8);
 *
 * {@literal @}Test
 * public void hasEightVMs() {
 *   assertThat(getVMCount()).isEqualTo(8);
 * }
 * </pre>
 *
 * <p>
 * {@code DistributedRule} can also be used in DistributedTests as a {@code ClassRule}. This ensures
 * that DUnit VMs will be available to non-Class {@code Rule}s. However, you may want to declare
 * {@code DistributedRule.TearDown} as a non-Class {@code Rule} so that check for suspect strings is
 * performed after each test method.
 *
 * <pre>
 * {@literal @}ClassRule
 * public static DistributedRule distributedRule = new DistributedRule();
 *
 * {@literal @}Rule
 * public DistributedRule.TearDown distributedRuleTearDown = new DistributedRule.TearDown();
 *
 * {@literal @}Test
 * public void shouldHaveFourDUnitVMsByDefault() {
 *   assertThat(getVMCount()).isEqualTo(4);
 * }
 * </pre>
 */
@SuppressWarnings("unused")
public class DistributedRule extends AbstractDistributedRule {

  private final int vmCount;

  /**
   * Use {@code Builder} for more options in constructing {@code DistributedRule}.
   */
  public static Builder builder() {
    return new Builder();
  }

  /**
   * Constructs DistributedRule and launches the default number of {@code VM}s (which is 4).
   */
  public DistributedRule() {
    this(new Builder());
  }

  /**
   * Constructs DistributedRule and launches the specified number of {@code VM}s.
   *
   * @param vmCount specified number of VMs
   */
  public DistributedRule(final int vmCount) {
    this(new Builder().withVMCount(vmCount));
  }

  DistributedRule(final Builder builder) {
    super(builder.vmCount);
    vmCount = builder.vmCount;
  }

  @Override
  protected void before() {
    DUnitLauncher.launchIfNeeded(vmCount);
    for (int i = 0; i < vmCount; i++) {
      assertThat(getVM(i)).isNotNull();
    }
  }

  @Override
  protected void after() {
    TearDown.doTearDown();
  }

  /**
   * Builds an instance of DistributedRule.
   */
  public static class Builder {

    private int vmCount = DEFAULT_VM_COUNT;

    public Builder withVMCount(final int vmCount) {
      if (vmCount < 0) {
        throw new IllegalArgumentException("VM count must be positive integer");
      }
      this.vmCount = vmCount;
      return this;
    }

    public DistributedRule build() {
      return new DistributedRule(this);
    }
  }

  /**
   * Cleans up horrendous things like static state and non-default instances in Geode.
   *
   * <p>
   * {@link DistributedRule#after()} invokes the same cleanup that this Rule does, but if you
   * defined {@code DistributedRule} as a {@code ClassRule} then you should declare TearDown
   * as a non-class {@code Rule} in your test:
   *
   * <pre>
   * {@literal @}ClassRule
   * public static DistributedRule distributedRule = new DistributedRule();
   *
   * {@literal @}Rule
   * public DistributedRule.TearDown tearDownRule = new DistributedRule.TearDown();
   *
   * {@literal @}Test
   * public void shouldHaveFourDUnitVMsByDefault() {
   *   assertThat(getVMCount()).isEqualTo(4);
   * }
   * </pre>
   *
   * <p>
   * Note: {@link CacheRule} handles its own cleanup of Cache and Regions.
   */
  public static class TearDown extends SerializableExternalResource {

    @Override
    protected void before() {
      // nothing
    }

    @Override
    protected void after() {
      doTearDown();
    }

    static void doTearDown() {
      tearDownInVM();
      invokeInEveryVM(() -> {
        tearDownInVM();
      });
      invokeInLocator(() -> {
        DistributionMessageObserver.setInstance(null);
        unregisterInstantiatorsInThisVM();
      });
      DUnitLauncher.closeAndCheckForSuspects();
    }

    public static void tearDownInVM() {
      // 1. Please do NOT add to this list. I'm trying to DELETE this list.
      // 2. Instead, please add to the after() of your test or your rule.

      disconnectFromDS();

      // keep alphabetized to detect duplicate lines
      CacheCreation.clearThreadLocals();
      CacheServerTestUtil.clearCacheReference();
      ClientProxyMembershipID.system = null;
      ClientServerTestCase.AUTO_LOAD_BALANCE = false;
      ClientStatsManager.cleanupForTests();
      DiskStoreObserver.setInstance(null);
      unregisterInstantiatorsInThisVM();
      DistributionMessageObserver.setInstance(null);
      InitialImageOperation.slowImageProcessing = 0;
      InternalClientMembership.unregisterAllListeners();
      LogWrapper.close();
      QueryObserverHolder.reset();
      QueryTestUtils.setCache(null);
      RegionTestCase.preSnapshotRegion = null;
      SocketCreator.resetHostNameCache();
      SocketCreator.resolve_dns = true;
      TcpClient.clearStaticData();

      // clear system properties -- keep alphabetized
      System.clearProperty(DistributionConfig.GEMFIRE_PREFIX + "log-level");
      System.clearProperty("jgroups.resolve_dns");
      System.clearProperty(Message.MAX_MESSAGE_SIZE_PROPERTY);

      if (InternalDistributedSystem.systemAttemptingReconnect != null) {
        InternalDistributedSystem.systemAttemptingReconnect.stopReconnecting();
      }

      IgnoredException.removeAllExpectedExceptions();
      SocketCreatorFactory.close();
      TypeRegistry.setPdxSerializer(null);
      TypeRegistry.init();
    }
  }
}
