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

import org.apache.geode.SystemConnectException;
import org.apache.geode.cache.client.internal.locator.ClientConnectionRequest;
import org.apache.geode.cache.client.internal.locator.ClientConnectionResponse;
import org.apache.geode.cache.client.internal.locator.QueueConnectionRequest;
import org.apache.geode.cache.client.internal.locator.QueueConnectionResponse;
import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.distributed.internal.InternalLocator;
import org.apache.geode.distributed.internal.ServerLocation;
import org.apache.geode.distributed.internal.membership.gms.messenger.JGroupsMessenger;
import org.apache.geode.distributed.internal.tcpserver.TcpClient;
import org.apache.geode.internal.AvailablePortHelper;
import org.apache.geode.internal.OSProcess;
import org.apache.geode.internal.cache.GemFireCacheImpl;
import org.apache.geode.internal.cache.tier.sockets.ClientProxyMembershipID;
import org.apache.geode.management.internal.JmxManagerAdvisor.JmxManagerProfile;
import org.apache.geode.management.internal.configuration.messages.ConfigurationRequest;
import org.apache.geode.management.internal.configuration.messages.SharedConfigurationStatusRequest;
import org.apache.geode.test.junit.categories.IntegrationTest;
import org.apache.geode.test.junit.categories.MembershipTest;
import org.apache.geode.test.junit.runners.CategoryWithParameterizedRunnerFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.IntSupplier;

import static org.apache.geode.distributed.ConfigurationProperties.*;
import static org.apache.geode.internal.AvailablePort.SOCKET;
import static org.apache.geode.internal.AvailablePort.getRandomAvailablePort;
import static org.junit.Assert.*;

import org.awaitility.Awaitility;

@Category({IntegrationTest.class, MembershipTest.class})
@RunWith(Parameterized.class)
@Parameterized.UseParametersRunnerFactory(CategoryWithParameterizedRunnerFactory.class)
public class LocatorJUnitTest {

  private static final int REQUEST_TIMEOUT = 5 * 1000;

  private Locator locator;
  private File tmpFile;
  private int port;

  @Parameterized.Parameters
  public static Collection<Object> data() {
    return Arrays.asList(new Object[] {(IntSupplier) () -> 0,
        (IntSupplier) () -> AvailablePortHelper.getRandomAvailableTCPPort()});
  }

  @Parameterized.Parameter
  public IntSupplier portSupplier;

  @Rule
  public TemporaryFolder temporaryFolder = new TemporaryFolder();

  @Before
  public void setUp() throws IOException {
    tmpFile = File.createTempFile("locator", ".log");
    this.port = portSupplier.getAsInt();
    File locatorFile = new File("locator" + this.port + ".dat");
    if (locatorFile.exists()) {
      locatorFile.delete();
    }
  }

  @After
  public void tearDown() {
    if (locator != null) {
      locator.stop();
    }
    assertEquals(false, Locator.hasLocator());
  }

  /**
   * TRAC #45804: if jmx-manager-start is true in a locator then gfsh connect will fail
   */
  @Test
  public void testGfshConnectShouldSucceedIfJmxManagerStartIsTrueInLocator() throws Exception {
    Properties dsprops = new Properties();
    int jmxPort = getRandomAvailablePort(SOCKET);
    dsprops.setProperty(MCAST_PORT, "0");
    dsprops.setProperty(JMX_MANAGER_PORT, "" + jmxPort);
    dsprops.setProperty(JMX_MANAGER_START, "true");
    dsprops.setProperty(JMX_MANAGER_HTTP_PORT, "0");
    dsprops.setProperty(ENABLE_CLUSTER_CONFIGURATION, "false");
    dsprops.setProperty(LOG_FILE, "");
    System.setProperty(DistributionConfig.GEMFIRE_PREFIX + "disableManagement", "false"); // not
                                                                                          // needed
    try {
      locator = Locator.startLocatorAndDS(port, null, dsprops);
      List<JmxManagerProfile> alreadyManaging =
          GemFireCacheImpl.getInstance().getJmxManagerAdvisor().adviseAlreadyManaging();
      assertEquals(1, alreadyManaging.size());
      assertEquals(GemFireCacheImpl.getInstance().getMyId(),
          alreadyManaging.get(0).getDistributedMember());
    } finally {
      System.clearProperty(DistributionConfig.GEMFIRE_PREFIX + "enabledManagement");
    }
  }

  /**
   * GEODE-2253 - a locator should handle a SharedConfigurationStatusRequest regardless of whether
   * it has the service or not
   */
  @Test
  public void testHandlersAreWaitedOn() throws Exception {
    Properties dsprops = new Properties();
    int jmxPort = getRandomAvailablePort(SOCKET);
    dsprops.setProperty(MCAST_PORT, "0");
    dsprops.setProperty(ENABLE_CLUSTER_CONFIGURATION, "false");
    dsprops.setProperty(LOCATOR_WAIT_TIME, "1"); // seconds
    dsprops.setProperty(LOG_FILE, "");
    locator = Locator.startLocatorAndDS(port, null, dsprops);
    InternalLocator internalLocator = (InternalLocator) locator;
    // the locator should always install a SharedConfigurationStatusRequest handler
    assertTrue(internalLocator.hasHandlerForClass(SharedConfigurationStatusRequest.class));
    // the locator should wait if a handler isn't installed
    assertFalse(internalLocator.hasHandlerForClass(ConfigurationRequest.class));
    ConfigurationRequest request = new ConfigurationRequest();
    Object result = internalLocator.getPrimaryHandler().processRequest(request);
    assertNull(result);
    assertTrue(internalLocator.getPrimaryHandler().hasWaitedForHandlerInitialization());
  }


  @Test
  public void testBasicInfo() throws Exception {
    locator = Locator.startLocator(port, tmpFile);
    int boundPort = (port == 0) ? locator.getPort() : port;
    TcpClient client = new TcpClient();
    String[] info = client.getInfo(InetAddress.getLocalHost(), boundPort);
    assertNotNull(info);
    assertTrue(info.length > 1);
  }

  @Test
  public void testNoThreadLeftBehind() throws Exception {
    Properties dsprops = new Properties();
    dsprops.setProperty(MCAST_PORT, "0");
    dsprops.setProperty(JMX_MANAGER_START, "false");
    dsprops.setProperty(ENABLE_CLUSTER_CONFIGURATION, "false");

    JGroupsMessenger.THROW_EXCEPTION_ON_START_HOOK = true;
    int threadCount = Thread.activeCount();
    try {
      locator = Locator.startLocatorAndDS(port, new File(""), dsprops);
      locator.stop();
      fail("expected an exception");
    } catch (SystemConnectException expected) {

      for (int i = 0; i < 10; i++) {
        if (threadCount < Thread.activeCount()) {
          Thread.sleep(1000);
        }
      }
      if (threadCount < Thread.activeCount()) {
        OSProcess.printStacks(0);
        fail("expected " + threadCount + " threads or fewer but found " + Thread.activeCount()
            + ".  Check log file for a thread dump.");
      }
    } finally {
      JGroupsMessenger.THROW_EXCEPTION_ON_START_HOOK = false;
    }
  }

  /**
   * Make sure two ServerLocation objects on different hosts but with the same port are not equal
   * <p/>
   * TRAC #42040: LoadBalancing directs all traffic to a single cache server if all servers are
   * started on the same port
   */
  @Test
  public void testServerLocationOnDifferentHostsShouldNotTestEqual() {
    ServerLocation sl1 = new ServerLocation("host1", 777);
    ServerLocation sl2 = new ServerLocation("host2", 777);
    if (sl1.equals(sl2)) {
      fail("ServerLocation instances on different hosts should not test equal");
    }
  }

}
