// Copyright 2018 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;

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

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
import com.google.devtools.build.android.desugar.langmodel.MethodId;
import com.google.devtools.build.android.desugar.langmodel.MethodInvocation;
import com.google.devtools.build.android.desugar.retarget.ClassMemberRetargetConfig;
import com.google.devtools.build.android.desugar.retarget.MethodInvocationReplacement;
import com.google.devtools.build.android.desugar.retarget.ReplacementRange;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.objectweb.asm.Opcodes;

/** Tests for {@link CoreLibrarySupport}. */
@RunWith(JUnit4.class)
public class CoreLibrarySupportTest {

  @Test
  public void testIsRenamedCoreLibrary() throws Exception {
    CoreLibrarySupport support =
        new CoreLibrarySupport(
            new CoreLibraryRewriter(""),
            null,
            ImmutableList.of("java/time/"),
            ImmutableList.of(),
            ImmutableList.of(),
            /* retargetConfig= */ null);
    assertThat(support.isRenamedCoreLibrary("java/time/X")).isTrue();
    assertThat(support.isRenamedCoreLibrary("java/time/y/X")).isTrue();
    assertThat(support.isRenamedCoreLibrary("java/io/X")).isFalse();
    assertThat(support.isRenamedCoreLibrary("java/io/X$$CC")).isTrue();
    assertThat(support.isRenamedCoreLibrary("java/io/X$$Lambda$17")).isTrue();
    assertThat(support.isRenamedCoreLibrary("com/google/X")).isFalse();
  }

  @Test
  public void testIsRenamedCoreLibrary_prefixedLoader() throws Exception {
    CoreLibrarySupport support =
        new CoreLibrarySupport(
            new CoreLibraryRewriter("__/"),
            null,
            ImmutableList.of("java/time/"),
            ImmutableList.of(),
            ImmutableList.of(),
            /* retargetConfig= */ null);
    assertThat(support.isRenamedCoreLibrary("__/java/time/X")).isTrue();
    assertThat(support.isRenamedCoreLibrary("__/java/time/y/X")).isTrue();
    assertThat(support.isRenamedCoreLibrary("__/java/io/X")).isFalse();
    assertThat(support.isRenamedCoreLibrary("__/java/io/X$$CC")).isTrue();
    assertThat(support.isRenamedCoreLibrary("__/java/io/X$$Lambda$17")).isTrue();
    assertThat(support.isRenamedCoreLibrary("com/google/X")).isFalse();
  }

  @Test
  public void testRenameCoreLibrary() throws Exception {
    CoreLibrarySupport support =
        new CoreLibrarySupport(
            new CoreLibraryRewriter(""),
            null,
            ImmutableList.of(),
            ImmutableList.of(),
            ImmutableList.of(),
            /* retargetConfig= */ null);
    assertThat(support.renameCoreLibrary("java/time/X")).isEqualTo("j$/time/X");
    assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X");
  }

  @Test
  public void testRenameCoreLibrary_prefixedLoader() throws Exception {
    CoreLibrarySupport support =
        new CoreLibrarySupport(
            new CoreLibraryRewriter("__/"),
            null,
            ImmutableList.of(),
            ImmutableList.of(),
            ImmutableList.of(),
            /* retargetConfig= */ null);
    assertThat(support.renameCoreLibrary("__/java/time/X")).isEqualTo("j$/time/X");
    assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X");
  }

  @Test
  public void testMoveTarget() throws Exception {
    CoreLibrarySupport support =
        new CoreLibrarySupport(
            new CoreLibraryRewriter("__desugar__/"),
            null,
            ImmutableList.of("java/util/Helper"),
            ImmutableList.of(),
            ImmutableList.of(),
            ClassMemberRetargetConfig.builder()
                .addInProcessReplacement(
                    MethodInvocationReplacement.newBuilder()
                        .setSource(
                            MethodInvocation.newBuilder()
                                .setOpcode(182)
                                .setIsInterface(false)
                                .setMethodId(
                                    MethodId.newBuilder()
                                        .setOwner("java/util/Existing")
                                        .setName("match")
                                        .setDesc("()V")))
                        .setDestination(
                            MethodInvocation.newBuilder()
                                .setOpcode(182)
                                .setIsInterface(false)
                                .setMethodId(
                                    MethodId.newBuilder()
                                        .setOwner("java/util/DesugarHelper")
                                        .setName("match")
                                        .setDesc("()V")))
                        .setAutoDeduceOpcodeAndDesc(true)
                        .addRange(ReplacementRange.ALL)
                        .build())
                .addInProcessReplacement(
                    MethodInvocationReplacement.newBuilder()
                        .setSource(
                            MethodInvocation.newBuilder()
                                .setOpcode(182)
                                .setIsInterface(false)
                                .setMethodId(
                                    MethodId.newBuilder()
                                        .setOwner("java/util/Existing")
                                        .setName("unused")
                                        .setDesc("()V")))
                        .setDestination(
                            MethodInvocation.newBuilder()
                                .setOpcode(182)
                                .setIsInterface(false)
                                .setMethodId(
                                    MethodId.newBuilder()
                                        .setOwner("com/google/Unused")
                                        .setName("unused")
                                        .setDesc("()V")))
                        .addRange(ReplacementRange.ALL)
                        .build())
                .addEnabledInvocationReplacementRange(ReplacementRange.ALL)
                .build());

    assertThat(support.getMoveTarget("__desugar__/java/util/Existing", "match", "()V"))
        .isEqualTo("j$/util/DesugarHelper");

    assertThat(support.getMoveTarget("java/util/Existing", "match", "()V"))
        .isEqualTo("j$/util/DesugarHelper");
    assertThat(support.getMoveTarget("__desugar__/java/util/Existing", "matchesnot", "()V"))
        .isNull();
    assertThat(support.getMoveTarget("__desugar__/java/util/ExistingOther", "match", "()V"))
        .isNull();
    assertThat(support.usedRuntimeHelpers()).containsExactly("j$/util/DesugarHelper");
  }

  @Test
  public void testIsEmulatedCoreClassOrInterface() throws Exception {
    CoreLibrarySupport support =
        new CoreLibrarySupport(
            new CoreLibraryRewriter(""),
            Thread.currentThread().getContextClassLoader(),
            ImmutableList.of("java/util/concurrent/"),
            ImmutableList.of("java/util/Map"),
            ImmutableList.of(),
            /* retargetConfig= */ null);
    assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map")).isTrue();
    assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map$$Lambda$17")).isFalse();
    assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map$$CC")).isFalse();
    assertThat(support.isEmulatedCoreClassOrInterface("java/util/HashMap")).isTrue();
    assertThat(support.isEmulatedCoreClassOrInterface("java/util/concurrent/ConcurrentMap"))
        .isFalse(); // false for renamed prefixes
    assertThat(support.isEmulatedCoreClassOrInterface("com/google/Map")).isFalse();
  }

  @Test
  public void testGetCoreInterfaceRewritingTarget_emulatedDefaultMethod() throws Exception {
    CoreLibrarySupport support =
        new CoreLibrarySupport(
            new CoreLibraryRewriter(""),
            Thread.currentThread().getContextClassLoader(),
            ImmutableList.of(),
            ImmutableList.of("java/util/Collection"),
            ImmutableList.of(),
            /* retargetConfig= */ null);
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEINTERFACE,
                "java/util/Collection",
                "removeIf",
                "(Ljava/util/function/Predicate;)Z",
                true))
        .isEqualTo(Collection.class);
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEVIRTUAL,
                "java/util/ArrayList",
                "removeIf",
                "(Ljava/util/function/Predicate;)Z",
                false))
        .isEqualTo(Collection.class);
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEINTERFACE,
                "com/google/HypotheticalListInterface",
                "removeIf",
                "(Ljava/util/function/Predicate;)Z",
                true))
        .isNull();
  }

  @Test
  public void testGetCoreInterfaceRewritingTarget_emulatedImplementationMoved() throws Exception {
    CoreLibrarySupport support =
        new CoreLibrarySupport(
            new CoreLibraryRewriter(""),
            Thread.currentThread().getContextClassLoader(),
            ImmutableList.of("java/util/Moved"),
            ImmutableList.of("java/util/Map"),
            ImmutableList.of(),
            ClassMemberRetargetConfig.builder()
                .addInProcessReplacement(
                    MethodInvocationReplacement.newBuilder()
                        .setSource(
                            MethodInvocation.newBuilder()
                                .setIsInterface(false)
                                .setOpcode(182)
                                .setMethodId(
                                    MethodId.newBuilder()
                                        .setOwner("java/util/LinkedHashMap")
                                        .setName("forEach")
                                        .setDesc("(Ljava/util/function/BiConsumer;)V")))
                        .setDestination(
                            MethodInvocation.newBuilder()
                                .setIsInterface(false)
                                .setOpcode(182)
                                .setMethodId(
                                    MethodId.newBuilder()
                                        .setOwner("java/util/Moved")
                                        .setName("forEach")
                                        .setDesc("(Ljava/util/function/BiConsumer;)V")))
                        .addRange(ReplacementRange.ALL)
                        .build())
                .addEnabledInvocationReplacementRange(ReplacementRange.ALL)
                .build());

    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEINTERFACE,
                "java/util/Map",
                "forEach",
                "(Ljava/util/function/BiConsumer;)V",
                true))
        .isEqualTo(Map.class);
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKESPECIAL,
                "java/util/Map",
                "forEach",
                "(Ljava/util/function/BiConsumer;)V",
                true))
        .isEqualTo(Map.class);
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEVIRTUAL,
                "java/util/LinkedHashMap",
                "forEach",
                "(Ljava/util/function/BiConsumer;)V",
                false))
        .isEqualTo(Map.class);
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKESPECIAL,
                "java/util/LinkedHashMap",
                "forEach",
                "(Ljava/util/function/BiConsumer;)V",
                false))
        .isEqualTo(LinkedHashMap.class);
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKESPECIAL,
                "java/util/HashMap",
                "forEach",
                "(Ljava/util/function/BiConsumer;)V",
                false))
        .isEqualTo(Map.class);
  }

  @Test
  public void testGetCoreInterfaceRewritingTarget_abstractMethod() throws Exception {
    CoreLibrarySupport support =
        new CoreLibrarySupport(
            new CoreLibraryRewriter(""),
            Thread.currentThread().getContextClassLoader(),
            ImmutableList.of(),
            ImmutableList.of("java/util/Collection"),
            ImmutableList.of(),
            /* retargetConfig= */ null);
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEINTERFACE, "java/util/Collection", "size", "()I", true))
        .isNull();
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEVIRTUAL, "java/util/ArrayList", "size", "()I", false))
        .isNull();
  }

  @Test
  public void testGetCoreInterfaceRewritingTarget_emulatedDefaultOverride() throws Exception {
    CoreLibrarySupport support =
        new CoreLibrarySupport(
            new CoreLibraryRewriter(""),
            Thread.currentThread().getContextClassLoader(),
            ImmutableList.of(),
            ImmutableList.of("java/util/Map"),
            ImmutableList.of(),
            /* retargetConfig= */ null);
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEINTERFACE,
                "java/util/Map",
                "putIfAbsent",
                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
                true))
        .isEqualTo(Map.class);
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEINTERFACE,
                "java/util/concurrent/ConcurrentMap",
                "putIfAbsent",
                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
                true))
        .isNull(); // putIfAbsent is default in Map but abstract in ConcurrentMap
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEINTERFACE,
                "java/util/concurrent/ConcurrentMap",
                "getOrDefault",
                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
                true))
        .isEqualTo(ConcurrentMap.class); // conversely, getOrDefault is overridden as default method
  }

  @Test
  public void testGetCoreInterfaceRewritingTarget_staticInterfaceMethod() throws Exception {
    CoreLibrarySupport support =
        new CoreLibrarySupport(
            new CoreLibraryRewriter(""),
            Thread.currentThread().getContextClassLoader(),
            ImmutableList.of(),
            ImmutableList.of("java/util/Comparator"),
            ImmutableList.of(),
            /* retargetConfig= */ null);
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKESTATIC,
                "java/util/Comparator",
                "reverseOrder",
                "()Ljava/util/Comparator;",
                true))
        .isEqualTo(Comparator.class);
  }

  /**
   * Tests that call sites of renamed core libraries are treated like call sites in regular {@link
   * InterfaceDesugaring}.
   */
  @Test
  public void testGetCoreInterfaceRewritingTarget_renamed() throws Exception {
    CoreLibrarySupport support =
        new CoreLibrarySupport(
            new CoreLibraryRewriter(""),
            Thread.currentThread().getContextClassLoader(),
            ImmutableList.of("java/util/"),
            ImmutableList.of(),
            ImmutableList.of(),
            /* retargetConfig= */ null);

    // regular invocations of default methods: ignored
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEINTERFACE,
                "java/util/Collection",
                "removeIf",
                "(Ljava/util/function/Predicate;)Z",
                true))
        .isNull();
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEVIRTUAL,
                "java/util/ArrayList",
                "removeIf",
                "(Ljava/util/function/Predicate;)Z",
                false))
        .isNull();

    // abstract methods: ignored
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEINTERFACE, "java/util/Collection", "size", "()I", true))
        .isNull();

    // static interface method
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKESTATIC,
                "java/util/Comparator",
                "reverseOrder",
                "()Ljava/util/Comparator;",
                true))
        .isEqualTo(Comparator.class);

    // invokespecial for default methods: find nearest definition
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKESPECIAL,
                "java/util/List",
                "removeIf",
                "(Ljava/util/function/Predicate;)Z",
                true))
        .isEqualTo(Collection.class);
    // invokespecial on a class: ignore (even if there's an inherited default method)
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKESPECIAL,
                "java/util/ArrayList",
                "removeIf",
                "(Ljava/util/function/Predicate;)Z",
                false))
        .isNull();
  }

  @Test
  public void testGetCoreInterfaceRewritingTarget_ignoreRenamedInvokeInterface() throws Exception {
    CoreLibrarySupport support =
        new CoreLibrarySupport(
            new CoreLibraryRewriter(""),
            Thread.currentThread().getContextClassLoader(),
            ImmutableList.of("java/util/concurrent/"), // should return null for these
            ImmutableList.of("java/util/Map"),
            ImmutableList.of(),
            /* retargetConfig= */ null);
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEINTERFACE,
                "java/util/Map",
                "getOrDefault",
                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
                true))
        .isEqualTo(Map.class);
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEINTERFACE,
                "java/util/concurrent/ConcurrentMap",
                "getOrDefault",
                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
                true))
        .isNull();
  }

  @Test
  public void testGetCoreInterfaceRewritingTarget_excludedMethodIgnored() throws Exception {
    CoreLibrarySupport support =
        new CoreLibrarySupport(
            new CoreLibraryRewriter(""),
            Thread.currentThread().getContextClassLoader(),
            ImmutableList.of(),
            ImmutableList.of("java/util/Collection"),
            ImmutableList.of("java/util/Collection#removeIf"),
            /* retargetConfig= */ null);
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEINTERFACE,
                "java/util/List",
                "removeIf",
                "(Ljava/util/function/Predicate;)Z",
                true))
        .isNull();
    assertThat(
            support.getCoreInterfaceRewritingTarget(
                Opcodes.INVOKEVIRTUAL,
                "java/util/ArrayList",
                "removeIf",
                "(Ljava/util/function/Predicate;)Z",
                false))
        .isNull();
  }

  @Test
  public void testEmulatedMethod_nullExceptions() throws Exception {
    CoreLibrarySupport.EmulatedMethod m =
        CoreLibrarySupport.EmulatedMethod.create(1, Number.class, "a", "()V", null);
    assertThat(m.access()).isEqualTo(1);
    assertThat(m.owner()).isEqualTo(Number.class);
    assertThat(m.name()).isEqualTo("a");
    assertThat(m.descriptor()).isEqualTo("()V");
    assertThat(m.exceptions()).isEmpty();
  }

  @Test
  public void testEmulatedMethod_givenExceptions() throws Exception {
    CoreLibrarySupport.EmulatedMethod m =
        CoreLibrarySupport.EmulatedMethod.create(
            1, Number.class, "a", "()V", new String[] {"b", "c"});
    assertThat(m.access()).isEqualTo(1);
    assertThat(m.owner()).isEqualTo(Number.class);
    assertThat(m.name()).isEqualTo("a");
    assertThat(m.descriptor()).isEqualTo("()V");
    assertThat(m.exceptions()).containsExactly("b", "c").inOrder();
  }
}
