/*
 * 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 java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.apache.geode.internal.process.ProcessUtils.identifyPid;
import static org.apache.geode.management.internal.MBeanJMXAdapter.getDistributedLockServiceName;
import static org.apache.geode.management.internal.MBeanJMXAdapter.getLockServiceMBeanName;
import static org.assertj.core.api.Assertions.assertThat;

import java.io.Serializable;
import java.util.Map;
import java.util.Set;

import javax.management.ObjectName;

import org.awaitility.Awaitility;
import org.awaitility.core.ConditionFactory;
import org.junit.Rule;
import org.junit.Test;

import org.apache.geode.distributed.DistributedLockService;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.internal.locks.DLockService;
import org.apache.geode.management.internal.SystemManagementService;
import org.apache.geode.test.dunit.VM;


@SuppressWarnings({"serial", "unused"})
public class DLockManagementDUnitTest implements Serializable {

  private static final int MAX_WAIT_MILLIS = 2 * 60 * 1000; // 2 MINUTES

  private static final String LOCK_SERVICE_NAME =
      DLockManagementDUnitTest.class.getSimpleName() + "_testLockService";

  @Member
  private VM[] memberVMs;

  @Manager
  private VM managerVM;

  @Rule
  public ManagementTestRule managementTestRule =
      ManagementTestRule.builder().defineManagersFirst(false).start(true).build();

  @Test
  public void testLockServiceMXBean() throws Throwable {
    createLockServiceGrantor(this.memberVMs[0]);
    createLockService(this.memberVMs[1]);
    createLockService(this.memberVMs[2]);

    for (VM memberVM : this.memberVMs) {
      verifyLockServiceMXBeanInMember(memberVM);
    }
    verifyLockServiceMXBeanInManager(this.managerVM);

    for (VM memberVM : this.memberVMs) {
      closeLockService(memberVM);
    }
  }

  @Test
  public void testDistributedLockServiceMXBean() throws Throwable {
    createLockServiceGrantor(this.memberVMs[0]);
    createLockService(this.memberVMs[1]);
    createLockService(this.memberVMs[2]);

    verifyDistributedLockServiceMXBean(this.managerVM, 3);

    DistributedMember member = this.managementTestRule.getDistributedMember(this.memberVMs[2]);
    verifyFetchOperations(this.managerVM, member);

    createLockService(this.managerVM);
    verifyDistributedLockServiceMXBean(this.managerVM, 4);

    for (VM memberVM : this.memberVMs) {
      closeLockService(memberVM);
    }
    verifyProxyCleanupInManager(this.managerVM);
    verifyDistributedLockServiceMXBean(this.managerVM, 1);

    closeLockService(this.managerVM);
    verifyDistributedLockServiceMXBean(this.managerVM, 0);
  }

  private void verifyProxyCleanupInManager(final VM managerVM) {
    managerVM.invoke("verifyProxyCleanupInManager", () -> {
      Set<DistributedMember> otherMembers = this.managementTestRule.getOtherNormalMembers();
      SystemManagementService service = this.managementTestRule.getSystemManagementService();

      for (final DistributedMember member : otherMembers) {
        ObjectName objectName = service.getRegionMBeanName(member, LOCK_SERVICE_NAME);
        await()
            .untilAsserted(() -> assertThat(lockServiceMXBeanIsGone(service, objectName)).isTrue());
      }
    });
  }

  private boolean lockServiceMXBeanIsGone(final SystemManagementService service,
      final ObjectName objectName) {
    return service.getMBeanProxy(objectName, LockServiceMXBean.class) == null;
  }

  private void createLockServiceGrantor(final VM memberVM) {
    memberVM.invoke("createLockServiceGrantor", () -> {
      assertThat(DistributedLockService.getServiceNamed(LOCK_SERVICE_NAME)).isNull();

      DLockService lockService = (DLockService) DistributedLockService.create(LOCK_SERVICE_NAME,
          this.managementTestRule.getCache().getDistributedSystem());
      DistributedMember grantor = lockService.getLockGrantorId().getLockGrantorMember();
      assertThat(grantor).isNotNull();

      LockServiceMXBean lockServiceMXBean = awaitLockServiceMXBean(LOCK_SERVICE_NAME);

      assertThat(lockServiceMXBean).isNotNull();
      assertThat(lockServiceMXBean.isDistributed()).isTrue();
      assertThat(lockServiceMXBean.getName()).isEqualTo(LOCK_SERVICE_NAME);
      assertThat(lockServiceMXBean.isLockGrantor()).isTrue();
      assertThat(lockServiceMXBean.fetchGrantorMember())
          .isEqualTo(this.managementTestRule.getDistributedMember().getId());
    });
  }

  private void createLockService(final VM anyVM) {
    anyVM.invoke("createLockService", () -> {
      assertThat(DistributedLockService.getServiceNamed(LOCK_SERVICE_NAME)).isNull();

      DistributedLockService.create(LOCK_SERVICE_NAME,
          this.managementTestRule.getCache().getDistributedSystem());

      LockServiceMXBean lockServiceMXBean = awaitLockServiceMXBean(LOCK_SERVICE_NAME);

      assertThat(lockServiceMXBean).isNotNull();
      assertThat(lockServiceMXBean.isDistributed()).isTrue();
      assertThat(lockServiceMXBean.isLockGrantor()).isFalse();
    });
  }

  private void closeLockService(final VM anyVM) {
    anyVM.invoke("closeLockService", () -> {
      assertThat(DistributedLockService.getServiceNamed(LOCK_SERVICE_NAME)).isNotNull();
      DistributedLockService.destroy(LOCK_SERVICE_NAME);

      awaitLockServiceMXBeanIsNull(LOCK_SERVICE_NAME);

      ManagementService service = this.managementTestRule.getManagementService();
      LockServiceMXBean lockServiceMXBean = service.getLocalLockServiceMBean(LOCK_SERVICE_NAME);
      assertThat(lockServiceMXBean).isNull();
    });
  }

  private void verifyLockServiceMXBeanInMember(final VM memberVM) {
    memberVM.invoke("verifyLockServiceMXBeanInManager", () -> {
      DistributedLockService lockService =
          DistributedLockService.getServiceNamed(LOCK_SERVICE_NAME);
      lockService.lock("lockObject_" + identifyPid(), MAX_WAIT_MILLIS, -1);

      ManagementService service = this.managementTestRule.getManagementService();
      LockServiceMXBean lockServiceMXBean = service.getLocalLockServiceMBean(LOCK_SERVICE_NAME);
      assertThat(lockServiceMXBean).isNotNull();

      String[] listHeldLock = lockServiceMXBean.listHeldLocks();
      assertThat(listHeldLock).hasSize(1);

      Map<String, String> lockThreadMap = lockServiceMXBean.listThreadsHoldingLock();
      assertThat(lockThreadMap).hasSize(1);
    });
  }

  /**
   * Verify lock data from remote Managing node
   */
  private void verifyLockServiceMXBeanInManager(final VM managerVM) throws Exception {
    managerVM.invoke("verifyLockServiceMXBeanInManager", () -> {
      Set<DistributedMember> otherMembers = this.managementTestRule.getOtherNormalMembers();

      for (DistributedMember member : otherMembers) {
        LockServiceMXBean lockServiceMXBean =
            awaitLockServiceMXBeanProxy(member, LOCK_SERVICE_NAME);
        assertThat(lockServiceMXBean).isNotNull();

        String[] listHeldLock = lockServiceMXBean.listHeldLocks();
        assertThat(listHeldLock).hasSize(1);

        Map<String, String> lockThreadMap = lockServiceMXBean.listThreadsHoldingLock();
        assertThat(lockThreadMap).hasSize(1);
      }
    });
  }

  private void verifyFetchOperations(final VM memberVM, final DistributedMember member) {
    memberVM.invoke("verifyFetchOperations", () -> {
      ManagementService service = this.managementTestRule.getManagementService();

      DistributedSystemMXBean distributedSystemMXBean = awaitDistributedSystemMXBean();
      ObjectName distributedLockServiceMXBeanName =
          getDistributedLockServiceName(LOCK_SERVICE_NAME);
      assertThat(distributedSystemMXBean.fetchDistributedLockServiceObjectName(LOCK_SERVICE_NAME))
          .isEqualTo(distributedLockServiceMXBeanName);

      ObjectName lockServiceMXBeanName = getLockServiceMBeanName(member.getId(), LOCK_SERVICE_NAME);
      assertThat(
          distributedSystemMXBean.fetchLockServiceObjectName(member.getId(), LOCK_SERVICE_NAME))
              .isEqualTo(lockServiceMXBeanName);
    });
  }

  /**
   * Verify Aggregate MBean
   */
  private void verifyDistributedLockServiceMXBean(final VM managerVM, final int memberCount) {
    managerVM.invoke("verifyDistributedLockServiceMXBean", () -> {
      ManagementService service = this.managementTestRule.getManagementService();

      if (memberCount == 0) {
        await().untilAsserted(
            () -> assertThat(service.getDistributedLockServiceMXBean(LOCK_SERVICE_NAME)).isNull());
        return;
      }

      DistributedLockServiceMXBean distributedLockServiceMXBean =
          awaitDistributedLockServiceMXBean(LOCK_SERVICE_NAME, memberCount);
      assertThat(distributedLockServiceMXBean).isNotNull();
      assertThat(distributedLockServiceMXBean.getName()).isEqualTo(LOCK_SERVICE_NAME);
    });
  }

  private DistributedSystemMXBean awaitDistributedSystemMXBean() {
    ManagementService service = this.managementTestRule.getManagementService();

    await().untilAsserted(() -> assertThat(service.getDistributedSystemMXBean()).isNotNull());

    return service.getDistributedSystemMXBean();
  }

  /**
   * Await and return a DistributedRegionMXBean proxy with specified member count.
   */
  private DistributedLockServiceMXBean awaitDistributedLockServiceMXBean(
      final String lockServiceName, final int memberCount) {
    ManagementService service = this.managementTestRule.getManagementService();

    await().untilAsserted(() -> {
      assertThat(service.getDistributedLockServiceMXBean(lockServiceName)).isNotNull();
      assertThat(service.getDistributedLockServiceMXBean(lockServiceName).getMemberCount())
          .isEqualTo(memberCount);
    });

    return service.getDistributedLockServiceMXBean(lockServiceName);
  }

  /**
   * Await and return a LockServiceMXBean proxy for a specific member and lockServiceName.
   */
  private LockServiceMXBean awaitLockServiceMXBeanProxy(final DistributedMember member,
      final String lockServiceName) {
    SystemManagementService service = this.managementTestRule.getSystemManagementService();
    ObjectName lockServiceMXBeanName = service.getLockServiceMBeanName(member, lockServiceName);

    await().untilAsserted(
        () -> assertThat(service.getMBeanProxy(lockServiceMXBeanName, LockServiceMXBean.class))
            .isNotNull());

    return service.getMBeanProxy(lockServiceMXBeanName, LockServiceMXBean.class);
  }

  /**
   * Await creation of local LockServiceMXBean for specified lockServiceName.
   */
  private LockServiceMXBean awaitLockServiceMXBean(final String lockServiceName) {
    SystemManagementService service = this.managementTestRule.getSystemManagementService();

    await().untilAsserted(
        () -> assertThat(service.getLocalLockServiceMBean(lockServiceName)).isNotNull());

    return service.getLocalLockServiceMBean(lockServiceName);
  }

  /**
   * Await destruction of local LockServiceMXBean for specified lockServiceName.
   */
  private void awaitLockServiceMXBeanIsNull(final String lockServiceName) {
    SystemManagementService service = this.managementTestRule.getSystemManagementService();

    await().untilAsserted(
        () -> assertThat(service.getLocalLockServiceMBean(lockServiceName)).isNull());
  }

  private ConditionFactory await() {
    return Awaitility.await().atMost(MAX_WAIT_MILLIS, MILLISECONDS);
  }
}
