/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * 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.facebook.buck.android.dalvik.firstorder;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.stream.StreamSupport;
import org.junit.Test;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;

public class FirstOrderTest {

  private static final ImmutableList<Class<?>> KNOWN_TYPES =
      ImmutableList.of(
          DependencyEnum.class,
          AnnotationDependency.class,
          DependencyInterface.class,
          DependencyBase.class,
          Dependency.class,
          DerivedInterface.class,
          Derived.class);

  private static final ImmutableList<ClassNode> KNOWN_CLASS_NODES =
      KNOWN_TYPES.stream()
          .map(FirstOrderTest::loadClassNode)
          .collect(ImmutableList.toImmutableList());

  enum DependencyEnum {
    DEFAULT
  }

  @Retention(RetentionPolicy.RUNTIME)
  @interface AnnotationDependency {
    DependencyEnum dependencyEnum() default DependencyEnum.DEFAULT;
  }

  interface DependencyInterface {}

  static class DependencyBase {}

  static class Dependency extends DependencyBase implements DependencyInterface {}

  interface DerivedInterface extends DependencyInterface {}

  static class Derived implements DerivedInterface {}

  @Test
  public void testHasBase() {
    class TestClass extends Dependency {}

    DependencyCheck.checkThat(TestClass.class)
        .doesNotDependOn(DependencyEnum.class)
        .doesNotDependOn(AnnotationDependency.class)
        .dependsOn(DependencyInterface.class)
        .dependsOn(DependencyBase.class)
        .dependsOn(Dependency.class)
        .dependsOn(TestClass.class);
  }

  @Test
  public void testHasField() {
    class TestClass {
      @SuppressWarnings("unused")
      Dependency a;
    }

    DependencyCheck.checkThat(TestClass.class)
        .doesNotDependOn(DependencyEnum.class)
        .doesNotDependOn(AnnotationDependency.class)
        .dependsOn(DependencyInterface.class)
        .dependsOn(DependencyBase.class)
        .dependsOn(Dependency.class)
        .dependsOn(TestClass.class);
  }

  @Test
  public void testHasParameter() {
    class TestClass {
      @SuppressWarnings("unused")
      void doSomething(Dependency dependency) {}
    }

    DependencyCheck.checkThat(TestClass.class)
        .doesNotDependOn(DependencyEnum.class)
        .doesNotDependOn(AnnotationDependency.class)
        .dependsOn(DependencyInterface.class)
        .dependsOn(DependencyBase.class)
        .dependsOn(Dependency.class)
        .dependsOn(TestClass.class);
  }

  @Test
  public void testHasReturnType() {
    class TestClass {
      @SuppressWarnings("unused")
      Dependency doSomething() {
        return null;
      }
    }

    DependencyCheck.checkThat(TestClass.class)
        .doesNotDependOn(DependencyEnum.class)
        .doesNotDependOn(AnnotationDependency.class)
        .dependsOn(DependencyInterface.class)
        .dependsOn(DependencyBase.class)
        .dependsOn(Dependency.class)
        .dependsOn(TestClass.class);
  }

  @Test
  public void testHasAnnotation() {
    class TestClass {
      @AnnotationDependency() int x;
    }

    DependencyCheck.checkThat(TestClass.class)
        .doesNotDependOn(DependencyEnum.class)
        .doesNotDependOn(AnnotationDependency.class)
        .doesNotDependOn(DependencyInterface.class)
        .doesNotDependOn(DependencyBase.class)
        .doesNotDependOn(Dependency.class)
        .dependsOn(TestClass.class);
  }

  @Test
  public void testHasAnnotationWithEnumProperty() {
    class TestClass {
      @AnnotationDependency(dependencyEnum = DependencyEnum.DEFAULT)
      int x;
    }

    DependencyCheck.checkThat(TestClass.class)
        .dependsOn(DependencyEnum.class)
        .doesNotDependOn(AnnotationDependency.class)
        .doesNotDependOn(DependencyInterface.class)
        .doesNotDependOn(DependencyBase.class)
        .doesNotDependOn(Dependency.class)
        .dependsOn(TestClass.class);
  }

  @Test
  public void testInterfaceInheritance() {
    DependencyCheck.checkThat(Derived.class)
        .dependsOn(Derived.class)
        .dependsOn(DerivedInterface.class)
        .dependsOn(DependencyInterface.class);
  }

  // Interestingly, observedDependencies includes DependencyInterface in
  // testInterfaceInheritance, but not in this test.  So this verifies more
  // completely that we are including the transitive closure of implemented
  // interfaces.
  @Test
  public void testInterfaceInheritanceWithField() {
    class TestClass {
      @SuppressWarnings("unused")
      Derived x;
    }
    DependencyCheck.checkThat(TestClass.class)
        .dependsOn(TestClass.class)
        .dependsOn(Derived.class)
        .dependsOn(DerivedInterface.class)
        .dependsOn(DependencyInterface.class);
  }

  private static ImmutableSet<String> getFirstOrderDependencies(
      Iterable<? extends Class<?>> types) {
    ImmutableSet.Builder<String> builder = ImmutableSet.builder();

    FirstOrderHelper.addTypesAndDependencies(
        FluentIterable.from(types).transform(Type::getType),
        loadAndMergeClasses(types, KNOWN_CLASS_NODES),
        builder);

    return builder.build();
  }

  private static ImmutableList<ClassNode> loadAndMergeClasses(
      Iterable<? extends Class<?>> classes, Iterable<ClassNode> alreadyLoaded) {
    ImmutableList.Builder<ClassNode> builder = ImmutableList.builder();

    builder.addAll(alreadyLoaded);
    builder.addAll(
        StreamSupport.stream(classes.spliterator(), false)
            .map(FirstOrderTest::loadClassNode)
            .iterator());

    return builder.build();
  }

  private static ClassNode loadClassNode(Class<?> input) {
    try {
      ClassReader reader = new ClassReader(input.getName());
      ClassNode node = new ClassNode(Opcodes.ASM7);
      reader.accept(node, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
      return node;
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  private static class DependencyCheck {

    private final FluentIterable<String> typeNames;

    DependencyCheck(Iterable<String> typeNames) {
      this.typeNames = FluentIterable.from(typeNames);
    }

    static DependencyCheck checkThat(Class<?>... types) {
      return checkThat(ImmutableList.copyOf(types));
    }

    static DependencyCheck checkThat(Iterable<? extends Class<?>> types) {
      return new DependencyCheck(getFirstOrderDependencies(types));
    }

    DependencyCheck dependsOn(Class<?> type) {
      String name = Type.getType(type).getInternalName();
      assertTrue(
          "Expected name '" + name + "' not found in list: " + typeNames, typeNames.contains(name));
      return this;
    }

    DependencyCheck doesNotDependOn(Class<?> type) {
      String name = Type.getType(type).getInternalName();
      assertFalse(
          "Unexpected name '" + name + "' found in list: " + typeNames, typeNames.contains(name));
      return this;
    }
  }
}
