/*
 * Copyright 2021 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.stringconcat;

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.FromParameterValueSource;
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.ParameterValueSource;
import com.google.devtools.build.android.desugar.testing.junit.RuntimeMethodHandle;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.Arrays;
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.MethodNode;

/**
 * Tests for accessing a series of private fields, constructors and methods from another class
 * within a nest.
 */
@RunWith(DesugarRunner.class)
@JdkSuppress(minJdkVersion = JdkVersion.V11)
public final class IndyStringConcatDesugaringlTest {

  @Rule
  public final DesugarRule desugarRule =
      DesugarRule.builder(this, MethodHandles.lookup())
          .addSourceInputsFromJvmFlag("input_srcs")
          .addJavacOptions("-source 11", "-target 11", "-XDstringConcat=indyWithConstants")
          .setWorkingJavaPackage("com.google.devtools.build.android.desugar.stringconcat")
          .addCommandOptions("desugar_indy_string_concat", "true")
          .build();

  @Test
  public void invokeDynamicInstr_presentBeforeDesugaring(
      @AsmNode(className = "StringConcatTestCases", memberName = "simplePrefix", round = 0)
          MethodNode simpleStrConcatBeforeDesugar) {
    AbstractInsnNode[] instructions = simpleStrConcatBeforeDesugar.instructions.toArray();
    assertThat(Arrays.stream(instructions).map(AbstractInsnNode::getOpcode))
        .contains(Opcodes.INVOKEDYNAMIC);
  }

  @Test
  public void invokeDynamicInstr_absentAfterDesugaring(
      @AsmNode(className = "StringConcatTestCases", memberName = "simplePrefix", round = 1)
          MethodNode simpleStrConcatAfterDesugar) {
    AbstractInsnNode[] instructions = simpleStrConcatAfterDesugar.instructions.toArray();
    assertThat(Arrays.stream(instructions).map(AbstractInsnNode::getOpcode))
        .doesNotContain(Opcodes.INVOKEDYNAMIC);
  }

  @Test
  @ParameterValueSource({"", "", ""})
  @ParameterValueSource({"", "b", "b"})
  @ParameterValueSource({"a", "", "a"})
  @ParameterValueSource({"a", "b", "ab"})
  @ParameterValueSource({"ab", "cd", "abcd"})
  public void twoConcat(
      @RuntimeMethodHandle(
              className = "StringConcatTestCases",
              memberName = "twoConcat",
              memberDescriptor = "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;")
          MethodHandle twoConcat,
      @FromParameterValueSource String x,
      @FromParameterValueSource String y,
      @FromParameterValueSource String expectedResult)
      throws Throwable {
    String result = (String) twoConcat.invoke(x, y);
    assertThat(result).isEqualTo(expectedResult);
  }

  @Test
  public void twoConcat_StringAndObject(
      @RuntimeMethodHandle(
              className = "StringConcatTestCases",
              memberName = "twoConcat",
              memberDescriptor = "(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;")
          MethodHandle twoConcat)
      throws Throwable {
    String result = (String) twoConcat.invoke("ab", (Object) "cd");
    assertThat(result).isEqualTo("T:abcd");
  }

  @Test
  public void twoConcatWithConstants(
      @RuntimeMethodHandle(
              className = "StringConcatTestCases",
              memberName = "twoConcatWithConstants",
              memberDescriptor = "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;")
          MethodHandle twoConcat)
      throws Throwable {
    String result = (String) twoConcat.invoke("ab", "cd");
    assertThat(result).isEqualTo("ab<constant>cd");
  }

  @Test
  public void threeConcat(
      @RuntimeMethodHandle(className = "StringConcatTestCases", memberName = "threeConcat")
          MethodHandle threeConcat)
      throws Throwable {
    String result = (String) threeConcat.invoke("ab", "cd", "ef");
    assertThat(result).isEqualTo("abcdef");
  }

  @Test
  public void twoConcatWithRecipe(
      @RuntimeMethodHandle(className = "StringConcatTestCases", memberName = "twoConcatWithRecipe")
          MethodHandle twoConcat)
      throws Throwable {
    String result = (String) twoConcat.invoke("ab", "cd");
    assertThat(result).isEqualTo("<p>ab<br>cd</p>");
  }

  @Test
  public void threeConcatWithRecipe(
      @RuntimeMethodHandle(
              className = "StringConcatTestCases",
              memberName = "threeConcatWithRecipe")
          MethodHandle threeConcat)
      throws Throwable {
    String result = (String) threeConcat.invoke("ab", "cd", "ef");
    assertThat(result).isEqualTo("<p>ab<br>cd<br>ef</p>");
  }

  @Test
  @ParameterValueSource({"a", "0", "a0"})
  @ParameterValueSource({"a", "1", "a1"})
  @ParameterValueSource({"b", "3", "b3"})
  @ParameterValueSource({"c", "7", "c7"})
  @ParameterValueSource({"a", "15", "a15"})
  @ParameterValueSource({"d", "2147483647", "d2147483647"}) // Integer.MAX_VALUE
  @ParameterValueSource({"d", "-2147483648", "d-2147483648"}) // Integer.MIN_VALUE
  public void twoConcatWithPrimitives_StringAndInt(
      @RuntimeMethodHandle(
              className = "StringConcatTestCases",
              memberName = "twoConcatWithPrimitives",
              memberDescriptor = "(Ljava/lang/String;I)Ljava/lang/String;")
          MethodHandle twoConcat,
      @FromParameterValueSource String x,
      @FromParameterValueSource int y,
      @FromParameterValueSource String expectedResult)
      throws Throwable {
    String result = (String) twoConcat.invoke(x, y);
    assertThat(result).isEqualTo(expectedResult);
  }

  @Test
  @ParameterValueSource({"a", "1", "a1"})
  @ParameterValueSource({"b", "3", "b3"})
  @ParameterValueSource({"c", "7", "c7"})
  @ParameterValueSource({"a", "15", "a15"})
  @ParameterValueSource({"d", "9223372036854775807", "d9223372036854775807"}) // Long.MAX_VALUE
  @ParameterValueSource({"e", "-9223372036854775808", "e-9223372036854775808"}) // Long.MIN_VALUE
  public void twoConcatWithPrimitives_StringAndLong(
      @RuntimeMethodHandle(
              className = "StringConcatTestCases",
              memberName = "twoConcatWithPrimitives",
              memberDescriptor = "(Ljava/lang/String;J)Ljava/lang/String;")
          MethodHandle twoConcat,
      @FromParameterValueSource String x,
      @FromParameterValueSource long y,
      @FromParameterValueSource String expectedResult)
      throws Throwable {
    String result = (String) twoConcat.invoke(x, y);
    assertThat(result).isEqualTo(expectedResult);
  }

  @Test
  @ParameterValueSource({"a", "123.125", "a123.125"})
  @ParameterValueSource({
    "max-double-",
    "1.7976931348623157E+308",
    "max-double-1.7976931348623157E308"
  })
  @ParameterValueSource({
    "min-double-",
    "2.2250738585072014E-308",
    "min-double-2.2250738585072014E-308"
  })
  public void twoConcatWithPrimitives_StringAndDouble(
      @RuntimeMethodHandle(
              className = "StringConcatTestCases",
              memberName = "twoConcatWithPrimitives",
              memberDescriptor = "(Ljava/lang/String;D)Ljava/lang/String;")
          MethodHandle twoConcat,
      @FromParameterValueSource String x,
      @FromParameterValueSource double y,
      @FromParameterValueSource String expectedResult)
      throws Throwable {
    String result = (String) twoConcat.invoke(x, y);
    assertThat(result).isEqualTo(expectedResult);
  }

  @Test
  public void twoConcatWithPrimitives_intAndString(
      @RuntimeMethodHandle(
              className = "StringConcatTestCases",
              memberName = "twoConcatWithPrimitives",
              memberDescriptor = "(ILjava/lang/String;)Ljava/lang/String;")
          MethodHandle twoConcat)
      throws Throwable {
    String result = (String) twoConcat.invoke(123, "ABC");
    assertThat(result).isEqualTo("123ABC");
  }

  @Test
  public void twoConcatWithPrimitives_longAndString(
      @RuntimeMethodHandle(
              className = "StringConcatTestCases",
              memberName = "twoConcatWithPrimitives",
              memberDescriptor = "(JLjava/lang/String;)Ljava/lang/String;")
          MethodHandle twoConcat)
      throws Throwable {
    String result = (String) twoConcat.invoke(123L, "ABC");
    assertThat(result).isEqualTo("123ABC");
  }

  @Test
  public void twoConcatWithPrimitives_doubleAndString(
      @RuntimeMethodHandle(
              className = "StringConcatTestCases",
              memberName = "twoConcatWithPrimitives",
              memberDescriptor = "(DLjava/lang/String;)Ljava/lang/String;")
          MethodHandle twoConcat)
      throws Throwable {
    String result = (String) twoConcat.invoke(123.125, "ABC");
    assertThat(result).isEqualTo("123.125ABC");
  }

  @Test
  @ParameterValueSource({
    "head", // string value
    "1", // int value
    "true", // boolean value
    "2", // byte value
    "c", // char value
    "4", // short value
    "5.25", // double value
    "6.5F", // float value
    "7", // long value
    "head/1/true/2/c/4/5.25/6.5/7" // expectedResult
  })
  @ParameterValueSource({
    "min:", // string value
    "-2147483648", // int value
    "false", // boolean value
    "-128", // byte value
    "~", // char value
    "-32768", // short value
    "1.7976931348623157E+308", // double value
    "1.4E-45F", // float value
    "-9223372036854775808", // long value
    "min:/-2147483648/false/-128/~/-32768/1.7976931348623157E308/1.4E-45/-9223372036854775808"
  })
  @ParameterValueSource({
    "max:", // string value
    "2147483647", // int value
    "true", // boolean value
    "127", // byte value
    "~", // char value
    "32767", // short value
    "2.2250738585072014E-308", // double value
    "3.4028235E+38F", // float value
    "9223372036854775807", // long value
    "max:/2147483647/true/127/~/32767/2.2250738585072014E-308/3.4028235E38/9223372036854775807"
  })
  public void concatWithAllPrimitiveTypes(
      @RuntimeMethodHandle(
              className = "StringConcatTestCases",
              memberName = "concatWithAllPrimitiveTypes")
          MethodHandle concatWithAllPrimitiveTypes,
      @FromParameterValueSource String stringValue,
      @FromParameterValueSource int intValue,
      @FromParameterValueSource boolean booleanValue,
      @FromParameterValueSource byte byteValue,
      @FromParameterValueSource char charValue,
      @FromParameterValueSource short shortValue,
      @FromParameterValueSource double doubleValue,
      @FromParameterValueSource float floatValue,
      @FromParameterValueSource long longValue,
      @FromParameterValueSource String expectedResult)
      throws Throwable {
    String result =
        (String)
            concatWithAllPrimitiveTypes.invoke(
                stringValue,
                intValue,
                booleanValue,
                byteValue,
                charValue,
                shortValue,
                doubleValue,
                floatValue,
                longValue);
    assertThat(result).isEqualTo(expectedResult);
  }
}
