/*
 * 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.internal.cache.tier.sockets;

import static org.apache.geode.test.awaitility.GeodeAwaitility.await;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.RestoreSystemProperties;
import org.junit.experimental.categories.Category;

import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.test.junit.categories.ClientServerTest;

/**
 * This is a functional-test for <code>ClientHealthMonitor</code>.
 */
@Category({ClientServerTest.class})
public class ClientHealthMonitorTest {
  @Rule
  public RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();
  private final int pingIntervalMillis = 200;
  private final int monitorIntervalMillis = 20;
  private ClientHealthMonitor clientHealthMonitor;
  private CacheClientNotifierStats mockStats;
  private InternalCache mockCache;

  @Before
  public void setUp() {
    System.setProperty(ClientHealthMonitor.CLIENT_HEALTH_MONITOR_INTERVAL_PROPERTY,
        Integer.toString(monitorIntervalMillis));

    mockCache = mock(InternalCache.class);
    mockStats = mock(CacheClientNotifierStats.class);
    clientHealthMonitor = ClientHealthMonitor.getInstance(mockCache, pingIntervalMillis, mockStats);
  }

  @After
  public void setup() {
    ClientHealthMonitor.shutdownInstance();
  }

  @Test
  public void idleServerConnectionTerminatedByHealthMonitor() throws Exception {
    ClientProxyMembershipID mockId = mock(ClientProxyMembershipID.class);
    ServerConnection mockConnection = mock(ServerConnection.class);

    clientHealthMonitor.registerClient(mockId);
    clientHealthMonitor.addConnection(mockId, mockConnection);
    clientHealthMonitor.receivedPing(mockId);
    clientHealthMonitor.testUseCustomHeartbeatCheck((a, b, c) -> true); // Fail all heartbeats

    await()
        .untilAsserted(() -> verify(mockConnection).handleTermination(true));
  }

  class HeartbeatOverride implements ClientHealthMonitor.HeartbeatTimeoutCheck {
    public int numHeartbeats = 0;

    @Override
    public boolean timedOut(long current, long lastHeartbeat, long interval) {
      ++numHeartbeats;
      return false;
    }
  }

  @Test
  public void activeServerConnectionNotTerminatedByHealthMonitor() throws Exception {
    ClientProxyMembershipID mockId = mock(ClientProxyMembershipID.class);
    ServerConnection mockConnection = mock(ServerConnection.class);

    clientHealthMonitor.registerClient(mockId);
    clientHealthMonitor.addConnection(mockId, mockConnection);
    clientHealthMonitor.receivedPing(mockId);

    HeartbeatOverride heartbeater = new HeartbeatOverride();
    clientHealthMonitor.testUseCustomHeartbeatCheck(heartbeater);

    await().until(() -> heartbeater.numHeartbeats >= 5);

    // Check that we never tried to terminate the connection
    verify(mockConnection, times(0)).handleTermination(true);
  }

  @Test
  public void registerClientNewClientAddedWithCurrentTimeAndIncrementsStat() {
    ClientProxyMembershipID mockId = mock(ClientProxyMembershipID.class);
    clientHealthMonitor.registerClient(mockId);
    assertThat(clientHealthMonitor.getClientHeartbeats().get(mockId)).isNotNull()
        .isLessThanOrEqualTo(System.currentTimeMillis());
    verify(mockStats).incClientRegisterRequests();
  }

  @Test
  public void registerClientExistingClientDoesNotUpdateTimeOrIncrementStat()
      throws InterruptedException {
    ClientProxyMembershipID mockId = mock(ClientProxyMembershipID.class);
    clientHealthMonitor.registerClient(mockId);
    Long expectedTime = clientHealthMonitor.getClientHeartbeats().get(mockId);

    Thread.sleep(2);
    clientHealthMonitor.registerClient(mockId);
    assertThat(clientHealthMonitor.getClientHeartbeats().get(mockId)).isNotNull()
        .isEqualTo(expectedTime);

    // incremented 1 time for the initial registerClient.
    verify(mockStats).incClientRegisterRequests();
  }

  @Test
  public void unregisterClientExistingClientIsRemovedAndIncrementsStat() {
    ClientProxyMembershipID mockId = mock(ClientProxyMembershipID.class);
    clientHealthMonitor.registerClient(mockId);

    clientHealthMonitor.unregisterClient(mockId, null, true, null);
    assertThat(clientHealthMonitor.getClientHeartbeats()).doesNotContainKey(mockId);
    verify(mockStats).incClientUnRegisterRequests();
  }

  @Test
  public void unregisterClientMissingClientIsDoesNotIncrementStat() {
    ClientProxyMembershipID mockId = mock(ClientProxyMembershipID.class);
    clientHealthMonitor.registerClient(mockId);

    clientHealthMonitor.unregisterClient(mockId, null, true, null);
    clientHealthMonitor.unregisterClient(mockId, null, true, null);
    assertThat(clientHealthMonitor.getClientHeartbeats()).doesNotContainKey(mockId);

    // incremented 1 time for the initial unregisterClient.
    verify(mockStats).incClientUnRegisterRequests();
  }

  @Test
  public void receivedPingNewClientRegistersWithCurrentTimeAndIncrementsStat() {
    ClientProxyMembershipID mockId = mock(ClientProxyMembershipID.class);
    clientHealthMonitor.registerClient(mockId);
    clientHealthMonitor.receivedPing(mockId);
    assertThat(clientHealthMonitor.getClientHeartbeats().get(mockId)).isNotNull()
        .isLessThanOrEqualTo(System.currentTimeMillis());
    verify(mockStats).incClientRegisterRequests();
  }

  @Test
  public void receivedPingExistingClientUpdatesTimeOnly() throws InterruptedException {
    ClientProxyMembershipID mockId = mock(ClientProxyMembershipID.class);
    clientHealthMonitor.registerClient(mockId);
    clientHealthMonitor.receivedPing(mockId);
    Long expectedTime = clientHealthMonitor.getClientHeartbeats().get(mockId);

    Thread.sleep(2);
    clientHealthMonitor.receivedPing(mockId);
    assertThat(clientHealthMonitor.getClientHeartbeats().get(mockId)).isNotNull()
        .isGreaterThan(expectedTime);

    // incremented 1 time for the initial receivedPing.
    verify(mockStats).incClientRegisterRequests();
  }

}
