/*
 * 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.test.dunit.tests;

import static org.apache.geode.test.dunit.Assert.*;

import java.io.Serializable;
import java.util.concurrent.atomic.AtomicInteger;

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

import org.apache.geode.test.dunit.AsyncInvocation;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.RMIException;
import org.apache.geode.test.dunit.SerializableRunnableIF;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.internal.JUnit4DistributedTestCase;
import org.apache.geode.test.junit.categories.DistributedTest;

/**
 * This class tests the functionality of the {@link VM} class.
 */
@Category(DistributedTest.class)
@SuppressWarnings("serial")
public class JUnit4VMDUnitTest extends JUnit4DistributedTestCase {

  private static final AtomicInteger COUNTER = new AtomicInteger();
  private static final boolean BOOLEAN_VALUE = true;
  private static final byte BYTE_VALUE = (byte) 40;
  private static final long LONG_VALUE = 42L;
  private static final String STRING_VALUE = "BLAH BLAH BLAH";

  @Test
  public void testInvokeStaticBoolean() {
    Host host = Host.getHost(0);
    VM vm = host.getVM(0);
    assertEquals(BOOLEAN_VALUE, (boolean) vm.invoke(() -> remoteBooleanMethod()));
  }

  @Test
  public void testInvokeStaticByte() {
    Host host = Host.getHost(0);
    VM vm = host.getVM(0);
    assertEquals(BYTE_VALUE, (byte) vm.invoke(() -> remoteByteMethod()));
  }

  @Test
  public void testInvokeStaticLong() {
    Host host = Host.getHost(0);
    VM vm = host.getVM(0);
    assertEquals(LONG_VALUE, (long) vm.invoke(() -> remoteLongMethod()));
  }

  @Test
  public void testInvokeInstance() {
    Host host = Host.getHost(0);
    VM vm = host.getVM(0);
    assertEquals(STRING_VALUE, vm.invoke(new ClassWithString(), "getString"));
  }

  @Test
  public void testInvokeRunnableWithException() {
    Host host = Host.getHost(0);
    VM vm = host.getVM(0);
    try {
      vm.invoke(new InvokeRunnable());
      fail("Should have thrown a BasicTestException");
    } catch (RMIException ex) {
      assertTrue(ex.getCause() instanceof BasicTestException);
    }
  }

  @Test
  public void testReturnValue() throws Exception {
    final Host host = Host.getHost(0);
    final VM vm = host.getVM(0);
    // Assert class static invocation works
    AsyncInvocation a1 = vm.invokeAsync(() -> getAndIncStaticCount());
    a1.join();
    assertEquals(new Integer(0), a1.getReturnValue());
    // Assert class static invocation with args works
    a1 = vm.invokeAsync(() -> incrementStaticCount(new Integer(2)));
    a1.join();
    assertEquals(new Integer(3), a1.getReturnValue());
    // Assert that previous values are not returned when invoking method w/ no return val
    a1 = vm.invokeAsync(() -> incStaticCount());
    a1.join();
    assertNull(a1.getReturnValue());
    // Assert that previous null returns are over-written
    a1 = vm.invokeAsync(() -> getAndIncStaticCount());
    a1.join();
    assertEquals(new Integer(4), a1.getReturnValue());

    // Assert object method invocation works with zero arg method
    final VMTestObject o = new VMTestObject(0);
    a1 = vm.invokeAsync(o, "incrementAndGet", new Object[] {});
    a1.join();
    assertEquals(new Integer(1), a1.getReturnValue());
    // Assert object method invocation works with no return
    a1 = vm.invokeAsync(o, "set", new Object[] {new Integer(3)});
    a1.join();
    assertNull(a1.getReturnValue());
  }

  private static Integer getAndIncStaticCount() {
    return new Integer(COUNTER.getAndIncrement());
  }

  private static Integer incrementStaticCount(Integer inc) {
    return new Integer(COUNTER.addAndGet(inc.intValue()));
  }

  private static void incStaticCount() {
    COUNTER.incrementAndGet();
  }

  /**
   * Accessed via reflection. DO NOT REMOVE
   */
  private static byte remoteByteMethod() {
    return BYTE_VALUE;
  }

  /**
   * Accessed via reflection. DO NOT REMOVE
   */
  private static boolean remoteBooleanMethod() {
    return BOOLEAN_VALUE;
  }

  /**
   * Accessed via reflection. DO NOT REMOVE
   */
  private static long remoteLongMethod() {
    return LONG_VALUE;
  }

  private static class ClassWithLong implements Serializable {
    public long getLong() {
      return LONG_VALUE;
    }
  }

  private static class ClassWithByte implements Serializable {
    public byte getByte() {
      return BYTE_VALUE;
    }
  }

  private static class InvokeRunnable implements SerializableRunnableIF {
    public void run() {
      throw new BasicTestException();
    }
  }

  private static class ClassWithString implements Serializable {
    public String getString() {
      return STRING_VALUE;
    }
  }

  private static class BasicTestException extends RuntimeException {
    BasicTestException() {
      this("Test exception.  Please ignore.");
    }

    BasicTestException(String s) {
      super(s);
    }
  }

  private static class VMTestObject implements Serializable {
    private static final long serialVersionUID = 1L;
    private final AtomicInteger val;

    public VMTestObject(int init) {
      this.val = new AtomicInteger(init);
    }

    public Integer get() {
      return new Integer(this.val.get());
    }

    public Integer incrementAndGet() {
      return new Integer(this.val.incrementAndGet());
    }

    public void set(Integer newVal) {
      this.val.set(newVal.intValue());
    }
  }
}
