/*
 * 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 static org.apache.geode.test.awaitility.GeodeAwaitility.await;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.catchThrowable;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import org.apache.geode.distributed.internal.DistributionManager;
import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
import org.apache.geode.test.dunit.AsyncInvocation;
import org.apache.geode.test.dunit.SerializableRunnableIF;
import org.apache.geode.test.dunit.rules.ClusterStartupRule;
import org.apache.geode.test.dunit.rules.MemberVM;

public class GrantorFailoverDUnitTest {
  private final List<MemberVM> locators = new ArrayList<>();

  @Rule
  public ClusterStartupRule clusterStartupRule = new ClusterStartupRule();

  public static final String SERVICE_NAME = "serviceName";

  @Before
  public void before() {
    locators.add(clusterStartupRule.startLocatorVM(0));
    locators.add(clusterStartupRule.startLocatorVM(1, locators.get(0).getPort()));
    locators.add(clusterStartupRule.startLocatorVM(2, locators.get(0).getPort()));


    for (MemberVM locator : locators) {
      locator.invoke((SerializableRunnableIF) () -> DistributedLockService.create(SERVICE_NAME,
          ClusterStartupRule.getCache().getDistributedSystem()));
    }
  }

  @After
  public void cleanup() {
    for (MemberVM locator : locators) {
      locator.invoke(() -> DistributedLockService.destroy(SERVICE_NAME));
    }
  }

  @Test
  public void cannotUnlockALockLockedByAnotherVm() {
    final String lock0 = "lock 0";
    final String lock1 = "lock 1";
    final AtomicBoolean lock0Status = new AtomicBoolean(false);
    final AtomicBoolean lock1Status = new AtomicBoolean(false);

    lock0Status.set(locators.get(0)
        .invoke(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).lock(lock0, 20, -1)));
    lock1Status.set(locators.get(1)
        .invoke(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).lock(lock1, 20, -1)));

    assertThat(lock0Status.get()).isTrue();
    assertThat(lock1Status.get()).isTrue();

    lock1Status.set(locators.get(0)
        .invoke(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).lock(lock1, 20, -1)));
    lock0Status.set(locators.get(1)
        .invoke(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).lock(lock0, 20, -1)));

    assertThat(lock0Status.get()).isFalse();
    assertThat(lock1Status.get()).isFalse();

    assertThatThrownBy(() -> locators.get(0)
        .invoke(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).unlock(lock1)))
            .hasCauseInstanceOf(LockNotHeldException.class);
    assertThatThrownBy(() -> locators.get(1)
        .invoke(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).unlock(lock0)))
            .hasCauseInstanceOf(LockNotHeldException.class);

    assertThat(catchThrowable(() -> locators.get(0)
        .invoke(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).unlock(lock0))))
            .isNull();
    assertThat(catchThrowable(() -> locators.get(1)
        .invoke(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).unlock(lock1))))
            .isNull();
  }

  @Test
  public void lockRecoveryAfterGrantorDies() throws Exception {
    final String lock1 = "lock 1";
    final String lock2 = "lock 2";
    final String lock3 = "lock 3";

    locators.get(0).invoke(GrantorFailoverDUnitTest::assertIsElderAndGetId);

    // Grantor but not the elder
    final MemberVM grantorVM = locators.get(1);
    final MemberVM survivor1 = locators.get(0);
    final MemberVM survivor2 = locators.get(2);
    grantorVM.invoke(() -> {
      DistributedLockService.becomeLockGrantor(SERVICE_NAME);
      await().untilAsserted(
          () -> assertThat(DistributedLockService.isLockGrantor(SERVICE_NAME)).isTrue());
    });

    assertThat(survivor1
        .invoke(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).lock(lock1, 20_000, -1)))
            .isTrue();
    assertThat(survivor2
        .invoke(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).lock(lock2, 20_000, -1)))
            .isTrue();

    clusterStartupRule.crashVM(1);

    locators.remove(grantorVM);

    // can't get the locks again
    assertThat(survivor2
        .invoke(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).lock(lock1, 2, -1)))
            .isFalse();
    assertThat(survivor1
        .invoke(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).lock(lock2, 2, -1)))
            .isFalse();

    final AsyncInvocation<Void> lock1FailsReleaseOnOtherVM =
        survivor2
            .invokeAsync(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).unlock(lock1));
    final AsyncInvocation<Void> lock2FailsReleaseOnOtherVM =
        survivor1
            .invokeAsync(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).unlock(lock2));


    assertThatThrownBy(lock1FailsReleaseOnOtherVM::get)
        .hasRootCauseInstanceOf(LockNotHeldException.class);
    assertThatThrownBy(lock2FailsReleaseOnOtherVM::get)
        .hasRootCauseInstanceOf(LockNotHeldException.class);

    assertThat(survivor1
        .invoke(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).lock(lock3, 20_000, -1)))
            .isTrue();

    final AsyncInvocation<Void> lock1SuccessfulRelease =
        survivor1
            .invokeAsync(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).unlock(lock1));

    final AsyncInvocation<Void> lock3SuccessfulRelease =
        survivor1
            .invokeAsync(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).unlock(lock3));

    final AsyncInvocation<Void> lock2SuccessfulRelease =
        survivor2
            .invokeAsync(() -> DistributedLockService.getServiceNamed(SERVICE_NAME).unlock(lock2));

    lock1SuccessfulRelease.get();
    lock2SuccessfulRelease.get();
    lock3SuccessfulRelease.get();
  }

  private static InternalDistributedMember assertIsElderAndGetId() {
    DistributionManager distributionManager =
        ClusterStartupRule.getCache().getInternalDistributedSystem().getDistributionManager();
    await("Wait to be elder")
        .untilAsserted(() -> assertThat(distributionManager.isElder()).isTrue());
    return distributionManager.getElderId();
  }
}
