/**
 * Copyright (C) 2008 Google Inc.
 *
 * 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.inject.spi;

import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.inject.Asserts.assertContains;
import static com.google.inject.Asserts.assertEqualsBothWays;
import static com.google.inject.Asserts.assertNotSerializable;
import static com.google.inject.name.Names.named;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.inject.ConfigurationException;
import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import com.google.inject.internal.ErrorsException;
import com.google.inject.name.Named;
import com.google.inject.spi.InjectionPoint.Signature;

import junit.framework.TestCase;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

/**
 * @author jessewilson@google.com (Jesse Wilson)
 */
public class InjectionPointTest extends TestCase {

  public @Inject @Named("a") String foo;
  public @Inject void bar(@Named("b") String param) {}

  public static class Constructable {
    @Inject public Constructable(@Named("c") String param) {}
  }

  public void testFieldInjectionPoint() throws NoSuchFieldException, IOException, ErrorsException {
    TypeLiteral<?> typeLiteral = TypeLiteral.get(getClass());
    Field fooField = getClass().getField("foo");

    InjectionPoint injectionPoint = new InjectionPoint(typeLiteral, fooField, false);
    assertSame(fooField, injectionPoint.getMember());
    assertFalse(injectionPoint.isOptional());
    assertEquals(getClass().getName() + ".foo", injectionPoint.toString());
    assertEqualsBothWays(injectionPoint, new InjectionPoint(typeLiteral, fooField, false));
    assertNotSerializable(injectionPoint);

    Dependency<?> dependency = getOnlyElement(injectionPoint.getDependencies());
    assertEquals("Key[type=java.lang.String, annotation=@com.google.inject.name.Named(value=a)]@"
        + getClass().getName() + ".foo", dependency.toString());
    assertEquals(fooField, dependency.getInjectionPoint().getMember());
    assertEquals(-1, dependency.getParameterIndex());
    assertEquals(Key.get(String.class, named("a")), dependency.getKey());
    assertFalse(dependency.isNullable());
    assertNotSerializable(dependency);
    assertEqualsBothWays(dependency,
        getOnlyElement(new InjectionPoint(typeLiteral, fooField, false).getDependencies()));
  }

  public void testMethodInjectionPoint() throws Exception {
    TypeLiteral<?> typeLiteral = TypeLiteral.get(getClass());

    Method barMethod = getClass().getMethod("bar", String.class);
    InjectionPoint injectionPoint = new InjectionPoint(typeLiteral, barMethod, false);
    assertSame(barMethod, injectionPoint.getMember());
    assertFalse(injectionPoint.isOptional());
    assertEquals(getClass().getName() + ".bar()", injectionPoint.toString());
    assertEqualsBothWays(injectionPoint, new InjectionPoint(typeLiteral, barMethod, false));
    assertNotSerializable(injectionPoint);

    Dependency<?> dependency = getOnlyElement(injectionPoint.getDependencies());
    assertEquals("Key[type=java.lang.String, annotation=@com.google.inject.name.Named(value=b)]@"
        + getClass().getName() + ".bar()[0]", dependency.toString());
    assertEquals(barMethod, dependency.getInjectionPoint().getMember());
    assertEquals(0, dependency.getParameterIndex());
    assertEquals(Key.get(String.class, named("b")), dependency.getKey());
    assertFalse(dependency.isNullable());
    assertNotSerializable(dependency);
    assertEqualsBothWays(dependency,
        getOnlyElement(new InjectionPoint(typeLiteral, barMethod, false).getDependencies()));
  }

  public void testConstructorInjectionPoint() throws NoSuchMethodException, IOException,
      ErrorsException {
    TypeLiteral<?> typeLiteral = TypeLiteral.get(Constructable.class);

    Constructor<?> constructor = Constructable.class.getConstructor(String.class);
    InjectionPoint injectionPoint = new InjectionPoint(typeLiteral, constructor);
    assertSame(constructor, injectionPoint.getMember());
    assertFalse(injectionPoint.isOptional());
    assertEquals(Constructable.class.getName() + ".<init>()", injectionPoint.toString());
    assertEqualsBothWays(injectionPoint, new InjectionPoint(typeLiteral, constructor));
    assertNotSerializable(injectionPoint);

    Dependency<?> dependency = getOnlyElement(injectionPoint.getDependencies());
    assertEquals("Key[type=java.lang.String, annotation=@com.google.inject.name.Named(value=c)]@"
        + Constructable.class.getName() + ".<init>()[0]", dependency.toString());
    assertEquals(constructor, dependency.getInjectionPoint().getMember());
    assertEquals(0, dependency.getParameterIndex());
    assertEquals(Key.get(String.class, named("c")), dependency.getKey());
    assertFalse(dependency.isNullable());
    assertNotSerializable(dependency);
    assertEqualsBothWays(dependency,
        getOnlyElement(new InjectionPoint(typeLiteral, constructor).getDependencies()));
  }

  public void testUnattachedDependency() throws IOException {
    Dependency<String> dependency = Dependency.get(Key.get(String.class, named("d")));
    assertEquals("Key[type=java.lang.String, annotation=@com.google.inject.name.Named(value=d)]",
        dependency.toString());
    assertNull(dependency.getInjectionPoint());
    assertEquals(-1, dependency.getParameterIndex());
    assertEquals(Key.get(String.class, named("d")), dependency.getKey());
    assertTrue(dependency.isNullable());
    assertNotSerializable(dependency);
    assertEqualsBothWays(dependency, Dependency.get(Key.get(String.class, named("d"))));
  }

  public void testForConstructor() throws NoSuchMethodException {
    Constructor<HashSet> constructor = HashSet.class.getConstructor();
    TypeLiteral<HashSet<String>> hashSet = new TypeLiteral<HashSet<String>>() {};

    InjectionPoint injectionPoint = InjectionPoint.forConstructor(constructor, hashSet);
    assertSame(constructor, injectionPoint.getMember());
    assertEquals(ImmutableList.<Dependency>of(), injectionPoint.getDependencies());
    assertFalse(injectionPoint.isOptional());

    try {
      InjectionPoint.forConstructor(constructor, new TypeLiteral<LinkedHashSet<String>>() {});
    } catch (ConfigurationException expected) {
      assertContains(expected.getMessage(), "java.util.LinkedHashSet<java.lang.String>",
          " does not define java.util.HashSet.<init>()",
          "  while locating java.util.LinkedHashSet<java.lang.String>");
    }

    try {
      InjectionPoint.forConstructor((Constructor) constructor, new TypeLiteral<Set<String>>() {});
    } catch (ConfigurationException expected) {
      assertContains(expected.getMessage(), "java.util.Set<java.lang.String>",
          " does not define java.util.HashSet.<init>()",
          "  while locating java.util.Set<java.lang.String>");
    }
  }

  public void testForConstructorOf() {
    InjectionPoint injectionPoint = InjectionPoint.forConstructorOf(Constructable.class);
    assertEquals(Constructable.class.getName() + ".<init>()", injectionPoint.toString());
  }

  public void testAddForInstanceMethodsAndFields() throws Exception {
    Method instanceMethod = HasInjections.class.getMethod("instanceMethod", String.class);
    Field instanceField = HasInjections.class.getField("instanceField");

    TypeLiteral<HasInjections> type = TypeLiteral.get(HasInjections.class);
    assertEquals(ImmutableSet.of(
        new InjectionPoint(type, instanceMethod, false),
        new InjectionPoint(type, instanceField, false)),
        InjectionPoint.forInstanceMethodsAndFields(HasInjections.class));
  }

  public void testAddForStaticMethodsAndFields() throws Exception {
    Method staticMethod = HasInjections.class.getMethod("staticMethod", String.class);
    Field staticField = HasInjections.class.getField("staticField");

    Set<InjectionPoint> injectionPoints = InjectionPoint.forStaticMethodsAndFields(
        HasInjections.class);
    assertEquals(ImmutableSet.of(
        new InjectionPoint(TypeLiteral.get(HasInjections.class), staticMethod, false),
        new InjectionPoint(TypeLiteral.get(HasInjections.class), staticField, false)),
        injectionPoints);
  }

  static class HasInjections {
    @Inject public static void staticMethod(@Named("a") String a) {}
    @Inject @Named("c") public static String staticField;
    @Inject public void instanceMethod(@Named("d") String d) {}
    @Inject @Named("f") public String instanceField;
  }

  public void testAddForParameterizedInjections() {
    TypeLiteral<?> type = new TypeLiteral<ParameterizedInjections<String>>() {};

    InjectionPoint constructor = InjectionPoint.forConstructorOf(type);
    assertEquals(new Key<Map<String, String>>() {},
        getOnlyElement(constructor.getDependencies()).getKey());

    InjectionPoint field = getOnlyElement(InjectionPoint.forInstanceMethodsAndFields(type));
    assertEquals(new Key<Set<String>>() {}, getOnlyElement(field.getDependencies()).getKey());
  }

  static class ParameterizedInjections<T> {
    @Inject Set<T> setOfTees;
    @Inject public ParameterizedInjections(Map<T, T> map) {}
  }

  public void testSignature() throws Exception {
    Signature fooA = new Signature(Foo.class.getDeclaredMethod(
        "a", String.class, int.class));
    Signature fooB = new Signature(Foo.class.getDeclaredMethod("b"));
    Signature barA = new Signature(Bar.class.getDeclaredMethod(
        "a", String.class, int.class));
    Signature barB = new Signature(Bar.class.getDeclaredMethod("b"));

    assertEquals(fooA.hashCode(), barA.hashCode());
    assertEquals(fooB.hashCode(), barB.hashCode());
    assertEquals(fooA, barA);
    assertEquals(fooB, barB);
  }

  static class Foo {
    void a(String s, int i) {}
    int b() {
      return 0;
    }
  }
  static class Bar {
    public void a(String s, int i) {}
    void b() {}
  }
  
  public void testOverrideBehavior() {
    Set<InjectionPoint> points;

    points = InjectionPoint.forInstanceMethodsAndFields(Super.class);
    assertEquals(points.toString(), 6, points.size());
    assertPoints(points, Super.class, "atInject", "gInject", "privateAtAndPublicG",
        "privateGAndPublicAt", "atFirstThenG", "gFirstThenAt");

    points = InjectionPoint.forInstanceMethodsAndFields(Sub.class);
    assertEquals(points.toString(), 7, points.size());
    // Superclass will always have is private members injected,
    // and 'gInject' was last @Injected in Super, so that remains the owner
    assertPoints(points, Super.class, "privateAtAndPublicG", "privateGAndPublicAt", "gInject");
    // Subclass also has the "private" methods, but they do not override
    // the superclass' methods, and it now owns the inject2 methods.
    assertPoints(points, Sub.class, "privateAtAndPublicG", "privateGAndPublicAt",
        "atFirstThenG", "gFirstThenAt");
    
    points = InjectionPoint.forInstanceMethodsAndFields(SubSub.class);
    assertEquals(points.toString(), 6, points.size());
    // Superclass still has all the injection points it did before..
    assertPoints(points, Super.class, "privateAtAndPublicG", "privateGAndPublicAt", "gInject");
    // Subclass is missing the privateGAndPublicAt because it first became public with
    // javax.inject.Inject and was overrode without an annotation, which means it
    // disappears.  (It was guice @Inject in Super, but it was private there, so it doesn't
    // effect the annotations of the subclasses.)
    assertPoints(points, Sub.class, "privateAtAndPublicG", "atFirstThenG", "gFirstThenAt");    
  }
  
  /**
   * This test serves two purposes:
   *   1) It makes sure that the bridge methods javax generates don't stop
   *   us from injecting superclass methods in the case of javax.inject.Inject.
   *   This would happen prior to java8 (where javac didn't copy annotations
   *   from the superclass into the subclass method when it generated the
   *   bridge methods).
   *   
   *   2) It makes sure that the methods we're going to inject have the correct
   *   generic types.  Java8 copies the annotations from super to subclasses,
   *   but it doesn't copy the generic type information.  Guice would naively
   *   consider the subclass an injectable method and eject the superclass
   *   from the 'overrideIndex', leaving only a class with improper generic types.
   */
  public void testSyntheticBridgeMethodsInSubclasses() {
    Set<InjectionPoint> points;
    
    points = InjectionPoint.forInstanceMethodsAndFields(RestrictedSuper.class);
    assertPointDependencies(points, new TypeLiteral<Provider<String>>() {});
    assertEquals(points.toString(), 2, points.size());
    assertPoints(points, RestrictedSuper.class, "jInject", "gInject");
    
    points = InjectionPoint.forInstanceMethodsAndFields(ExposedSub.class);
    assertPointDependencies(points, new TypeLiteral<Provider<String>>() {});
    assertEquals(points.toString(), 2, points.size());
    assertPoints(points, RestrictedSuper.class, "jInject", "gInject");
  }
  
  private void assertPoints(Iterable<InjectionPoint> points, Class<?> clazz,
      String... methodNames) {
    Set<String> methods = new HashSet<String>();
    for (InjectionPoint point : points) {
      if (point.getDeclaringType().getRawType() == clazz) {
        methods.add(point.getMember().getName());
      }
    }
    assertEquals(points.toString(), ImmutableSet.copyOf(methodNames), methods);
  }
  
  /** Asserts that each injection point has the specified dependencies, in the given order. */
  private void assertPointDependencies(Iterable<InjectionPoint> points,
      TypeLiteral<?>... literals) {
    for (InjectionPoint point : points) {
      assertEquals(literals.length, point.getDependencies().size());
      for (Dependency<?> dep : point.getDependencies()) {
        assertEquals(literals[dep.getParameterIndex()], dep.getKey().getTypeLiteral());
      }
    }
  }
  
  static class Super {
    @javax.inject.Inject public void atInject() {}
    @com.google.inject.Inject public void gInject() {}

    @javax.inject.Inject private void privateAtAndPublicG() {}
    @com.google.inject.Inject private void privateGAndPublicAt() {}

    @javax.inject.Inject public void atFirstThenG() {}
    @com.google.inject.Inject public void gFirstThenAt() {}
  }
  
  static class Sub extends Super {
    public void atInject() {}
    public void gInject() {}
    
    @com.google.inject.Inject public void privateAtAndPublicG() {}
    @javax.inject.Inject public void privateGAndPublicAt() {}
    
    @com.google.inject.Inject
    @Override 
    public void atFirstThenG() {}
    
    @javax.inject.Inject
    @Override 
    public void gFirstThenAt() {}
  }
  
  static class SubSub extends Sub {
    @Override public void privateAtAndPublicG() {}
    @Override public void privateGAndPublicAt() {}
    
    @Override public void atFirstThenG() {}
    @Override public void gFirstThenAt() {}
  }
  
  static class RestrictedSuper {
    @com.google.inject.Inject public void gInject(Provider<String> p) {}
    @javax.inject.Inject public void jInject(Provider<String> p) {}
  }
  
  public static class ExposedSub extends RestrictedSuper {
    // The subclass may generate bridge/synthetic methods to increase the visibility
    // of the superclass methods, since the superclass was package-private but this is public.
  }
}
