/*
 * Copyright 2019 The Bazel Authors. All rights reserved.
 *
 * 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 com.google.devtools.build.android.desugar.testing.junit;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static org.junit.Assert.assertThrows;

import com.google.devtools.build.android.desugar.io.JarItem;
import com.google.devtools.build.android.desugar.testing.junit.RuntimeMethodHandle.MemberUseContext;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.nio.file.Paths;
import java.util.Arrays;
import javax.inject.Inject;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;

/** The test for {@link DesugarRule}. */
@JdkSuppress(minJdkVersion = JdkVersion.V11)
@RunWith(DesugarRunner.class)
public final class DesugarRuleTest {

  @Rule
  public final DesugarRule desugarRule =
      DesugarRule.builder(this, MethodHandles.lookup())
          .addInputs(Paths.get(System.getProperty("input_jar")))
          .addSourceInputsFromJvmFlag("input_srcs")
          .addJavacOptions("--release 11")
          .enableIterativeTransformation(3)
          .setWorkingJavaPackage("com.google.devtools.build.android.desugar.testing.junit")
          .build();

  @Inject
  @DynamicClassLiteral("DesugarRuleTestTarget$InterfaceSubjectToDesugar")
  private Class<?> interfaceSubjectToDesugarRound1;

  @Inject
  @DynamicClassLiteral(value = "DesugarRuleTestTarget$InterfaceSubjectToDesugar", round = 2)
  private Class<?> interfaceSubjectToDesugarRound2;

  @Inject
  @DynamicClassLiteral("DesugarRuleTestTarget$InterfaceSubjectToDesugar")
  private Class<?> interfaceSubjectToDesugarFromSimpleClassName;

  @Inject
  @DynamicClassLiteral(
      "com.google.devtools.build.android.desugar.testing.junit.DesugarRuleTestTarget$InterfaceSubjectToDesugar")
  private Class<?> interfaceSubjectToDesugarFromQualifiedClassName;

  @Inject
  @DynamicClassLiteral("DesugarRuleTestTarget$InterfaceSubjectToDesugar$$CC")
  private Class<?> interfaceSubjectToDesugarCompanionClassRound1;

  @Inject
  @DynamicClassLiteral(value = "DesugarRuleTestTarget$InterfaceSubjectToDesugar$$CC", round = 2)
  private Class<?> interfaceSubjectToDesugarCompanionClassRound2;

  @Inject
  @AsmNode(className = "DesugarRuleTestTarget")
  private ClassNode desugarRuleTestTargetClassNode;

  @Inject
  @AsmNode(className = "DesugarRuleTestTarget$Alpha", memberName = "twoIntSum")
  private MethodNode twoIntSum;

  @Inject
  @AsmNode(
      className = "DesugarRuleTestTarget$Alpha",
      memberName = "multiplier",
      memberDescriptor = "J")
  private FieldNode multiplier;

  @Inject
  @RuntimeMethodHandle(className = "DesugarRuleTestTarget$Alpha", memberName = "<init>")
  private MethodHandle alphaConstructor;

  @Inject
  @RuntimeMethodHandle(
      className = "DesugarRuleTestTarget$Alpha",
      memberName = "linearLongTransform")
  private MethodHandle linearLongTransform;

  @Inject
  @RuntimeMethodHandle(
      className = "DesugarRuleTestTarget$Alpha",
      memberName = "multiplier",
      usage = MemberUseContext.FIELD_GETTER)
  private MethodHandle alphaMultiplierGetter;

  @Inject
  @RuntimeMethodHandle(
      className = "DesugarRuleTestTarget$Alpha",
      memberName = "multiplier",
      usage = MemberUseContext.FIELD_SETTER)
  private MethodHandle alphaMultiplierSetter;

  @Test
  public void staticMethodsAreMovedFromOriginatingClass() {
    assertThrows(
        NoSuchMethodException.class,
        () -> interfaceSubjectToDesugarRound1.getDeclaredMethod("staticMethod"));
  }

  @Test
  public void staticMethodsAreMovedFromOriginatingClass_desugarTwice() {
    assertThrows(
        NoSuchMethodException.class,
        () -> interfaceSubjectToDesugarRound2.getDeclaredMethod("staticMethod"));
  }

  @Test
  public void staticMethodsAreMovedToCompanionClass() {
    assertThat(
            Arrays.stream(interfaceSubjectToDesugarCompanionClassRound1.getDeclaredMethods())
                .map(Method::getName))
        .contains("staticMethod$$STATIC$$");
  }

  @Test
  public void innerClasses() {
    assertThat(
            desugarRuleTestTargetClassNode.innerClasses.stream().map(classNode -> classNode.name))
        .contains(
            "com/google/devtools/build/android/desugar/testing/junit/DesugarRuleTestTarget$InterfaceSubjectToDesugar");
  }

  @Test
  public void idempotencyOperation(
      @RuntimeJarEntry(
              value = "DesugarRuleTestTarget$InterfaceSubjectToDesugar$$CC.class",
              round = 1)
          JarItem interfaceSubjectToDesugarJarEntryRound1,
      @RuntimeJarEntry(
              value = "DesugarRuleTestTarget$InterfaceSubjectToDesugar$$CC.class",
              round = 2)
          JarItem interfaceSubjectToDesugarJarEntryRound2) {
    assertThat(interfaceSubjectToDesugarJarEntryRound1.jarEntry().getCrc())
        .isEqualTo(interfaceSubjectToDesugarJarEntryRound2.jarEntry().getCrc());
  }

  @Test
  public void classLoaders_sameInstanceInSameRound() {
    assertThat(interfaceSubjectToDesugarRound1.getClassLoader())
        .isSameInstanceAs(interfaceSubjectToDesugarCompanionClassRound1.getClassLoader());
    assertThat(interfaceSubjectToDesugarRound2.getClassLoader())
        .isSameInstanceAs(interfaceSubjectToDesugarCompanionClassRound2.getClassLoader());
  }

  @Test
  public void classLiterals_requestedWithFullQualifiedClassName() {
    assertThat(interfaceSubjectToDesugarFromQualifiedClassName)
        .isSameInstanceAs(interfaceSubjectToDesugarFromSimpleClassName);
  }

  @Test
  public void injectClassLiteralsFromParam(
      @DynamicClassLiteral("DesugarRuleTestTarget$InterfaceSubjectToDesugar")
          Class<?> interfaceSubjectToDesugarParam) {
    assertThat(interfaceSubjectToDesugarParam).isSameInstanceAs(interfaceSubjectToDesugarRound1);
  }

  @Test
  public void injectFieldNodes() {
    assertThat(twoIntSum.desc).isEqualTo("(II)I");
  }

  @Test
  public void injectMethodNodes() {
    assertThat(multiplier.desc).isEqualTo("J");
  }

  @Test
  @ParameterValueSource({"1", "2", "3"})
  @ParameterValueSource({"100", "400", "500"})
  public void invokeStaticMethodHandle(
      @RuntimeMethodHandle(className = "DesugarRuleTestTarget$Alpha", memberName = "twoIntSum")
          MethodHandle twoIntSum,
      @FromParameterValueSource int x,
      @FromParameterValueSource int y,
      @FromParameterValueSource int expectedResult)
      throws Throwable {
    int result = (int) twoIntSum.invoke(x, y);
    assertThat(result).isEqualTo(expectedResult);
  }

  @Test
  public void invokeVirtualMethodHandle() throws Throwable {
    long result = (long) linearLongTransform.invoke(alphaConstructor.invoke(1000, 2), 3L);
    assertThat(result).isEqualTo(3002);
  }

  @Test
  public void invokeFieldGetter() throws Throwable {
    Object alpha = alphaConstructor.invoke(1000, 2);
    long result = (long) alphaMultiplierGetter.invoke(alpha);
    assertThat(result).isEqualTo(1000);
  }

  @Test
  public void invokeFieldSetter() throws Throwable {
    Object alpha = alphaConstructor.invoke(1000, 2);
    alphaMultiplierSetter.invoke(alpha, 1111);

    long result = (long) alphaMultiplierGetter.invoke(alpha);
    assertThat(result).isEqualTo(1111);
  }

  @Test
  public void invokeRuntimeCompiledTarget(
      @RuntimeMethodHandle(className = "DesugarRuleTestSourceTarget", memberName = "twoSum")
          MethodHandle twoSum)
      throws Throwable {
    int result = (int) twoSum.invokeExact(1, 2);
    assertThat(result).isEqualTo(3);
  }
}
