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

import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.stream.Collectors.joining;
import static org.apache.geode.distributed.ConfigurationProperties.ENABLE_CLUSTER_CONFIGURATION;
import static org.apache.geode.distributed.ConfigurationProperties.LOAD_CLUSTER_CONFIGURATION_FROM_DIR;
import static org.apache.geode.distributed.ConfigurationProperties.LOCATORS;
import static org.apache.geode.distributed.ConfigurationProperties.LOG_FILE;
import static org.apache.geode.distributed.ConfigurationProperties.MCAST_PORT;
import static org.apache.geode.distributed.ConfigurationProperties.NAME;
import static org.apache.geode.distributed.ConfigurationProperties.USE_CLUSTER_CONFIGURATION;
import static org.apache.geode.internal.AvailablePortHelper.getRandomAvailableTCPPorts;
import static org.apache.geode.test.dunit.Host.getHost;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;

import org.awaitility.Awaitility;
import org.awaitility.core.ConditionFactory;
import org.apache.geode.cache.Region;
import org.apache.geode.distributed.Locator;
import org.apache.geode.distributed.internal.ClusterConfigurationService;
import org.apache.geode.distributed.internal.InternalLocator;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.cache.internal.JUnit4CacheTestCase;
import org.apache.geode.test.junit.categories.DistributedTest;
import org.apache.geode.test.junit.categories.FlakyTest;
import org.apache.geode.util.test.TestUtil;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Properties;

@Category({DistributedTest.class, FlakyTest.class}) // GEODE-1165
public class ClusterConfigurationServiceUsingDirDUnitTest extends JUnit4CacheTestCase {

  @Override
  public final void preTearDownCacheTestCase() throws Exception {
    for (int i = 0; i < 2; i++) {
      VM vm = getHost(0).getVM(i);
      vm.invoke("Removing shared configuration", () -> {
        InternalLocator locator = InternalLocator.getLocator();
        if (locator == null) {
          return;
        }

        ClusterConfigurationService sharedConfig = locator.getSharedConfiguration();
        if (sharedConfig != null) {
          sharedConfig.destroySharedConfiguration();
        }
      });
    }
  }

  @Test
  public void basicClusterConfigDirWithOneLocator() throws Exception {
    final int[] ports = getRandomAvailableTCPPorts(1);
    final int locatorCount = ports.length;

    for (int i = 0; i < locatorCount; i++) {
      VM vm = getHost(0).getVM(i);
      copyClusterXml(vm, "cluster-region.xml");
      startLocator(vm, i, ports);
      waitForSharedConfiguration(vm);
    }

    for (int i = 2; i < 4; i++) {
      VM vm = getHost(0).getVM(i);
      restartCache(vm, i, ports);

      vm.invoke("Checking for region presence", () -> {
        await().until(() -> getRootRegion("newReplicatedRegion") != null);
      });
    }
  }

  @Test
  public void basicClusterConfigDirWithTwoLocators() throws Exception {
    final int[] ports = getRandomAvailableTCPPorts(2);
    final int locatorCount = ports.length;

    for (int i = 0; i < locatorCount; i++) {
      VM vm = getHost(0).getVM(i);
      copyClusterXml(vm, "cluster-region.xml");
      startLocator(vm, i, ports);
      waitForSharedConfiguration(vm);
    }

    for (int i = 2; i < 4; i++) {
      VM vm = getHost(0).getVM(i);
      restartCache(vm, i, ports);

      vm.invoke("Checking for region presence", () -> {
        await().until(() -> getRootRegion("newReplicatedRegion") != null);
      });
    }
  }

  @Test
  public void updateClusterConfigDirWithTwoLocatorsNoRollingServerRestart() throws Exception {
    final int[] ports = getRandomAvailableTCPPorts(2);
    final int locatorCount = ports.length;

    for (int i = 0; i < locatorCount; i++) {
      VM vm = getHost(0).getVM(i);
      copyClusterXml(vm, "cluster-empty.xml");
      startLocator(vm, i, ports);
      waitForSharedConfiguration(vm);
    }

    for (int i = 2; i < 4; i++) {
      VM vm = getHost(0).getVM(i);
      restartCache(vm, i, ports);

      vm.invoke("Checking for region absence", () -> {
        Region r = getRootRegion("newReplicatedRegion");
        assertNull("Region does exist", r);
      });
    }

    // Shut down the locators in reverse order to how we will start them up in the next step.
    // Unless we start them asynchronously, the older one will want to wait for a new diskstore
    // to become available and will time out.
    for (int i = locatorCount; i > 0; i--) {
      VM vm = getHost(0).getVM(i - 1);
      stopLocator(vm);
    }

    for (int i = 0; i < locatorCount; i++) {
      VM vm = getHost(0).getVM(i);
      copyClusterXml(vm, "cluster-region.xml");
      startLocator(vm, i, ports);
      waitForSharedConfiguration(vm);
    }

    for (int i = 2; i < 4; i++) {
      VM vm = getHost(0).getVM(i);
      vm.invoke(() -> disconnectFromDS());
    }

    for (int i = 2; i < 4; i++) {
      VM vm = getHost(0).getVM(i);
      restartCache(vm, i, ports);

      vm.invoke("Checking for region presence", () -> {
        await().until(() -> getRootRegion("newReplicatedRegion") != null);
      });
    }
  }

  @Test
  public void updateClusterConfigDirWithTwoLocatorsAndRollingServerRestart() throws Exception {
    final int[] ports = getRandomAvailableTCPPorts(2);
    final int locatorCount = ports.length;

    for (int i = 0; i < locatorCount; i++) {
      VM vm = getHost(0).getVM(i);
      copyClusterXml(vm, "cluster-empty.xml");
      startLocator(vm, i, ports);
      waitForSharedConfiguration(vm);
    }

    for (int i = 2; i < 4; i++) {
      VM vm = getHost(0).getVM(i);
      restartCache(vm, i, ports);

      vm.invoke("Checking for region absence", () -> {
        Region r = getRootRegion("newReplicatedRegion");
        assertNull("Region does exist", r);
      });
    }

    // Shut down the locators in reverse order to how we will start them up in the next step.
    // Unless we start them asynchronously, the older one will want to wait for a new diskstore
    // to become available and will time out.
    for (int i = locatorCount; i > 0; i--) {
      VM vm = getHost(0).getVM(i - 1);
      stopLocator(vm);
    }

    for (int i = 0; i < locatorCount; i++) {
      VM vm = getHost(0).getVM(i);
      copyClusterXml(vm, "cluster-region.xml");
      startLocator(vm, i, ports);
      waitForSharedConfiguration(vm);
    }

    for (int i = 2; i < 4; i++) {
      VM vm = getHost(0).getVM(i);
      restartCache(vm, i, ports);

      vm.invoke("Checking for region presence", () -> {
        await().until(() -> getRootRegion("newReplicatedRegion") != null);
      });
    }
  }

  @Test
  public void updateClusterConfigDirWithTwoLocatorsRollingRestartAndRollingServerRestart()
      throws Exception {
    final int[] ports = getRandomAvailableTCPPorts(2);
    final int locatorCount = ports.length;

    for (int i = 0; i < locatorCount; i++) {
      VM vm = getHost(0).getVM(i);
      copyClusterXml(vm, "cluster-empty.xml");
      startLocator(vm, i, ports);
      waitForSharedConfiguration(vm);
    }

    for (int i = 2; i < 4; i++) {
      VM vm = getHost(0).getVM(i);
      restartCache(vm, i, ports);

      vm.invoke("Checking for region absence", () -> {
        Region r = getRootRegion("newReplicatedRegion");
        assertNull("Region does exist", r);
      });
    }

    // Roll the locators
    for (int i = locatorCount - 1; i >= 0; i--) {
      VM vm = getHost(0).getVM(i);
      stopLocator(vm);
      copyClusterXml(vm, "cluster-region.xml");
      startLocator(vm, i, ports);
      waitForSharedConfiguration(vm);
    }

    // Roll the servers
    for (int i = 2; i < 4; i++) {
      VM vm = getHost(0).getVM(i);
      restartCache(vm, i, ports);

      vm.invoke("Checking for region presence", () -> {
        await().until(() -> getRootRegion("newReplicatedRegion") != null);
      });
    }
  }

  private void copyClusterXml(final VM vm, final String clusterXml) {
    vm.invoke("Copying new cluster.xml from " + clusterXml, () -> {
      String clusterXmlPath =
          TestUtil.getResourcePath(ClusterConfigurationServiceUsingDirDUnitTest.class, clusterXml);
      InputStream cacheXml = new FileInputStream(clusterXmlPath);
      assertNotNull("Could not create InputStream from " + clusterXmlPath, cacheXml);
      Files.createDirectories(Paths.get("cluster_config", "cluster"));
      Files.copy(cacheXml, Paths.get("cluster_config", "cluster", "cluster.xml"),
          StandardCopyOption.REPLACE_EXISTING);
    });
  }

  private void startLocator(final VM vm, final int i, final int[] locatorPorts) {
    vm.invoke("Creating locator on " + vm, () -> {
      final String locatorName = "locator" + i;
      final File logFile = new File("locator-" + i + ".log");
      final Properties props = new Properties();
      props.setProperty(NAME, locatorName);
      props.setProperty(MCAST_PORT, "0");
      props.setProperty(ENABLE_CLUSTER_CONFIGURATION, "true");
      props.setProperty(LOAD_CLUSTER_CONFIGURATION_FROM_DIR, "true");

      if (locatorPorts.length > 1) {
        int otherLocatorPort = locatorPorts[(i + 1) % locatorPorts.length];
        props.setProperty(LOCATORS, "localhost[" + otherLocatorPort + "]");
      }

      Locator.startLocatorAndDS(locatorPorts[i], logFile, props);
    });
  }

  private void waitForSharedConfiguration(final VM vm) {
    vm.invoke("Waiting for shared configuration", () -> {
      final InternalLocator locator = InternalLocator.getLocator();
      await().until(() -> {
        return locator.isSharedConfigurationRunning();
      });
    });
  }

  private void stopLocator(final VM vm) {
    vm.invoke("Stopping locator on " + vm, () -> {
      InternalLocator locator = InternalLocator.getLocator();
      assertNotNull("No locator found", locator);
      locator.stop();
      disconnectAllFromDS();
    });
  }

  private void restartCache(final VM vm, final int i, final int[] locatorPorts) {
    vm.invoke("Creating cache on VM " + i, () -> {
      disconnectFromDS();

      final Properties props = new Properties();
      props.setProperty(NAME, "member" + i);
      props.setProperty(MCAST_PORT, "0");
      props.setProperty(LOCATORS, getLocatorStr(locatorPorts));
      props.setProperty(LOG_FILE, "server-" + i + ".log");
      props.setProperty(USE_CLUSTER_CONFIGURATION, "true");
      props.setProperty(ENABLE_CLUSTER_CONFIGURATION, "true");

      getSystem(props);
      getCache();
    });
  }

  private String getLocatorStr(final int[] locatorPorts) {
    return Arrays.stream(locatorPorts).mapToObj(p -> "localhost[" + p + "]").collect(joining(","));
  }

  private ConditionFactory await() {
    return Awaitility.await().atMost(2, MINUTES);
  }
}
