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

import static org.apache.geode.cache.Region.SEPARATOR;
import static org.apache.geode.distributed.ConfigurationProperties.STATISTIC_SAMPLING_ENABLED;
import static org.apache.geode.test.awaitility.GeodeAwaitility.await;
import static org.assertj.core.api.Assertions.assertThat;

import java.net.InetAddress;
import java.util.Set;

import org.apache.logging.log4j.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.CacheException;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.cache.Scope;
import org.apache.geode.distributed.DistributedLockService;
import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
import org.apache.geode.internal.Config;
import org.apache.geode.internal.admin.Alert;
import org.apache.geode.internal.admin.AlertListener;
import org.apache.geode.internal.admin.ApplicationVM;
import org.apache.geode.internal.admin.DLockInfo;
import org.apache.geode.internal.admin.EntryValueNode;
import org.apache.geode.internal.admin.GemFireVM;
import org.apache.geode.internal.admin.GfManagerAgent;
import org.apache.geode.internal.admin.GfManagerAgentConfig;
import org.apache.geode.internal.admin.GfManagerAgentFactory;
import org.apache.geode.internal.admin.StatResource;
import org.apache.geode.internal.admin.remote.RemoteTransportConfig;
import org.apache.geode.internal.logging.InternalLogWriter;
import org.apache.geode.logging.internal.log4j.api.LogService;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.IgnoredException;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.cache.CacheTestCase;

/**
 * This class tests the functionality of the {@linkplain org.apache.geode.internal.admin internal
 * admin} API.
 */

public class ClusterDistributionManagerForAdminDUnitTest extends CacheTestCase
    implements AlertListener {

  private static final Logger logger = LogService.getLogger();

  private transient GfManagerAgent agent;

  @Before
  public void setUp() throws Exception {
    IgnoredException.addIgnoredException("Error occurred while reading system log");

    ClusterDistributionManager.setIsDedicatedAdminVM(true);

    populateCache();

    boolean created = !isConnectedToDS();
    InternalDistributedSystem ds = getSystem();
    RemoteTransportConfig transport =
        new RemoteTransportConfig(ds.getConfig(), ClusterDistributionManager.ADMIN_ONLY_DM_TYPE);
    if (created) {
      disconnectFromDS();
    }

    // create a GfManagerAgent in the master vm.
    agent = GfManagerAgentFactory.getManagerAgent(
        new GfManagerAgentConfig(null, transport, getLogWriter(), Alert.SEVERE, this, null));

    await().untilAsserted(() -> assertThat(agent.isConnected()).isTrue());
  }

  @Override
  @After
  public void preTearDownCacheTestCase() throws Exception {
    try {
      if (agent != null) {
        agent.disconnect();
      }
      disconnectFromDS();
    } finally {
      ClusterDistributionManager.setIsDedicatedAdminVM(false);
    }
  }

  @Test
  public void testGetDistributionVMType() {
    DistributionManager dm = agent.getDM();
    assertThat(dm.getId().getVmKind()).isEqualTo(ClusterDistributionManager.ADMIN_ONLY_DM_TYPE);
  }

  @Test
  public void testAgent() {
    assertThat(agent.listPeers()).hasSize(0);
    assertThat(agent.isConnected()).isTrue();
    agent.disconnect();
    assertThat(agent.isConnected()).isFalse();
  }

  @Test
  public void testApplications() throws Exception {
    await()
        .untilAsserted(() -> assertThat(agent.listApplications().length).isGreaterThanOrEqualTo(4));

    ApplicationVM[] applications = agent.listApplications();
    for (int whichApplication = 0; whichApplication < applications.length; whichApplication++) {

      InetAddress host = applications[whichApplication].getHost();
      String appHostName = host.getHostName();

      assertThat(host).isEqualTo(InetAddress.getByName(appHostName));

      StatResource[] stats = applications[whichApplication].getStats(null);
      assertThat(stats.length).isGreaterThan(0);

      Config config = applications[whichApplication].getConfig();
      String[] attributeNames = config.getAttributeNames();
      boolean foundStatisticSamplingEnabled = false;
      for (String attributeName : attributeNames) {
        if (attributeName.equals(STATISTIC_SAMPLING_ENABLED)) {
          foundStatisticSamplingEnabled = true;
          assertThat(config.getAttribute(attributeName)).isEqualTo("true");
          break;
        }
      }
      assertThat(foundStatisticSamplingEnabled).isTrue();

      String[] logs = applications[whichApplication].getSystemLogs();
      assertThat(logs.length).isGreaterThan(0);

      VM vm = findVMForAdminObject(applications[whichApplication]);
      assertThat(vm).isNotNull();

      String lockName = "cdm_testlock" + whichApplication;
      assertThat(acquireDistributedLock(vm, lockName)).isTrue();

      DLockInfo[] locks = applications[whichApplication].getDistributedLockInfo();
      assertThat(locks.length).isGreaterThan(0);

      boolean foundLock = false;
      for (DLockInfo lock : locks) {
        if (lock.getLockName().equals(lockName)) {
          foundLock = true;
          assertThat(lock.isAcquired()).isTrue();
        }
      }
      assertThat(foundLock).isTrue();

      Region[] roots = applications[whichApplication].getRootRegions();
      assertThat(roots.length).isGreaterThan(0);

      Region root = roots[0];
      assertThat(root).isNotNull();
      assertThat(root.getName()).isEqualTo("root");
      assertThat(root.getFullPath()).isEqualTo(SEPARATOR + "root");

      RegionAttributes attributes = root.getAttributes();
      assertThat(attributes).isNotNull();
      if (attributes.getStatisticsEnabled()) {
        assertThat(root.getStatistics()).isNotNull();
      }

      Set subregions = root.subregions(false);
      assertThat(subregions).hasSize(3);
      assertThat(root.keySet()).hasSize(2);

      Region.Entry entry = root.getEntry("cacheObj1");
      assertThat(entry).isNotNull();
      if (attributes.getStatisticsEnabled()) {
        assertThat(entry.getStatistics()).isNotNull();
      }
      assertThat(entry.getValue()).isEqualTo("null");

      /// test lightweight inspection;
      entry = root.getEntry("cacheObj2");
      assertThat(entry).isNotNull();

      Object val = entry.getValue();
      assertThat(val).isInstanceOf(String.class);
      assertThat(((String) val)).contains("java.lang.StringBuilder");

      /// test physical inspection
      applications[whichApplication].setCacheInspectionMode(GemFireVM.PHYSICAL_CACHE_VALUE);
      entry = root.getEntry("cacheObj2");
      assertThat(entry).isNotNull();

      val = entry.getValue();
      assertThat(val).isInstanceOf(EntryValueNode.class);

      EntryValueNode node = (EntryValueNode) val;
      String type = node.getType();
      assertThat(type).contains("java.lang.StringBuilder");
      assertThat(node.isPrimitiveOrString()).isFalse();

      EntryValueNode[] fields = node.getChildren();
      assertThat(fields).isNotNull();

      getLogWriter().warning(
          "The tests use StringBuilders for values which might be implemented differently in jdk 1.5");

      /// test destruction in the last valid app
      int lastApplication = applications.length - 1;
      if (whichApplication == lastApplication) {
        int expectedSize = subregions.size() - 1;
        Region subRegion = (Region) subregions.iterator().next();
        Region rootRegion = root;
        subRegion.destroyRegion();

        await()
            .untilAsserted(
                () -> assertThat(rootRegion.subregions(false).size()).isEqualTo(expectedSize));
      }
    }
  }

  @Override
  public void alert(Alert alert) {
    getLogWriter().info("DEBUG: alert=" + alert);
  }

  private void populateCache() {
    for (int i = 0; i < Host.getHostCount(); i++) {
      Host host = Host.getHost(i);

      for (int j = 0; j < host.getVMCount(); j++) {
        VM vm = host.getVM(j);
        vm.invoke(() -> {
          AttributesFactory attributesFactory = new AttributesFactory();
          attributesFactory.setScope(Scope.DISTRIBUTED_NO_ACK);

          RegionAttributes regionAttributes = attributesFactory.create();
          createRegion("cdm-testSubRegion1", regionAttributes);
          createRegion("cdm-testSubRegion2", regionAttributes);
          createRegion("cdm-testSubRegion3", regionAttributes);
          remoteCreateEntry("", "cacheObj1", null);
          StringBuilder val = new StringBuilder("userDefValue1");
          remoteCreateEntry("", "cacheObj2", val);
        });
      }
    }
  }

  /**
   * Puts (or creates) a value in a region named <code>regionName</code> named
   * <code>entryName</code>.
   */
  private void remoteCreateEntry(String regionName, String entryName, Object value)
      throws CacheException {

    Region root = getRootRegion();
    Region region = root.getSubregion(regionName);
    region.create(entryName, value);

    logger.info("Put value " + value + " in entry " + entryName + " in region '"
        + region.getFullPath() + "'");
  }

  private boolean acquireDistributedLock(VM vm, String lockName) {
    return vm.invoke(() -> remoteAcquireDistLock(lockName));
  }

  private boolean remoteAcquireDistLock(String lockName) {
    String serviceName = "cdmtest_service";
    DistributedLockService service = DistributedLockService.getServiceNamed(serviceName);
    if (service == null) {
      service =
          DistributedLockService.create(serviceName, InternalDistributedSystem.getAnyInstance());
    }
    assertThat(service).isNotNull();
    return service.lock(lockName, 1000, 3000);
  }

  private VM findVMForAdminObject(GemFireVM gemFireVM) {
    for (int i = 0; i < Host.getHostCount(); i++) {
      Host host = Host.getHost(i);
      for (int j = 0; j < host.getVMCount(); j++) {
        VM vm = host.getVM(j);
        InternalDistributedMember member = getJavaGroupsIdForVM(vm);
        if (gemFireVM.getId().equals(member)) {
          return vm;
        }
      }
    }
    return null;
  }

  private InternalDistributedMember getJavaGroupsIdForVM(VM vm) {
    return vm.invoke(this::remoteGetJavaGroupsIdForVM);
  }

  private InternalDistributedMember remoteGetJavaGroupsIdForVM() {
    InternalDistributedSystem system = InternalDistributedSystem.getAnyInstance();
    return system.getDistributionManager().getDistributionManagerId();
  }

  private InternalLogWriter getLogWriter() {
    return (InternalLogWriter) getSystem().getLogWriter();
  }
}
