/*
 * 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.cache.Region.SEPARATOR;
import static org.apache.geode.internal.AvailablePortHelper.getRandomAvailableTCPPort;
import static org.apache.geode.test.awaitility.GeodeAwaitility.await;
import static org.assertj.core.api.Assertions.assertThat;

import org.junit.Test;

import org.apache.geode.cache.Operation;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.client.internal.PingOp;
import org.apache.geode.cache.client.internal.PoolImpl;
import org.apache.geode.distributed.internal.ServerLocation;
import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
import org.apache.geode.internal.cache.EntryEventImpl;
import org.apache.geode.internal.cache.EventID;
import org.apache.geode.internal.cache.LocalRegion;
import org.apache.geode.internal.cache.RegionEntry;
import org.apache.geode.internal.cache.versions.VersionTag;
import org.apache.geode.test.dunit.NetworkUtils;
import org.apache.geode.test.dunit.VM;


public class ClientServerMiscDUnitTest extends ClientServerMiscDUnitTestBase {


  /**
   * Ensure that an Invalidate event that
   */
  @Test
  public void testInvalidateOnInvalidEntryInServerReachesClient() throws Exception {
    VM server = VM.getVM(0);
    String regionPath = SEPARATOR + REGION_NAME2;
    int serverPort = getRandomAvailableTCPPort();
    PORT1 = server.invoke(() -> {
      int port = createServerCache(true, -1, false, serverPort);
      getCache().getRegion(regionPath).put(server_k1, "VALUE1");
      getCache().getRegion(regionPath).invalidate(server_k1);
      return port;
    });
    createClientCache(NetworkUtils.getServerHostName(), PORT1);
    registerInterest();
    Region region = static_cache.getRegion(regionPath);
    assertThat(region.containsKey(server_k1)).isTrue();
    assertThat(region.get(server_k1)).isNull();

    System.out.println("do it with a forceEntry==false code path");
    RegionEntry entry = ((LocalRegion) region).getRegionEntry(server_k1);
    int entryVersion = entry.getVersionStamp().getEntryVersion();
    server.invoke(() -> {
      // getCache().getRegion(regionPath).invalidate(server_k1);

      // create a "remote" invalidateion event and invalidate the already-invalid entry
      LocalRegion localRegion = (LocalRegion) getCache().getRegion(regionPath);
      VersionTag tag = localRegion.getRegionEntry(server_k1).getVersionStamp().asVersionTag();
      InternalDistributedMember id = localRegion.getMyId();
      tag.setMemberID(
          new InternalDistributedMember(id.getInetAddress(), id.getMembershipPort() + 1));
      tag.setEntryVersion(tag.getEntryVersion() + 1);
      tag.setEntryVersion(5);
      tag.setIsRemoteForTesting();
      EntryEventImpl event =
          EntryEventImpl.create(localRegion, Operation.INVALIDATE, server_k1, null,
              null, false, id);
      EventID eventID = new EventID(new byte[100], 1, 1);
      event.setVersionTag(tag);
      event.setEventId(eventID);
      localRegion.getRegionMap().invalidate(event, false, false, false);
    });
    await()
        .until(() -> entry.getVersionStamp().getEntryVersion() > entryVersion);

    System.out.println("do it again with a forceEntry==true code path");
    RegionEntry entry2 = ((LocalRegion) region).getRegionEntry(server_k1);
    int entryVersion2 = entry.getVersionStamp().getEntryVersion();
    server.invoke(() -> {
      // create a "remote" invalidation event and invalidate the already-invalid entry
      LocalRegion localRegion = (LocalRegion) getCache().getRegion(regionPath);
      VersionTag tag = localRegion.getRegionEntry(server_k1).getVersionStamp().asVersionTag();
      InternalDistributedMember id = localRegion.getMyId();
      tag.setMemberID(
          new InternalDistributedMember(id.getInetAddress(), id.getMembershipPort() + 1));
      tag.setEntryVersion(tag.getEntryVersion() + 1);
      tag.setEntryVersion(6);
      tag.setIsRemoteForTesting();
      EntryEventImpl event =
          EntryEventImpl.create(localRegion, Operation.INVALIDATE, server_k1, null,
              null, false, id);
      EventID eventID = new EventID(new byte[100], 1, 2);
      event.setVersionTag(tag);
      event.setEventId(eventID);
      localRegion.getRegionMap().invalidate(event, false, true, false);
    });
    await()
        .until(() -> entry2.getVersionStamp().getEntryVersion() > entryVersion2);
  }

  @Test
  public void testPingWrongServer() {
    PORT1 = initServerCache(true);
    initServerCache2();
    InternalDistributedMember server2ID = server2.invoke("get ID", () -> cache.getMyId());
    pool = (PoolImpl) createClientCache(NetworkUtils.getServerHostName(), PORT1);
    // send the ping to server1 but use server2's identifier so the ping will be forwarded
    ClientProxyMembershipID proxyID = server1.invoke(
        () -> CacheClientNotifier.getInstance().getClientProxies().iterator().next().getProxyID());
    server2.invoke(() -> {
      assertThat(ClientHealthMonitor.getInstance().getClientHeartbeats().containsKey(proxyID))
          .isFalse();
      assertThat(ClientHealthMonitor.getInstance().getClientHeartbeats().keySet().size())
          .isEqualTo(0);
    });
    PingOp.execute(pool, new ServerLocation(NetworkUtils.getServerHostName(), PORT1), server2ID);
    // if the ping made it to server2 it will have the client's ID in its health monitor
    server2.invoke(() -> {
      await("For heartbeat to be received").untilAsserted(() -> assertThat(
          ClientHealthMonitor.getInstance().getClientHeartbeats().keySet().size())
              .isEqualTo(1));
      ClientProxyMembershipID proxyIDFound =
          ClientHealthMonitor.getInstance().getClientHeartbeats().keySet().iterator().next();
      assertThat(
          ClientHealthMonitor.getInstance().getClientHeartbeats().containsKey(proxyID))
              .isTrue();
    });
  }
}
