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

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

import com.google.devtools.build.android.desugar.testing.junit.AsmNode;
import com.google.devtools.build.android.desugar.testing.junit.DesugarRule;
import com.google.devtools.build.android.desugar.testing.junit.DesugarRunner;
import com.google.devtools.build.android.desugar.testing.junit.DynamicClassLiteral;
import com.google.devtools.build.android.desugar.testing.junit.JdkSuppress;
import com.google.devtools.build.android.desugar.testing.junit.JdkVersion;
import com.google.devtools.build.android.desugar.testing.junit.RuntimeMethodHandle;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Inject;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;

/** Tests for accessing private fields from another class within a nest. */
@RunWith(DesugarRunner.class)
@JdkSuppress(minJdkVersion = JdkVersion.V11)
public final class NestDesugaringFieldAccessTest {

  @Rule
  public final DesugarRule desugarRule =
      DesugarRule.builder(this, MethodHandles.lookup())
          .addSourceInputsFromJvmFlag("input_srcs")
          .setWorkingJavaPackage(
              "com.google.devtools.build.android.desugar.nest.testsrc.simpleunit.field")
          .addJavacOptions("--release 11")
          .addCommandOptions("desugar_nest_based_private_access", "true")
          .build();

  @Inject
  @DynamicClassLiteral("FieldNest$FieldOwnerMate")
  private Class<?> mate;

  private Object mateInstance;

  @Before
  public void loadClasses() throws Exception {
    mateInstance = mate.getConstructor().newInstance();
  }

  @Test
  public void inputClassFileMajorVersions(
      @AsmNode(className = "FieldNest", round = 0) ClassNode beforeDesugarClassNode,
      @AsmNode(className = "FieldNest", round = 1) ClassNode afterDesugarClassNode) {
    assertThat(beforeDesugarClassNode.version).isEqualTo(JdkVersion.V11);
    assertThat(afterDesugarClassNode.version).isEqualTo(JdkVersion.V1_7);
  }

  @Test
  public void bridgeMethodGeneration() {
    List<String> bridgeMethodNames =
        Arrays.stream(mate.getDeclaredMethods())
            .map(Method::getName)
            .filter(name -> !name.startsWith("$jacoco"))
            .collect(Collectors.toList());
    assertThat(bridgeMethodNames)
        .containsExactly(
            "privateStaticField$bridge_getter",
            "privateStaticField$bridge_setter",
            "privateInstanceField$bridge_getter",
            "privateInstanceField$bridge_setter",
            "privateInstanceWideField$bridge_setter",
            "getPrivateStaticFieldInBoundary",
            "getPrivateInstanceFieldInBoundary",
            "privateStaticFieldReadOnly$bridge_getter",
            "privateInstanceFieldReadOnly$bridge_getter",
            "privateStaticArrayField$bridge_getter",
            "privateInstanceArrayField$bridge_getter");
  }

  @Test
  public void getStaticField(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "getStaticField")
          MethodHandle getStaticField)
      throws Throwable {
    long result = (long) getStaticField.invoke();
    assertThat(result).isEqualTo(10L);
  }

  @Test
  public void getInstanceField(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "getInstanceField")
          MethodHandle getInstanceField)
      throws Throwable {
    long result = (long) getInstanceField.invoke(mateInstance);
    assertThat(result).isEqualTo(20);
  }

  @Test
  public void getPrivateStaticField(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "getPrivateStaticField")
          MethodHandle getPrivateStaticField)
      throws Throwable {
    long result = (long) getPrivateStaticField.invoke();
    assertThat(result).isEqualTo(30L);
  }

  @Test
  public void setPrivateStaticField(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "setPrivateStaticField")
          MethodHandle setPrivateStaticField)
      throws Throwable {
    long result = (long) setPrivateStaticField.invoke((long) 35L);
    assertThat(result).isEqualTo(35L);

    Field privateStaticField = mate.getDeclaredField("privateStaticField");
    privateStaticField.setAccessible(true);
    assertThat(privateStaticField.get(null)).isEqualTo(35L);
  }

  @Test
  public void getPrivateInstanceField(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "getPrivateInstanceField")
          MethodHandle getPrivateInstanceField)
      throws Throwable {
    int result = (int) getPrivateInstanceField.invoke(mateInstance);
    assertThat(result).isEqualTo(40);
  }

  @Test
  public void setPrivateInstanceField(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "setPrivateInstanceField")
          MethodHandle setPrivateInstanceField)
      throws Throwable {
    int result = (int) setPrivateInstanceField.invoke(mateInstance, (int) 45);
    assertThat(result).isEqualTo(45);

    Field privateInstanceField = mate.getDeclaredField("privateInstanceField");
    privateInstanceField.setAccessible(true);
    assertThat(privateInstanceField.get(mateInstance)).isEqualTo(45);
  }

  @Test
  public void setPrivateInstanceWideField(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "setPrivateInstanceWideField")
          MethodHandle setPrivateInstanceWideField)
      throws Throwable {
    long result = (long) setPrivateInstanceWideField.invoke(mateInstance, 47L);
    assertThat(result).isEqualTo(47L);

    Field privateInstanceField = mate.getDeclaredField("privateInstanceWideField");
    privateInstanceField.setAccessible(true);
    assertThat(privateInstanceField.get(mateInstance)).isEqualTo(47L);
  }

  @Test
  public void setPrivateInstanceWideField_opcodes(
      @AsmNode(className = "FieldNest", memberName = "setPrivateInstanceWideField")
          MethodNode setPrivateInstanceWideField)
      throws Throwable {
    AbstractInsnNode[] instructions = setPrivateInstanceWideField.instructions.toArray();
    assertThat(Arrays.stream(instructions).map(AbstractInsnNode::getOpcode))
        .containsAtLeast(
            Opcodes.ALOAD,
            Opcodes.LLOAD,
            Opcodes.DUP2_X1,
            Opcodes.INVOKESTATIC,
            Opcodes.POP2,
            Opcodes.LRETURN)
        .inOrder();
  }

  @Test
  public void getPrivateStaticFieldReadOnly(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "getPrivateStaticFieldReadOnly")
          MethodHandle getPrivateStaticFieldReadOnly)
      throws Throwable {
    long result = (long) getPrivateStaticFieldReadOnly.invoke();
    assertThat(result).isEqualTo(50L);
  }

  @Test
  public void getPrivateInstanceFieldReadOnly(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "getPrivateInstanceFieldReadOnly")
          MethodHandle getPrivateInstanceFieldReadOnly)
      throws Throwable {
    long result = (long) getPrivateInstanceFieldReadOnly.invoke(mateInstance);
    assertThat(result).isEqualTo(60L);
  }

  @Test
  public void getPrivateInstanceFieldReadOnly_trace(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "getPrivateInstanceFieldReadOnly")
          MethodHandle getPrivateInstanceFieldReadOnly)
      throws Throwable {
    long result = (long) getPrivateInstanceFieldReadOnly.invoke(mateInstance);
    assertThat(result).isEqualTo(60L);
  }

  @Test
  public void getPrivateStaticFieldInBoundary(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "getPrivateStaticFieldInBoundary")
          MethodHandle getPrivateStaticFieldInBoundary)
      throws Throwable {
    long result = (long) getPrivateStaticFieldInBoundary.invoke();
    assertThat(result).isEqualTo(70L);
  }

  @Test
  public void getPrivateInstanceFieldInBoundary(
      @RuntimeMethodHandle(
              className = "FieldNest",
              memberName = "getPrivateInstanceFieldInBoundary")
          MethodHandle getPrivateInstanceFieldInBoundary)
      throws Throwable {
    int result = (int) getPrivateInstanceFieldInBoundary.invoke(mateInstance);
    assertThat(result).isEqualTo(80);
  }

  @Test
  public void getPrivateStaticArrayFieldElement(
      @RuntimeMethodHandle(
              className = "FieldNest",
              memberName = "getPrivateStaticArrayFieldElement")
          MethodHandle getPrivateStaticArrayFieldElement)
      throws Throwable {
    long[] arrayFieldInitialValue = {100L, 200L, 300L};

    Field field = mate.getDeclaredField("privateStaticArrayField");
    field.setAccessible(true);
    field.set(null, arrayFieldInitialValue);

    long result = (long) getPrivateStaticArrayFieldElement.invoke(1); // 1 is for index.
    assertThat(result).isEqualTo(200L);
  }

  @Test
  public void setPrivateStaticArrayFieldElement(
      @RuntimeMethodHandle(
              className = "FieldNest",
              memberName = "setPrivateStaticArrayFieldElement")
          MethodHandle setPrivateStaticArrayFieldElement)
      throws Throwable {
    long[] arrayFieldInitialValue = {200L, 300L, 400L};
    long overriddenValue = 3000L;

    Field field = mate.getDeclaredField("privateStaticArrayField");
    field.setAccessible(true);
    field.set(null, arrayFieldInitialValue);

    long result = (long) setPrivateStaticArrayFieldElement.invoke(1, overriddenValue);
    assertThat(result).isEqualTo(overriddenValue);

    long[] actual = (long[]) field.get(null);
    assertThat(actual).asList().containsExactly(200L, 3000L, 400L).inOrder();
  }

  @Test
  public void getPrivateInstanceArrayFieldElement(
      @RuntimeMethodHandle(
              className = "FieldNest",
              memberName = "getPrivateInstanceArrayFieldElement")
          MethodHandle getPrivateInstanceArrayFieldElement)
      throws Throwable {
    int[] arrayFieldInitialValue = {300, 400, 500};

    Field field = mate.getDeclaredField("privateInstanceArrayField");
    field.setAccessible(true);
    field.set(mateInstance, arrayFieldInitialValue);

    int result = (int) getPrivateInstanceArrayFieldElement.invoke(mateInstance, 1);
    assertThat(result).isEqualTo(400);
  }

  @Test
  public void setPrivateInstanceArrayFieldElement(
      @RuntimeMethodHandle(
              className = "FieldNest",
              memberName = "setPrivateInstanceArrayFieldElement")
          MethodHandle setPrivateInstanceArrayFieldElement)
      throws Throwable {
    int[] arrayFieldInitialValue = {400, 500, 600};
    int overriddenValue = 5000;

    Field field = mate.getDeclaredField("privateInstanceArrayField");
    field.setAccessible(true);
    field.set(mateInstance, arrayFieldInitialValue);

    long result =
        (long) setPrivateInstanceArrayFieldElement.invoke(mateInstance, 1, overriddenValue);

    assertThat(result).isEqualTo(overriddenValue);

    int[] actual = (int[]) field.get(mateInstance);
    assertThat(actual).asList().containsExactly(400, 5000, 600).inOrder();
  }

  @Test
  public void compoundSetPrivateStaticField(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "compoundSetPrivateStaticField")
          MethodHandle compoundSetPrivateStaticField)
      throws Throwable {
    long fieldInitialValue = 100;
    long fieldValueDelta = 20;

    Field field = mate.getDeclaredField("privateStaticField");
    field.setAccessible(true);
    field.set(null, fieldInitialValue);

    long result = (long) compoundSetPrivateStaticField.invoke(fieldValueDelta);
    assertThat(result).isEqualTo(fieldInitialValue + fieldValueDelta);

    assertThat(field.get(null)).isEqualTo(fieldInitialValue + fieldValueDelta);
  }

  @Test
  public void preIncrementPrivateStaticField(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "preIncrementPrivateStaticField")
          MethodHandle preIncrementPrivateStaticField)
      throws Throwable {
    long fieldInitialValue = 200;

    Field field = mate.getDeclaredField("privateStaticField");
    field.setAccessible(true);
    field.set(null, fieldInitialValue);

    long result = (long) preIncrementPrivateStaticField.invoke();
    assertThat(result).isEqualTo(fieldInitialValue + 1);

    assertThat(field.get(null)).isEqualTo(fieldInitialValue + 1);
  }

  @Test
  public void postIncrementPrivateStaticField(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "postIncrementPrivateStaticField")
          MethodHandle postIncrementPrivateStaticField)
      throws Throwable {
    long fieldInitialValue = 300;

    Field field = mate.getDeclaredField("privateStaticField");
    field.setAccessible(true);
    field.set(null, fieldInitialValue);

    long result = (long) postIncrementPrivateStaticField.invoke();
    assertThat(result).isEqualTo(fieldInitialValue);

    assertThat(field.get(null)).isEqualTo(fieldInitialValue + 1);
  }

  @Test
  public void compoundSetPrivateInstanceField(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "compoundSetPrivateInstanceField")
          MethodHandle compoundSetPrivateInstanceField)
      throws Throwable {
    int fieldInitialValue = 400;
    int fieldDelta = 10;

    Field field = mate.getDeclaredField("privateInstanceField");
    field.setAccessible(true);
    field.set(mateInstance, fieldInitialValue);

    int result = (int) compoundSetPrivateInstanceField.invoke(mateInstance, fieldDelta);
    assertThat(result).isEqualTo(fieldInitialValue + fieldDelta);

    assertThat(field.get(mateInstance)).isEqualTo(fieldInitialValue + fieldDelta);
  }

  @Test
  public void preIncrementPrivateInstanceField(
      @RuntimeMethodHandle(className = "FieldNest", memberName = "preIncrementPrivateInstanceField")
          MethodHandle preIncrementPrivateInstanceField)
      throws Throwable {
    int fieldInitialValue = 500;

    Field field = mate.getDeclaredField("privateInstanceField");
    field.setAccessible(true);
    field.set(mateInstance, fieldInitialValue);

    int result = (int) preIncrementPrivateInstanceField.invoke(mateInstance);
    assertThat(result).isEqualTo(fieldInitialValue + 1);

    assertThat(field.get(mateInstance)).isEqualTo(fieldInitialValue + 1);
  }

  @Test
  public void postIncrementPrivateInstanceField(
      @RuntimeMethodHandle(
              className = "FieldNest",
              memberName = "postIncrementPrivateInstanceField")
          MethodHandle postIncrementPrivateInstanceField)
      throws Throwable {
    int fieldInitialValue = 600;

    Field field = mate.getDeclaredField("privateInstanceField");
    field.setAccessible(true);
    field.set(mateInstance, fieldInitialValue);

    int result = (int) postIncrementPrivateInstanceField.invoke(mateInstance);
    assertThat(result).isEqualTo(fieldInitialValue);

    assertThat(field.get(mateInstance)).isEqualTo(fieldInitialValue + 1);
  }
}
