/*
 * 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 org.junit.experimental.categories.Category;
import org.junit.Test;

import static org.junit.Assert.*;

import org.apache.geode.test.dunit.cache.internal.JUnit4CacheTestCase;
import org.apache.geode.test.dunit.internal.JUnit4DistributedTestCase;
import org.apache.geode.test.junit.categories.DistributedTest;

import java.util.Iterator;

import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheException;
import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.PartitionAttributes;
import org.apache.geode.cache.PartitionAttributesFactory;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.cache.RegionDestroyedException;
import org.apache.geode.cache30.CacheSerializableRunnable;
import org.apache.geode.test.dunit.Assert;
import org.apache.geode.test.dunit.AsyncInvocation;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.LogWriterUtils;
import org.apache.geode.test.dunit.SerializableRunnable;
import org.apache.geode.test.dunit.ThreadUtils;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.Wait;

/**
 * This test aims to test the destroyRegion functionality.
 * 
 * 
 * 
 */
@Category(DistributedTest.class)
public class PartitionedRegionDestroyDUnitTest extends PartitionedRegionDUnitTestCase {

  ////// constructor //////////
  public PartitionedRegionDestroyDUnitTest() {

    super();
  }// end of constructor

  public static final String PR_PREFIX = "PR";

  final static int MAX_REGIONS = 2;

  final int totalNumBuckets = 5;

  VM vm0, vm1, vm2, vm3;

  @Test
  public void testDestroyRegion() throws Exception, Throwable {
    Host host = Host.getHost(0);
    vm0 = host.getVM(0);
    vm1 = host.getVM(1);
    vm2 = host.getVM(2);
    vm3 = host.getVM(3);
    AsyncInvocation async1 = null;
    CacheSerializableRunnable createPRs = new CacheSerializableRunnable("createPrRegions") {

      public void run2() throws CacheException {
        Cache cache = getCache();
        for (int i = 0; i < MAX_REGIONS; i++) {
          cache.createRegion(PR_PREFIX + i, createRegionAttrsForPR(0, 200));
        }
        LogWriterUtils.getLogWriter()
            .info("Successfully created " + MAX_REGIONS + " PartitionedRegions.");
      }
    };
    // Create PRs
    vm0.invoke(createPRs);
    vm1.invoke(createPRs);
    vm2.invoke(createPRs);
    vm3.invoke(createPRs);

    vm1.invoke(new CacheSerializableRunnable("doPutOperations-1") {

      public void run2() {
        int j = 0;
        final String expectedExistsException = RegionDestroyedException.class.getName();
        getCache().getLogger().info(
            "<ExpectedException action=add>" + expectedExistsException + "</ExpectedException>");
        try {
          Cache cache = getCache();
          for (; j < MAX_REGIONS; j++) {
            PartitionedRegion pr =
                (PartitionedRegion) cache.getRegion(Region.SEPARATOR + PR_PREFIX + j);
            assertNotNull(pr);
            // Create enough entries such that all bucket are created, integer keys assumes mod
            // distribution
            int totalEntries = pr.getTotalNumberOfBuckets() * 2;
            for (int k = 0; k < totalEntries; k++) {
              pr.put(new Integer(k), PR_PREFIX + k);
            }
          }

        } catch (RegionDestroyedException e) {
          // getLogWriter().info (
          // "RegionDestroyedException occurred for Region = " + PR_PREFIX + j);
        }
        getCache().getLogger().info(
            "<ExpectedException action=remove>" + expectedExistsException + "</ExpectedException>");
      }
    });
    async1 = vm2.invokeAsync(new CacheSerializableRunnable("doPutOperations-2") {

      public void run2() throws CacheException {

        int j = 0;
        final String expectedException = RegionDestroyedException.class.getName();
        getCache().getLogger()
            .info("<ExpectedException action=add>" + expectedException + "</ExpectedException>");
        try {
          Cache cache = getCache();

          // Grab the regions right away, before they get destroyed
          // by the other thread
          PartitionedRegion prs[] = new PartitionedRegion[MAX_REGIONS];
          for (j = 0; j < MAX_REGIONS; j++) {
            prs[j] = (PartitionedRegion) cache.getRegion(Region.SEPARATOR + PR_PREFIX + j);
            if (prs[j] == null) {
              fail("Region was destroyed before putter could find it");
            }
          }

          for (j = 0; j < MAX_REGIONS; j++) {
            PartitionedRegion pr = prs[j];
            assertNotNull(pr);
            int startEntries = pr.getTotalNumberOfBuckets() * 20;
            int endEntries = startEntries + pr.getTotalNumberOfBuckets();
            for (int k = startEntries; k < endEntries; k++) {
              pr.put(new Integer(k), PR_PREFIX + k);
            }
            try {
              Thread.sleep(100);
            } catch (InterruptedException ie) {
              fail("interrupted");
            }
          }
        } catch (RegionDestroyedException e) {
          LogWriterUtils.getLogWriter()
              .info("RegionDestroyedException occurred for Region = " + PR_PREFIX + j);
        }
        getCache().getLogger()
            .info("<ExpectedException action=remove>" + expectedException + "</ExpectedException>");
      }
    });

    ThreadUtils.join(async1, 30 * 1000);
    if (async1.exceptionOccurred()) {
      Assert.fail("async1 failed", async1.getException());
    }
    final String expectedExceptions = "org.apache.geode.distributed.internal.ReplyException";
    addExceptionTag(expectedExceptions);

    Wait.pause(1000); // give async a chance to grab the regions...

    vm0.invoke(new CacheSerializableRunnable("destroyPRRegions") {

      public void run2() throws CacheException {
        Cache cache = getCache();
        for (int i = 0; i < MAX_REGIONS; i++) {
          Region pr = cache.getRegion(Region.SEPARATOR + PR_PREFIX + i);
          assertNotNull(pr);
          pr.destroyRegion();
          assertTrue(pr.isDestroyed());
          Region prDes = cache.getRegion(Region.SEPARATOR + PR_PREFIX + i);
          assertNull(prDes);
        }
      }
    });

    addExceptionTag(expectedExceptions);
    CacheSerializableRunnable validateMetaDataAfterDestroy =
        new CacheSerializableRunnable("validateMetaDataAfterDestroy") {

          public void run2() throws CacheException {

            InternalCache cache = getCache();
            Region rootRegion = PartitionedRegionHelper.getPRRoot(cache);
            // Region allPRs = PartitionedRegionHelper.getPRConfigRegion(rootRegion,
            // getCache());

            int trial = 0;
            // verify that all the regions have received the destroy call.
            while (trial < 10) {
              if (cache.rootRegions().size() > 1) {
                try {
                  Thread.sleep(500);
                } catch (InterruptedException e) {
                  fail("interrupted");
                }
                trial++;
              } else {
                break;
              }
            }

            if (cache.rootRegions().size() > 1) {
              fail("All Regions Not destroyed. # OF Regions Not Destroyed = "
                  + (cache.rootRegions().size() - 1));
            }

            // Assert that all PartitionedRegions are gone
            assertEquals(0, rootRegion.size());
            LogWriterUtils.getLogWriter()
                .info("allPartitionedRegions size() =" + rootRegion.size());
            assertEquals(
                "ThePrIdToPR Map size is:" + PartitionedRegion.prIdToPR.size() + " instead of 0",
                MAX_REGIONS, PartitionedRegion.prIdToPR.size());
            LogWriterUtils.getLogWriter()
                .info("PartitionedRegion.prIdToPR.size() =" + PartitionedRegion.prIdToPR.size());
            LogWriterUtils.getLogWriter()
                .info("# of Subregions of root Region after destroy call = "
                    + rootRegion.subregions(false).size());
            Iterator itr = (rootRegion.subregions(false)).iterator();
            while (itr.hasNext()) {
              Region rg = (Region) itr.next();
              LogWriterUtils.getLogWriter().info("Root Region SubRegionName = " + rg.getName());
              // assertIndexDetailsEquals("REGION NAME FOUND:"+rg.getName(),-1,
              // rg.getName().indexOf(
              // PartitionedRegionHelper.BUCKET_2_NODE_TABLE_PREFIX));
              assertEquals("regionFound that should be gone!:" + rg.getName(), -1,
                  rg.getName().indexOf(PartitionedRegionHelper.BUCKET_REGION_PREFIX));
            }
          }
        };
    vm0.invoke(validateMetaDataAfterDestroy);
    vm1.invoke(validateMetaDataAfterDestroy);
    vm2.invoke(validateMetaDataAfterDestroy);
    vm3.invoke(validateMetaDataAfterDestroy);
    vm0.invoke(createPRs);
    vm1.invoke(createPRs);
    vm2.invoke(createPRs);
    vm3.invoke(createPRs);

  }

  protected RegionAttributes createRegionAttrsForPR(int red, int localMaxMem) {

    AttributesFactory attr = new AttributesFactory();
    attr.setDataPolicy(DataPolicy.PARTITION);
    PartitionAttributesFactory paf = new PartitionAttributesFactory();
    PartitionAttributes prAttr = paf.setRedundantCopies(red).setLocalMaxMemory(localMaxMem)
        .setTotalNumBuckets(totalNumBuckets).create();
    attr.setPartitionAttributes(prAttr);
    return attr.create();
  }

  private void addExceptionTag(final String expectedException) {

    SerializableRunnable addExceptionTag = new CacheSerializableRunnable("addExceptionTag") {
      public void run2() {
        getCache().getLogger()
            .info("<ExpectedException action=add>" + expectedException + "</ExpectedException>");
      }
    };

    vm0.invoke(addExceptionTag);
    vm1.invoke(addExceptionTag);
    vm2.invoke(addExceptionTag);
    vm3.invoke(addExceptionTag);
  }

  private void removeExceptionTag(final String expectedException) {

    SerializableRunnable removeExceptionTag = new CacheSerializableRunnable("removeExceptionTag") {
      public void run2() throws CacheException {
        getCache().getLogger()
            .info("<ExpectedException action=remove>" + expectedException + "</ExpectedException>");
      }
    };
    vm0.invoke(removeExceptionTag);
    vm1.invoke(removeExceptionTag);
    vm2.invoke(removeExceptionTag);
    vm3.invoke(removeExceptionTag);
  }
}
