/*
 * Copyright 2017, OpenCensus Authors
 *
 * Licensed 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 io.opencensus.contrib.agent.instrumentation;

import static com.google.common.truth.Truth.assertThat;

import io.grpc.Context;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Integration tests for {@link ExecutorInstrumentation}.
 *
 * <p>The integration tests are executed in a separate JVM that has the OpenCensus agent enabled via
 * the {@code -javaagent} command line option.
 */
@RunWith(JUnit4.class)
@SuppressWarnings("checkstyle:AbbreviationAsWordInName")
public class ExecutorInstrumentationIT {

  private static final Context.Key<String> KEY = Context.key("mykey");

  private ExecutorService executor;
  private Context previousContext;

  @Before
  public void beforeMethod() {
    executor = Executors.newCachedThreadPool();
  }

  @After
  public void afterMethod() {
    Context.current().detach(previousContext);
    executor.shutdown();
  }

  @Test(timeout = 60000)
  public void execute() throws Exception {
    final Thread callerThread = Thread.currentThread();
    final Context context = Context.current().withValue(KEY, "myvalue");
    previousContext = context.attach();

    final AtomicBoolean tested = new AtomicBoolean(false);

    executor.execute(
        new Runnable() {
          @Override
          public void run() {
            assertThat(Thread.currentThread()).isNotSameAs(callerThread);
            assertThat(Context.current()).isSameAs(context);
            assertThat(KEY.get()).isEqualTo("myvalue");
            tested.set(true);

            synchronized (tested) {
              tested.notify();
            }
          }
        });

    synchronized (tested) {
      tested.wait();
    }

    assertThat(tested.get()).isTrue();
  }

  @Test(timeout = 60000)
  public void submit_Callable() throws Exception {
    final Thread callerThread = Thread.currentThread();
    final Context context = Context.current().withValue(KEY, "myvalue");
    previousContext = context.attach();

    final AtomicBoolean tested = new AtomicBoolean(false);

    executor
        .submit(
            new Callable<Void>() {
              @Override
              public Void call() throws Exception {
                assertThat(Thread.currentThread()).isNotSameAs(callerThread);
                assertThat(Context.current()).isSameAs(context);
                assertThat(KEY.get()).isEqualTo("myvalue");
                tested.set(true);

                return null;
              }
            })
        .get();

    assertThat(tested.get()).isTrue();
  }

  @Test(timeout = 60000)
  public void submit_Runnable() throws Exception {
    final Thread callerThread = Thread.currentThread();
    final Context context = Context.current().withValue(KEY, "myvalue");
    previousContext = context.attach();

    final AtomicBoolean tested = new AtomicBoolean(false);

    executor
        .submit(
            new Runnable() {
              @Override
              public void run() {
                assertThat(Thread.currentThread()).isNotSameAs(callerThread);
                assertThat(Context.current()).isSameAs(context);
                assertThat(KEY.get()).isEqualTo("myvalue");
                tested.set(true);
              }
            })
        .get();

    assertThat(tested.get()).isTrue();
  }

  @Test(timeout = 60000)
  public void submit_RunnableWithResult() throws Exception {
    final Thread callerThread = Thread.currentThread();
    final Context context = Context.current().withValue(KEY, "myvalue");
    previousContext = context.attach();

    final AtomicBoolean tested = new AtomicBoolean(false);
    Object result = new Object();

    Future<Object> future =
        executor.submit(
            new Runnable() {
              @Override
              public void run() {
                assertThat(Thread.currentThread()).isNotSameAs(callerThread);
                assertThat(Context.current()).isNotSameAs(Context.ROOT);
                assertThat(Context.current()).isSameAs(context);
                assertThat(KEY.get()).isEqualTo("myvalue");
                tested.set(true);
              }
            },
            result);

    assertThat(future.get()).isSameAs(result);
    assertThat(tested.get()).isTrue();
  }

  @Test(timeout = 60000)
  public void currentContextExecutor() throws Exception {
    final Thread callerThread = Thread.currentThread();
    final Context context = Context.current().withValue(KEY, "myvalue");
    previousContext = context.attach();

    final AtomicBoolean tested = new AtomicBoolean(false);

    Context.currentContextExecutor(executor)
        .execute(
            new Runnable() {
              @Override
              public void run() {
                StackTraceElement[] ste = new Exception().fillInStackTrace().getStackTrace();
                assertThat(ste[0].getClassName()).doesNotContain("Context");
                assertThat(ste[1].getClassName()).startsWith("io.grpc.Context$");
                // NB: Actually, we want the Runnable to be wrapped only once, but currently it is
                // still wrapped twice. The two places where the Runnable is wrapped are: (1) the
                // executor implementation itself, e.g. ThreadPoolExecutor, to which the Agent added
                // automatic context propagation, (2) CurrentContextExecutor.
                // ExecutorInstrumentation already avoids adding the automatic context propagation
                // to CurrentContextExecutor, but does not make it a no-op yet. Also see
                // ExecutorInstrumentation#createMatcher.
                assertThat(ste[2].getClassName()).startsWith("io.grpc.Context$");
                assertThat(ste[3].getClassName()).doesNotContain("Context");

                assertThat(Thread.currentThread()).isNotSameAs(callerThread);
                assertThat(Context.current()).isSameAs(context);
                assertThat(KEY.get()).isEqualTo("myvalue");
                tested.set(true);

                synchronized (tested) {
                  tested.notify();
                }
              }
            });

    synchronized (tested) {
      tested.wait();
    }

    assertThat(tested.get()).isTrue();
  }
}

@@ -886,24 +886,63 @@
 ent.Future;%0A
+import java.util.concurrent.Semaphore;%0A
 import java.
@@ -2055,37 +2055,33 @@
 %0A%0A    final 
-AtomicBoolean
+Semaphore
  tested = ne
@@ -2082,35 +2082,27 @@
 d = new 
-AtomicBoolean(false
+Semaphore(0
 );%0A%0A    
@@ -2408,216 +2408,61 @@
 ted.
-set(true);%0A%0A            synchronized (tested) %7B%0A              tested.notify();%0A            %7D%0A          %7D%0A        %7D);%0A%0A    synchronized (tested) %7B%0A      tested.wait();%0A    %7D%0A%0A    assertThat(tested.get()).isTru
+release();%0A          %7D%0A        %7D);%0A%0A    tested.acquir
 e();
@@ -5228,37 +5228,33 @@
 %0A%0A    final 
-AtomicBoolean
+Semaphore
  tested = ne
@@ -5255,35 +5255,27 @@
 d = new 
-AtomicBoolean(false
+Semaphore(0
 );%0A%0A    
@@ -6650,32 +6650,33 @@
 lTo(%22myvalue%22);%0A
+%0A
                 
@@ -6686,97 +6686,19 @@
 ted.
-set(true);%0A%0A                synchronized (tested) %7B%0A                  tested.notify
+release
 ();%0A
-  
     
@@ -6725,119 +6725,30 @@
     
-  %7D%0A            %7D);%0A%0A    synchronized (tested) %7B%0A      tested.wait();%0A    %7D%0A%0A    assertThat(tested.get()).isTru
+%7D);%0A%0A    tested.acquir
 e();

Fix the wrong use of notify and wait. (#798)