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

import static org.apache.geode.cache.Region.SEPARATOR;
import static org.apache.geode.distributed.ConfigurationProperties.ENABLE_CLUSTER_CONFIGURATION;
import static org.apache.geode.distributed.ConfigurationProperties.JMX_MANAGER_HTTP_PORT;
import static org.apache.geode.distributed.ConfigurationProperties.LOCATORS;
import static org.apache.geode.distributed.ConfigurationProperties.LOG_LEVEL;
import static org.apache.geode.distributed.ConfigurationProperties.MCAST_PORT;
import static org.apache.geode.internal.AvailablePortHelper.getRandomAvailableTCPPort;
import static org.apache.geode.internal.cache.GemFireCacheImpl.getInstance;
import static org.apache.geode.management.ManagementService.getManagementService;
import static org.apache.geode.management.internal.MBeanJMXAdapter.getClientServiceMBeanName;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Properties;

import javax.management.InstanceNotFoundException;
import javax.management.MBeanServer;
import javax.management.Notification;
import javax.management.NotificationListener;
import javax.management.ObjectName;

import org.junit.Ignore;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.cache.Cache;
import org.apache.geode.cache.client.internal.LocatorTestBase;
import org.apache.geode.cache.query.IndexExistsException;
import org.apache.geode.cache.query.IndexInvalidException;
import org.apache.geode.cache.query.IndexNameConflictException;
import org.apache.geode.cache.query.QueryService;
import org.apache.geode.cache.query.RegionNotFoundException;
import org.apache.geode.cache.query.cq.dunit.CqQueryDUnitTest;
import org.apache.geode.cache.query.internal.cq.CqService;
import org.apache.geode.cache.server.CacheServer;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.Locator;
import org.apache.geode.internal.AvailablePortHelper;
import org.apache.geode.internal.cache.GemFireCacheImpl;
import org.apache.geode.management.internal.JmxManagerLocatorRequest;
import org.apache.geode.management.internal.MBeanJMXAdapter;
import org.apache.geode.management.internal.SystemManagementService;
import org.apache.geode.test.awaitility.GeodeAwaitility;
import org.apache.geode.test.dunit.Assert;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.LogWriterUtils;
import org.apache.geode.test.dunit.NetworkUtils;
import org.apache.geode.test.dunit.SerializableRunnable;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.Wait;
import org.apache.geode.test.dunit.WaitCriterion;
import org.apache.geode.test.junit.categories.ClientSubscriptionTest;

/**
 * Cache Server related management test cases
 */
@Category({ClientSubscriptionTest.class})
public class CacheServerManagementDUnitTest extends LocatorTestBase {

  private static final long serialVersionUID = 1L;

  private static final int CONNECT_LOCATOR_TIMEOUT_MS = 30000;

  private final ManagementTestBase helper;

  private static final String queryName = "testClientWithFeederAndCQ_0";

  private static final String indexName = "testIndex";

  private static final MBeanServer mbeanServer = MBeanJMXAdapter.mbeanServer;

  protected CqQueryDUnitTest cqDUnitTest = new CqQueryDUnitTest();

  public CacheServerManagementDUnitTest() {
    super();
    helper = new ManagementTestBase() {
      {
      }
    };

  }

  @Override
  public final void preSetUp() throws Exception {
    disconnectAllFromDS();
  }

  @Override
  protected final void postTearDownLocatorTestBase() throws Exception {
    disconnectAllFromDS();
  }

  @Test
  public void testCacheServerMBean() throws Exception {
    final Host host = Host.getHost(0);
    VM server = host.getVM(0);
    VM client = host.getVM(1);
    VM managingNode = host.getVM(2);

    // Managing Node is created first
    helper.createManagementCache(managingNode);
    helper.startManagingNode(managingNode);
    // helper.createCache(server);
    int serverPort = AvailablePortHelper.getRandomAvailableTCPPort();
    cqDUnitTest.createServer(server, serverPort);

    DistributedMember member = helper.getMember(server);

    verifyCacheServer(server, serverPort);

    final int port = server.invoke(CqQueryDUnitTest::getCacheServerPort);
    final String host0 = NetworkUtils.getServerHostName(server.getHost());

    // Create client.
    cqDUnitTest.createClient(client, port, host0);

    cqDUnitTest.createCQ(client, queryName, cqDUnitTest.cqs[0]);
    cqDUnitTest.executeCQ(client, queryName, false, null);

    final int size = 10;
    cqDUnitTest.createValues(client, cqDUnitTest.regions[0], size);
    cqDUnitTest.waitForCreated(client, queryName, CqQueryDUnitTest.KEY + size);

    cqDUnitTest.validateCQ(client, queryName, /* resultSize: */CqQueryDUnitTest.noTest,
        /* creates: */size, /* updates: */0, /* deletes; */0, /* queryInserts: */size,
        /* queryUpdates: */0, /* queryDeletes: */0, /* totalEvents: */size);

    // Close.

    Wait.pause(2000);
    checkNavigation(managingNode, member, serverPort);
    verifyIndex(server, serverPort);
    // This will test all CQs and will close the cq in its final step
    verifyCacheServerRemote(managingNode, member, serverPort);

    verifyClosedCQ(server);

    cqDUnitTest.closeClient(client);
    cqDUnitTest.closeServer(server);
    helper.stopManagingNode(managingNode);
    helper.closeCache(client);
    helper.closeCache(server);
    helper.closeCache(managingNode);
  }

  /**
   * Test for client server connection related management artifacts like notifications
   *
   */

  @Test
  public void testCacheClient() throws Exception {

    final Host host = Host.getHost(0);
    VM locator = host.getVM(0);
    VM server = host.getVM(1);
    VM client = host.getVM(2);

    int locatorPort = getRandomAvailableTCPPort();
    locator.invoke("Start Locator", () -> startLocator(locator.getHost(), locatorPort, ""));

    String locators = NetworkUtils.getServerHostName(locator.getHost()) + "[" + locatorPort + "]";

    int serverPort = server.invoke("Start BridgeServer", () -> startBridgeServer(null, locators));

    addClientNotifListener(server, serverPort);

    // Start a client and make sure that proper notification is received
    client.invoke("Start BridgeClient", () -> startBridgeClient(null,
        NetworkUtils.getServerHostName(locator.getHost()), locatorPort));

    // stop the client and make sure the cache server notifies
    stopBridgeMemberVM(client);
    helper.closeCache(locator);
    helper.closeCache(server);
    helper.closeCache(client);

  }

  /**
   * Intention of this test is to check if a node becomes manager after all the nodes are alive it
   * should have all the information of all the members.
   * <p>
   * Thats why used service.getLocalManager().runManagementTaskAdhoc() to make node ready for
   * federation when manager node comes up
   *
   */

  // renable when bug 46138
  @Ignore("Bug46049")
  @Test
  public void testBug46049() throws Exception {
    final Host host = Host.getHost(0);
    VM locator = host.getVM(0);
    VM server = host.getVM(1);

    // Step 1:
    final int locatorPort = getRandomAvailableTCPPort();
    locator.invoke("Start Locator", () -> startLocator(locator.getHost(), locatorPort, ""));

    String locators = NetworkUtils.getServerHostName(locator.getHost()) + "[" + locatorPort + "]";

    // Step 2:
    server.invoke("Start BridgeServer", () -> startBridgeServer(null, locators));

    // Step 3:
    server.invoke("Check Server", () -> {
      Cache cache = GemFireCacheImpl.getInstance();
      assertNotNull(cache);
      SystemManagementService service =
          (SystemManagementService) ManagementService.getExistingManagementService(cache);
      assertNotNull(service);
      assertFalse(service.isManager());
      assertNotNull(service.getMemberMXBean());
      service.getLocalManager().runManagementTaskAdhoc();
    });

    // Step 4:
    JmxManagerLocatorRequest.send(locator.getHost().getHostName(), locatorPort,
        CONNECT_LOCATOR_TIMEOUT_MS, new Properties());

    // Step 5:
    locator.invoke("Check locator", () -> {
      Cache cache = GemFireCacheImpl.getInstance();
      assertNotNull(cache);
      ManagementService service = ManagementService.getExistingManagementService(cache);
      assertNotNull(service);
      assertTrue(service.isManager());
      LocatorMXBean bean = service.getLocalLocatorMXBean();
      assertEquals(locatorPort, bean.getPort());
      DistributedSystemMXBean dsBean = service.getDistributedSystemMXBean();

      assertEquals(2, dsBean.listMemberObjectNames().length);
    });

    helper.closeCache(locator);
    helper.closeCache(server);

  }

  protected void startLocator(Host vmHost, final int locatorPort, final String otherLocators) {
    disconnectFromDS();
    Properties props = new Properties();
    props.setProperty(MCAST_PORT, String.valueOf(0));
    props.setProperty(LOCATORS, otherLocators);
    props.setProperty(LOG_LEVEL, LogWriterUtils.getDUnitLogLevel());
    props.setProperty(JMX_MANAGER_HTTP_PORT, "0");
    props.setProperty(ENABLE_CLUSTER_CONFIGURATION, "false");
    File logFile = new File(getUniqueName() + "-locator" + locatorPort + ".log");
    try {
      InetAddress bindAddr = InetAddress.getByName(NetworkUtils.getServerHostName(vmHost));
      Locator locator = Locator.startLocatorAndDS(locatorPort, logFile, bindAddr, props);
      remoteObjects.put(LOCATOR_KEY, locator);
    } catch (UnknownHostException uhe) {
      Assert.fail("While resolving bind address ", uhe);
    } catch (IOException ex) {
      Assert.fail("While starting locator on port " + locatorPort, ex);
    }
  }

  protected void checkNavigation(final VM vm, final DistributedMember cacheServerMember,
      final int serverPort) {
    SerializableRunnable checkNavigation = new SerializableRunnable("Check Navigation") {
      @Override
      public void run() {

        final ManagementService service = ManagementTestBase.getManagementService();

        DistributedSystemMXBean disMBean = service.getDistributedSystemMXBean();
        try {
          ObjectName expected =
              MBeanJMXAdapter.getClientServiceMBeanName(serverPort, cacheServerMember.getId());
          ObjectName actual =
              disMBean.fetchCacheServerObjectName(cacheServerMember.getId(), serverPort);
          assertEquals(expected, actual);
        } catch (Exception e) {
          fail("Cache Server Navigation Failed " + e);
        }

        try {
          assertEquals(1, disMBean.listCacheServerObjectNames().length);
        } catch (Exception e) {
          fail("Cache Server Navigation Failed " + e);
        }

      }
    };
    vm.invoke(checkNavigation);
  }

  /**
   * Verify the Cache Server details
   *
   */
  @SuppressWarnings("serial")
  protected void addClientNotifListener(final VM vm, final int serverPort) throws Exception {
    SerializableRunnable addClientNotifListener =
        new SerializableRunnable("Add Client Notif Listener") {
          @Override
          public void run() {
            GemFireCacheImpl cache = getInstance();
            ManagementService service = getManagementService(cache);
            final CacheServerMXBean bean = service.getLocalCacheServerMXBean(serverPort);
            assertNotNull(bean);
            WaitCriterion ev = new WaitCriterion() {
              @Override
              public boolean done() {
                return bean.isRunning();
              }

              @Override
              public String description() {
                return null;
              }
            };
            GeodeAwaitility.await().untilAsserted(ev);
            assertTrue(bean.isRunning());
            TestCacheServerNotif nt = new TestCacheServerNotif();
            try {
              mbeanServer.addNotificationListener(getClientServiceMBeanName(
                  serverPort, cache.getDistributedSystem().getMemberId()), nt, null, null);
            } catch (InstanceNotFoundException e) {
              throw new RuntimeException("Failed With Exception ", e);
            }

          }
        };
    vm.invoke(addClientNotifListener);
  }

  /**
   * Verify the closed CQ which is closed from Managing Node
   *
   */
  @SuppressWarnings("serial")
  protected void verifyIndex(final VM vm, final int serverPort) throws Exception {
    SerializableRunnable verifyIndex = new SerializableRunnable("Verify Index ") {
      @Override
      public void run() {
        GemFireCacheImpl cache = GemFireCacheImpl.getInstance();
        ManagementService service = ManagementService.getManagementService(cache);
        QueryService qs = cache.getQueryService();
        try {
          qs.createIndex(indexName, "p.ID",
              SEPARATOR + "root" + SEPARATOR + cqDUnitTest.regions[0]);
        } catch (RegionNotFoundException e) {
          fail("Failed With Exception " + e);
        } catch (IndexInvalidException e) {
          fail("Failed With Exception " + e);
        } catch (IndexNameConflictException e) {
          fail("Failed With Exception " + e);
        } catch (IndexExistsException e) {
          fail("Failed With Exception " + e);
        } catch (UnsupportedOperationException e) {
          fail("Failed With Exception " + e);
        }

        CacheServerMXBean bean = service.getLocalCacheServerMXBean(serverPort);
        assertEquals(bean.getIndexCount(), 1);
        LogWriterUtils.getLogWriter()
            .info("<ExpectedString> Index is   " + bean.getIndexList()[0] + "</ExpectedString> ");
        try {
          bean.removeIndex(indexName);
        } catch (Exception e) {
          fail("Failed With Exception " + e);

        }
        assertEquals(bean.getIndexCount(), 0);

      }
    };
    vm.invoke(verifyIndex);
  }

  /**
   * Verify the closed CQ which is closed from Managing Node
   *
   */
  @SuppressWarnings("serial")
  protected void verifyClosedCQ(final VM vm) throws Exception {
    SerializableRunnable verifyClosedCQ = new SerializableRunnable("Verify Closed CQ") {
      @Override
      public void run() {
        CqService cqService = GemFireCacheImpl.getInstance().getCqService();
        if (cqService != null) {
          assertNull(cqService.getCq(queryName));
        }

      }
    };
    vm.invoke(verifyClosedCQ);
  }

  /**
   * Verify the Cache Server details
   *
   */
  @SuppressWarnings("serial")
  protected void verifyCacheServer(final VM vm, final int serverPort) throws Exception {
    SerializableRunnable verifyCacheServer = new SerializableRunnable("Verify Cache Server") {
      @Override
      public void run() {
        GemFireCacheImpl cache = getInstance();
        ManagementService service = getManagementService(cache);
        final CacheServerMXBean bean = service.getLocalCacheServerMXBean(serverPort);
        assertNotNull(bean);
        WaitCriterion ev = new WaitCriterion() {
          @Override
          public boolean done() {
            return bean.isRunning();
          }

          @Override
          public String description() {
            return null;
          }
        };
        GeodeAwaitility.await().untilAsserted(ev);
        assertTrue(bean.isRunning());
        assertCacheServerConfig(bean);

      }
    };
    vm.invoke(verifyCacheServer);
  }

  protected void assertCacheServerConfig(CacheServerMXBean bean) {
    // assertIndexDetailsEquals(ServerInfo.getInstance().getServerPort(), bean.getPort());
    assertEquals(CacheServer.DEFAULT_BIND_ADDRESS, bean.getBindAddress());
    assertEquals(CacheServer.DEFAULT_HOSTNAME_FOR_CLIENTS, bean.getHostNameForClients());
    assertEquals(CacheServer.DEFAULT_SOCKET_BUFFER_SIZE, bean.getSocketBufferSize());
    assertEquals(CacheServer.DEFAULT_MAXIMUM_TIME_BETWEEN_PINGS, bean.getMaximumTimeBetweenPings());
    assertEquals(CacheServer.DEFAULT_MAX_CONNECTIONS, bean.getMaxConnections());
    assertEquals(CacheServer.DEFAULT_MAX_THREADS, bean.getMaxThreads());
    assertEquals(CacheServer.DEFAULT_MAXIMUM_MESSAGE_COUNT, bean.getMaximumMessageCount());
    assertEquals(CacheServer.DEFAULT_MESSAGE_TIME_TO_LIVE, bean.getMessageTimeToLive());
    assertEquals(CacheServer.DEFAULT_LOAD_POLL_INTERVAL, bean.getLoadPollInterval());
    LogWriterUtils.getLogWriter().info("<ExpectedString> LoadProbe of the Server is  "
        + bean.fetchLoadProbe().toString() + "</ExpectedString> ");
  }

  /**
   * Verify the Cache Server details
   *
   */
  @SuppressWarnings("serial")
  protected void verifyCacheServerRemote(final VM vm, final DistributedMember serverMember,
      final int serverPort) {
    SerializableRunnable verifyCacheServerRemote =
        new SerializableRunnable("Verify Cache Server Remote") {
          @Override
          public void run() {
            GemFireCacheImpl cache = GemFireCacheImpl.getInstance();
            try {

              CacheServerMXBean bean = MBeanUtil.getCacheServerMbeanProxy(serverMember, serverPort);

              // Check for bean configuration
              assertCacheServerConfig(bean);

              String clientId = bean.getClientIds()[0];
              assertNotNull(clientId);
              LogWriterUtils.getLogWriter().info(
                  "<ExpectedString> ClientId of the Server is  " + clientId + "</ExpectedString> ");
              LogWriterUtils.getLogWriter().info("<ExpectedString> Active Query Count  "
                  + bean.getActiveCQCount() + "</ExpectedString> ");

              LogWriterUtils.getLogWriter().info("<ExpectedString> Registered Query Count  "
                  + bean.getRegisteredCQCount() + "</ExpectedString> ");

              assertTrue(bean.showAllClientStats()[0].getClientCQCount() == 1);
              int numQueues = bean.getNumSubscriptions();
              assertEquals(numQueues, 1);

              bean.getContinuousQueryList();
              // Only temporarily stops the query
              bean.stopContinuousQuery("testClientWithFeederAndCQ_0");

              // Start a stopped query
              bean.executeContinuousQuery("testClientWithFeederAndCQ_0");

              // Close the continuous query
              bean.closeContinuousQuery("testClientWithFeederAndCQ_0");
            } catch (Exception e) {
              fail("Error while verifying cache server from remote member " + e);
            }

          }
        };
    vm.invoke(verifyCacheServerRemote);
  }

  /**
   * Notification handler
   */
  private static class TestCacheServerNotif implements NotificationListener {

    @Override
    public void handleNotification(Notification notification, Object handback) {
      assertNotNull(notification);
      LogWriterUtils.getLogWriter().info("Expected String :" + notification);
    }

  }

}
