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

import static org.apache.geode.distributed.ConfigurationProperties.LOG_LEVEL;
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 java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

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

import org.apache.geode.LogWriter;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.EntryEvent;
import org.apache.geode.cache.InterestResultPolicy;
import org.apache.geode.cache.Operation;
import org.apache.geode.cache.PartitionAttributesFactory;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionFactory;
import org.apache.geode.cache.RegionShortcut;
import org.apache.geode.cache.client.ClientCache;
import org.apache.geode.cache.client.ClientCacheFactory;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.apache.geode.cache.server.CacheServer;
import org.apache.geode.cache.util.CacheListenerAdapter;
import org.apache.geode.internal.AvailablePortHelper;
import org.apache.geode.internal.cache.tier.InterestType;
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.SerializableCallable;
import org.apache.geode.test.dunit.SerializableCallableIF;
import org.apache.geode.test.dunit.SerializableRunnable;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.cache.internal.JUnit4CacheTestCase;
import org.apache.geode.test.junit.categories.ClientServerTest;

/**
 * This tests the fix for bug #43407 under a variety of configurations and also tests that
 * tombstones are treated in a similar manner. The ticket complains that a client that does a get(K)
 * does not end up with the entry in its cache if K is invalid on the server.
 */
@Category({ClientServerTest.class})
public class ClientServerInvalidAndDestroyedEntryDUnitTest extends JUnit4CacheTestCase {

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

  @Test
  public void testClientGetsInvalidEntry() throws Exception {
    final String regionName = getUniqueName() + "Region";
    doTestClientGetsInvalidEntry(regionName, false, false);
  }

  @Test
  public void testClientGetsInvalidEntryPR() throws Exception {
    final String regionName = getUniqueName() + "Region";
    doTestClientGetsInvalidEntry(regionName, true, false);
  }

  @Test
  public void testClientGetsTombstone() throws Exception {
    final String regionName = getUniqueName() + "Region";
    doTestClientGetsTombstone(regionName, false, false);
  }

  @Test
  public void testClientGetsTombstonePR() throws Exception {
    final String regionName = getUniqueName() + "Region";
    doTestClientGetsTombstone(regionName, true, false);
  }



  // same tests but with transactions...



  @Test
  public void testClientGetsInvalidEntryTX() throws Exception {
    final String regionName = getUniqueName() + "Region";
    doTestClientGetsInvalidEntry(regionName, false, true);
  }

  @Test
  public void testClientGetsInvalidEntryPRTX() throws Exception {
    final String regionName = getUniqueName() + "Region";
    doTestClientGetsInvalidEntry(regionName, true, true);
  }

  @Test
  public void testClientGetsTombstoneTX() throws Exception {
    final String regionName = getUniqueName() + "Region";
    doTestClientGetsTombstone(regionName, false, true);
  }

  @Test
  public void testClientGetsTombstonePRTX() throws Exception {
    final String regionName = getUniqueName() + "Region";
    doTestClientGetsTombstone(regionName, true, true);
  }


  // tests for bug #46780, tombstones left in client after RI

  @Test
  public void testRegisterInterestRemovesOldEntry() throws Exception {
    final String regionName = getUniqueName() + "Region";
    doTestRegisterInterestRemovesOldEntry(regionName, false);
  }

  @Test
  public void testRegisterInterestRemovesOldEntryPR() throws Exception {
    final String regionName = getUniqueName() + "Region";
    doTestRegisterInterestRemovesOldEntry(regionName, true);
  }

  /* this method creates a server cache and is used by all of the tests in this class */
  private SerializableCallableIF getCreateServerCallable(final String regionName,
      final boolean usePR) {
    return new SerializableCallable("create server and entries") {
      @Override
      public Object call() {
        Cache cache = getCache();
        List<CacheServer> servers = cache.getCacheServers();
        CacheServer server;
        if (servers.size() > 0) {
          server = servers.get(0);
        } else {
          server = cache.addCacheServer();
          int port = AvailablePortHelper.getRandomAvailableTCPPort();
          server.setPort(port);
          server.setHostnameForClients("localhost");
          try {
            server.start();
          } catch (IOException e) {
            Assert.fail("Failed to start server ", e);
          }
        }
        if (usePR) {
          RegionFactory factory = cache.createRegionFactory(RegionShortcut.PARTITION);
          PartitionAttributesFactory pf = new PartitionAttributesFactory();
          pf.setTotalNumBuckets(2);
          factory.setPartitionAttributes(pf.create());
          factory.create(regionName);
        } else {
          cache.createRegionFactory(RegionShortcut.REPLICATE).create(regionName);
        }
        return server.getPort();
      }
    };
  }


  /**
   * Bug #43407 - when a client does a get(k) and the entry is invalid in the server we want the
   * client to end up with an entry that is invalid.
   */
  private void doTestClientGetsInvalidEntry(final String regionName, final boolean usePR,
      boolean useTX) throws Exception {
    VM vm1 = Host.getHost(0).getVM(1);
    VM vm2 = Host.getHost(0).getVM(2);

    // here are the keys that will be used to validate behavior. Keys must be
    // colocated if using both a partitioned region in the server and transactions
    // in the client. All of these keys hash to bucket 0 in a two-bucket PR
    // except Object11 and IDoNotExist1
    final String notAffectedKey = "Object1";
    final String nonexistentKey = (usePR && useTX) ? "IDoNotExist2" : "IDoNotExist1";
    final String key1 = "Object10";
    final String key2 = (usePR && useTX) ? "Object12" : "Object11";

    SerializableCallableIF createServer = getCreateServerCallable(regionName, usePR);
    int serverPort = (Integer) vm1.invoke(createServer);
    vm2.invoke(createServer);
    vm1.invoke(new SerializableRunnable("populate server and create invalid entry") {
      @Override
      public void run() {
        Region myRegion = getCache().getRegion(regionName);
        for (int i = 1; i <= 20; i++) {
          myRegion.put("Object" + i, "Value" + i);
        }
        myRegion.invalidate(key1);
        myRegion.invalidate(key2);
      }
    });
    org.apache.geode.test.dunit.LogWriterUtils.getLogWriter().info("creating client cache");
    ClientCache c = new ClientCacheFactory().addPoolServer("localhost", serverPort)
        .set(LOG_LEVEL, LogWriterUtils.getDUnitLogLevel()).create();
    Region myRegion =
        c.createClientRegionFactory(ClientRegionShortcut.CACHING_PROXY).create(regionName);
    if (useTX) {
      c.getCacheTransactionManager().begin();
    }

    // get of a valid entry should work
    assertNotNull(myRegion.get(notAffectedKey));

    // get of an invalid entry should return null and create the entry in an invalid state
    org.apache.geode.test.dunit.LogWriterUtils.getLogWriter()
        .info("getting " + key1 + " - should reach this cache and be INVALID");
    assertNull(myRegion.get(key1));
    assertTrue(myRegion.containsKey(key1));

    // since this might be a PR we also check the next key to force PR Get messaging
    assertNull(myRegion.get(key2));
    assertTrue(myRegion.containsKey(key2));

    // now try a key that doesn't exist anywhere
    assertNull(myRegion.get(nonexistentKey));
    assertFalse(myRegion.containsKey(nonexistentKey));

    if (useTX) {
      c.getCacheTransactionManager().commit();

      // test that the commit correctly created the entries in the region
      assertNotNull(myRegion.get(notAffectedKey));
      assertNull(myRegion.get(key1));
      assertTrue(myRegion.containsKey(key1));
      assertNull(myRegion.get(key2));
      assertTrue(myRegion.containsKey(key2));
    }

    myRegion.localDestroy(notAffectedKey);
    myRegion.localDestroy(key1);
    myRegion.localDestroy(key2);

    if (useTX) {
      c.getCacheTransactionManager().begin();
    }

    // check that getAll returns invalidated entries
    List keys = new LinkedList();
    keys.add(notAffectedKey);
    keys.add(key1);
    keys.add(key2);
    Map result = myRegion.getAll(keys);

    assertNotNull(result.get(notAffectedKey));
    assertNull(result.get(key1));
    assertNull(result.get(key2));
    assertTrue(result.containsKey(key1));
    assertTrue(result.containsKey(key2));
    assertTrue(myRegion.containsKey(key1));
    assertTrue(myRegion.containsKey(key2));

    if (useTX) {
      c.getCacheTransactionManager().commit();
      // test that the commit correctly created the entries in the region
      assertNotNull(myRegion.get(notAffectedKey));
      assertNull(myRegion.get(key1));
      assertTrue(myRegion.containsKey(key1));
      assertNull(myRegion.get(key2));
      assertTrue(myRegion.containsKey(key2));
    }

    // test that a listener is not invoked when there is already an invalidated
    // entry in the client cache
    UpdateListener listener = new UpdateListener();
    listener.log = org.apache.geode.test.dunit.LogWriterUtils.getLogWriter();
    myRegion.getAttributesMutator().addCacheListener(listener);
    myRegion.get(key1);
    assertEquals("expected no cache listener invocations", 0, listener.updateCount,
        listener.updateCount);

    myRegion.localDestroy(notAffectedKey);
    myRegion.getAll(keys);
    assertTrue("expected to find " + notAffectedKey, myRegion.containsKey(notAffectedKey));
    assertEquals("expected only one listener invocation for " + notAffectedKey, 1,
        listener.updateCount);
  }



  /**
   * Similar to bug #43407 but not reported in a ticket, we want a client that does a get() on a
   * destroyed entry to end up with a tombstone for that entry. This was already the case but there
   * were no unit tests covering this for different server configurations and with/without
   * transactions.
   */
  private void doTestClientGetsTombstone(final String regionName, final boolean usePR,
      boolean useTX) throws Exception {
    VM vm1 = Host.getHost(0).getVM(1);
    VM vm2 = Host.getHost(0).getVM(2);

    // here are the keys that will be used to validate behavior. Keys must be
    // colocated if using both a partitioned region in the server and transactions
    // in the client. All of these keys hash to bucket 0 in a two-bucket PR
    // except Object11 and IDoNotExist1
    final String notAffectedKey = "Object1";
    final String nonexistentKey = (usePR && useTX) ? "IDoNotExist2" : "IDoNotExist1";
    final String key1 = "Object10";
    final String key2 = (usePR && useTX) ? "Object12" : "Object11";

    SerializableCallableIF createServer = getCreateServerCallable(regionName, usePR);
    int serverPort = (Integer) vm1.invoke(createServer);
    vm2.invoke(createServer);
    vm1.invoke(new SerializableRunnable("populate server and create invalid entry") {
      @Override
      public void run() {
        Region myRegion = getCache().getRegion(regionName);
        for (int i = 1; i <= 20; i++) {
          myRegion.put("Object" + i, "Value" + i);
        }
        myRegion.destroy(key1);
        myRegion.destroy(key2);
      }
    });
    org.apache.geode.test.dunit.LogWriterUtils.getLogWriter().info("creating client cache");
    ClientCache c = new ClientCacheFactory().addPoolServer("localhost", serverPort)
        .set(LOG_LEVEL, LogWriterUtils.getDUnitLogLevel()).create();
    Region myRegion =
        c.createClientRegionFactory(ClientRegionShortcut.CACHING_PROXY).create(regionName);
    if (useTX) {
      c.getCacheTransactionManager().begin();
    }
    // get of a valid entry should work
    assertNotNull(myRegion.get(notAffectedKey));
    // get of an invalid entry should return null and create the entry in an invalid state
    org.apache.geode.test.dunit.LogWriterUtils.getLogWriter()
        .info("getting " + key1 + " - should reach this cache and be a TOMBSTONE");
    assertNull(myRegion.get(key1));
    assertFalse(myRegion.containsKey(key1));
    RegionEntry entry;
    if (!useTX) {
      entry = ((LocalRegion) myRegion).getRegionEntry(key1);
      assertNotNull(entry); // it should be there
      assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE
    }

    // since this might be a PR we also check the next key to force PR Get messaging
    assertNull(myRegion.get(key2));
    assertFalse(myRegion.containsKey(key2));
    if (!useTX) {
      entry = ((LocalRegion) myRegion).getRegionEntry(key2);
      assertNotNull(entry); // it should be there
      assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE
    }


    // now try a key that doesn't exist anywhere
    assertNull(myRegion.get(nonexistentKey));
    assertFalse(myRegion.containsKey(nonexistentKey));

    if (useTX) {
      c.getCacheTransactionManager().commit();

      // test that the commit correctly created the entries in the region
      assertNotNull(myRegion.get(notAffectedKey));
      assertNull(myRegion.get(key1));
      assertFalse(myRegion.containsKey(key1));
      entry = ((LocalRegion) myRegion).getRegionEntry(key1);
      assertNotNull(entry); // it should be there
      assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE

      assertNull(myRegion.get(key2));
      assertFalse(myRegion.containsKey(key2));
      entry = ((LocalRegion) myRegion).getRegionEntry(key2);
      assertNotNull(entry); // it should be there
      assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE
    }

    myRegion.localDestroy(notAffectedKey);

    if (useTX) {
      c.getCacheTransactionManager().begin();
    }

    // check that getAll returns invalidated entries
    List keys = new LinkedList();
    keys.add(notAffectedKey);
    keys.add(key1);
    keys.add(key2);
    Map result = myRegion.getAll(keys);

    org.apache.geode.test.dunit.LogWriterUtils.getLogWriter().info("result of getAll = " + result);
    assertNotNull(result.get(notAffectedKey));
    assertNull(result.get(key1));
    assertNull(result.get(key2));
    assertFalse(myRegion.containsKey(key1));
    assertFalse(myRegion.containsKey(key2));
    if (!useTX) {
      entry = ((LocalRegion) myRegion).getRegionEntry(key1);
      assertNotNull(entry); // it should be there
      assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE

      entry = ((LocalRegion) myRegion).getRegionEntry(key2);
      assertNotNull(entry); // it should be there
      assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE
    } else { // useTX
      c.getCacheTransactionManager().commit();
      // test that the commit correctly created the entries in the region
      assertNotNull(myRegion.get(notAffectedKey));

      assertNull(myRegion.get(key1));
      assertFalse(myRegion.containsKey(key1));
      entry = ((LocalRegion) myRegion).getRegionEntry(key1);
      assertNotNull(entry); // it should be there
      assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE

      assertNull(myRegion.get(key2));
      assertFalse(myRegion.containsKey(key2));
      entry = ((LocalRegion) myRegion).getRegionEntry(key2);
      assertNotNull(entry); // it should be there
      assertTrue(entry.isTombstone()); // it should be a destroyed entry with Token.TOMBSTONE
    }

  }


  private void doTestRegisterInterestRemovesOldEntry(final String regionName, final boolean usePR)
      throws Exception {
    VM vm1 = Host.getHost(0).getVM(1);
    VM vm2 = Host.getHost(0).getVM(2);

    // here are the keys that will be used to validate behavior. Keys must be
    // colocated if using both a partitioned region in the server and transactions
    // in the client. All of these keys hash to bucket 0 in a two-bucket PR
    // except Object11 and IDoNotExist1
    final String key10 = "Object10";
    final String interestPattern = "Object.*";

    SerializableCallableIF createServer = getCreateServerCallable(regionName, usePR);
    int serverPort = (Integer) vm1.invoke(createServer);
    vm2.invoke(createServer);
    vm1.invoke(new SerializableRunnable("populate server") {
      @Override
      public void run() {
        Region myRegion = getCache().getRegion(regionName);
        for (int i = 1; i <= 20; i++) {
          myRegion.put("Object" + i, "Value" + i);
        }
      }
    });
    org.apache.geode.test.dunit.LogWriterUtils.getLogWriter().info("creating client cache");
    ClientCache c = new ClientCacheFactory().addPoolServer("localhost", serverPort)
        .set(LOG_LEVEL, LogWriterUtils.getDUnitLogLevel()).setPoolSubscriptionEnabled(true)
        .create();

    Region myRegion =
        c.createClientRegionFactory(ClientRegionShortcut.CACHING_PROXY).create(regionName);

    myRegion.registerInterestRegex(interestPattern);

    // make sure key1 is in the client because we're going to mess with it
    assertNotNull(myRegion.get(key10));

    // remove the entry for key1 on the servers and then simulate interest recovery
    // to show that the entry for key1 is no longer there in the client when recovery
    // finishes
    SerializableRunnable destroyKey10 =
        new SerializableRunnable("locally destroy " + key10 + " in the servers") {
          @Override
          public void run() {
            Region myRegion = getCache().getRegion(regionName);
            EntryEventImpl event = ((LocalRegion) myRegion).generateEvictDestroyEvent(key10);
            event.setOperation(Operation.LOCAL_DESTROY);
            if (usePR) {
              BucketRegion bucket = ((PartitionedRegion) myRegion).getBucketRegion(key10);
              if (bucket != null) {
                event.setRegion(bucket);
                org.apache.geode.test.dunit.LogWriterUtils.getLogWriter()
                    .info("performing local destroy in " + bucket + " ccEnabled="
                        + bucket.getConcurrencyChecksEnabled() + " rvv="
                        + bucket.getVersionVector());
                bucket.setConcurrencyChecksEnabled(false); // turn off cc so entry is removed
                bucket.mapDestroy(event, false, false, null);
                bucket.setConcurrencyChecksEnabled(true);
              }
            } else {
              ((LocalRegion) myRegion).setConcurrencyChecksEnabled(false); // turn off cc so entry
                                                                           // is
              // removed
              ((LocalRegion) myRegion).mapDestroy(event, false, false, null);
              ((LocalRegion) myRegion).setConcurrencyChecksEnabled(true);
            }
          }
        };

    vm1.invoke(destroyKey10);
    vm2.invoke(destroyKey10);

    myRegion.getCache().getLogger().info("clearing keys of interest");
    ((LocalRegion) myRegion).clearKeysOfInterest(interestPattern, InterestType.REGULAR_EXPRESSION,
        InterestResultPolicy.KEYS_VALUES);
    myRegion.getCache().getLogger().info("done clearing keys of interest");

    assertTrue("expected region to be empty but it has " + myRegion.size() + " entries",
        myRegion.size() == 0);

    RegionEntry entry;
    entry = ((LocalRegion) myRegion).getRegionEntry(key10);
    assertNull(entry); // it should have been removed

    // now register interest. At the end, finishRegisterInterest should clear
    // out the entry for key1 because it was stored in image-state as a
    // destroyed RI entry in clearKeysOfInterest
    myRegion.registerInterestRegex(interestPattern);

    entry = ((LocalRegion) myRegion).getRegionEntry(key10);
    assertNull(entry); // it should not be there
  }

  static class UpdateListener extends CacheListenerAdapter {
    int updateCount;
    LogWriter log;

    @Override
    public void afterUpdate(EntryEvent event) {
      // log.info("UpdateListener.afterUpdate invoked for " + event, new Exception("stack trace"));
      updateCount++;
    }

    @Override
    public void afterCreate(EntryEvent event) {
      // log.info("UpdateListener.afterCreate invoked for " + event, new Exception("stack trace"));
      updateCount++;
    }
  }

}
