/*
 * Copyright (C) 2014 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 dagger.internal.codegen;

import com.google.auto.common.MoreTypes;
import com.google.common.base.Equivalence;
import com.google.common.base.Optional;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.testing.compile.CompilationRule;
import dagger.Module;
import dagger.Provides;
import dagger.producers.ProducerModule;
import dagger.producers.Produces;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Qualifier;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import static com.google.common.truth.Truth.assertThat;
import static dagger.Provides.Type.SET;
import static dagger.Provides.Type.SET_VALUES;

/**
 * Tests {@link Key}.
 */
@RunWith(JUnit4.class)
public class KeyTest {
  @Rule public CompilationRule compilationRule = new CompilationRule();

  private Elements elements;
  private Types types;
  private Key.Factory keyFactory;

  @Before public void setUp() {
    this.types = compilationRule.getTypes();
    this.elements = compilationRule.getElements();
    this.keyFactory = new Key.Factory(types, elements);
  }

  @Test public void forInjectConstructorWithResolvedType() {
    TypeElement typeElement =
        compilationRule.getElements().getTypeElement(InjectedClass.class.getCanonicalName());
    ExecutableElement constructor =
        Iterables.getOnlyElement(ElementFilter.constructorsIn(typeElement.getEnclosedElements()));
    assertThat(
        keyFactory.forInjectConstructorWithResolvedType(constructor.getEnclosingElement().asType()))
        .isEqualTo(new AutoValue_Key(
            Optional.<Equivalence.Wrapper<AnnotationMirror>>absent(),
            MoreTypes.equivalence().wrap(typeElement.asType())));
  }

  static final class InjectedClass {
    @SuppressWarnings("unused")
    @Inject InjectedClass(String s, int i) {}
  }

  @Test public void forProvidesMethod() {
    TypeMirror stringType = elements.getTypeElement(String.class.getCanonicalName()).asType();
    TypeElement moduleElement =
        elements.getTypeElement(ProvidesMethodModule.class.getCanonicalName());
    ExecutableElement providesMethod =
        Iterables.getOnlyElement(ElementFilter.methodsIn(moduleElement.getEnclosedElements()));
    assertThat(
        keyFactory.forProvidesMethod((ExecutableType) providesMethod.asType(), providesMethod))
        .isEqualTo(new AutoValue_Key(
            Optional.<Equivalence.Wrapper<AnnotationMirror>>absent(),
            MoreTypes.equivalence().wrap(stringType)));
  }

  @Module
  static final class ProvidesMethodModule {
    @Provides String provideString() {
      return null;
    }
  }

  @Test public void forProvidesMethod_qualified() {
    TypeMirror stringType = elements.getTypeElement(String.class.getCanonicalName()).asType();
    TypeElement qualifierElement =
        elements.getTypeElement(TestQualifier.class.getCanonicalName());
    TypeElement moduleElement =
        elements.getTypeElement(QualifiedProvidesMethodModule.class.getCanonicalName());
    ExecutableElement providesMethod =
        Iterables.getOnlyElement(ElementFilter.methodsIn(moduleElement.getEnclosedElements()));
    Key key =
        keyFactory.forProvidesMethod((ExecutableType) providesMethod.asType(), providesMethod);
    assertThat(MoreTypes.equivalence().wrap(key.qualifier().get().getAnnotationType()))
        .isEqualTo(MoreTypes.equivalence().wrap(qualifierElement.asType()));
    assertThat(key.wrappedType()).isEqualTo(MoreTypes.equivalence().wrap(stringType));
  }

  @Test public void qualifiedKeyEquivalents() {
    TypeElement moduleElement =
        elements.getTypeElement(QualifiedProvidesMethodModule.class.getCanonicalName());
    ExecutableElement providesMethod =
        Iterables.getOnlyElement(ElementFilter.methodsIn(moduleElement.getEnclosedElements()));
    Key provisionKey =
        keyFactory.forProvidesMethod((ExecutableType) providesMethod.asType(), providesMethod);

    TypeMirror type = elements.getTypeElement(String.class.getCanonicalName()).asType();
    TypeElement injectableElement =
        elements.getTypeElement(QualifiedFieldHolder.class.getCanonicalName());
    Element injectionField =
        Iterables.getOnlyElement(ElementFilter.fieldsIn(injectableElement.getEnclosedElements()));
    AnnotationMirror qualifier = Iterables.getOnlyElement(injectionField.getAnnotationMirrors());
    Key injectionKey = keyFactory.forQualifiedType(Optional.<AnnotationMirror>of(qualifier), type);

    assertThat(provisionKey).isEqualTo(injectionKey);
  }

  @Module
  static final class QualifiedProvidesMethodModule {
    @Provides
    @TestQualifier(@InnerAnnotation)
    String provideQualifiedString() {
      return null;
    }
  }

  static final class QualifiedFieldHolder {
    @TestQualifier(@InnerAnnotation) String aString;
  }

  @Qualifier
  @interface TestQualifier {
    InnerAnnotation[] value();
  }

  @interface InnerAnnotation {}

  @Test public void forProvidesMethod_sets() {
    TypeElement setElement = elements.getTypeElement(Set.class.getCanonicalName());
    TypeMirror stringType = elements.getTypeElement(String.class.getCanonicalName()).asType();
    TypeMirror setOfStringsType = types.getDeclaredType(setElement, stringType);
    TypeElement moduleElement =
        elements.getTypeElement(SetProvidesMethodsModule.class.getCanonicalName());
    for (ExecutableElement providesMethod
        : ElementFilter.methodsIn(moduleElement.getEnclosedElements())) {
      assertThat(
          keyFactory.forProvidesMethod((ExecutableType) providesMethod.asType(), providesMethod))
              .isEqualTo(new AutoValue_Key(
                  Optional.<Equivalence.Wrapper<AnnotationMirror>>absent(),
                  MoreTypes.equivalence().wrap(setOfStringsType)));
    }
  }

  @Module
  static final class SetProvidesMethodsModule {
    @Provides(type = SET) String provideString() {
      return null;
    }

    @Provides(type = SET_VALUES) Set<String> provideStrings() {
      return null;
    }
  }

  @Module
  static final class PrimitiveTypes {
    @Provides int foo() {
      return 0;
    }
  }

  @Module
  static final class BoxedPrimitiveTypes {
    @Provides Integer foo() {
      return 0;
    }
  }

  @Test public void primitiveKeysMatchBoxedKeys() {
    TypeElement primitiveHolder = elements.getTypeElement(PrimitiveTypes.class.getCanonicalName());
    ExecutableElement intMethod =
        Iterables.getOnlyElement(ElementFilter.methodsIn(primitiveHolder.getEnclosedElements()));
    TypeElement boxedPrimitiveHolder =
        elements.getTypeElement(BoxedPrimitiveTypes.class.getCanonicalName());
    ExecutableElement integerMethod = Iterables.getOnlyElement(
        ElementFilter.methodsIn(boxedPrimitiveHolder.getEnclosedElements()));

    // TODO(cgruber): Truth subject for TypeMirror and TypeElement
    TypeMirror intType = intMethod.getReturnType();
    assertThat(intType.getKind().isPrimitive()).isTrue();
    TypeMirror integerType = integerMethod.getReturnType();
    assertThat(integerType.getKind().isPrimitive()).isFalse();
    assertThat(types.isSameType(intType, integerType)).named("type equality").isFalse();

    Key intKey = keyFactory.forProvidesMethod((ExecutableType) intMethod.asType(), intMethod);
    Key integerKey =
        keyFactory.forProvidesMethod((ExecutableType) integerMethod.asType(), integerMethod);
    assertThat(intKey).isEqualTo(integerKey);
  }

  @Test public void forProducesMethod() {
    TypeMirror stringType = elements.getTypeElement(String.class.getCanonicalName()).asType();
    TypeElement moduleElement =
        elements.getTypeElement(ProducesMethodsModule.class.getCanonicalName());
    for (ExecutableElement producesMethod
        : ElementFilter.methodsIn(moduleElement.getEnclosedElements())) {
      assertThat(keyFactory.forProducesMethod(
          (ExecutableType) producesMethod.asType(), producesMethod))
              .isEqualTo(new AutoValue_Key(
                  Optional.<Equivalence.Wrapper<AnnotationMirror>>absent(),
                  MoreTypes.equivalence().wrap(stringType)));
    }
  }

  @ProducerModule
  static final class ProducesMethodsModule {
    @Produces String produceString() {
      return null;
    }

    @Produces ListenableFuture<String> produceFutureString() {
      return null;
    }
  }

  @Test public void forProducesMethod_sets() {
    TypeElement setElement = elements.getTypeElement(Set.class.getCanonicalName());
    TypeMirror stringType = elements.getTypeElement(String.class.getCanonicalName()).asType();
    TypeMirror setOfStringsType = types.getDeclaredType(setElement, stringType);
    TypeElement moduleElement =
        elements.getTypeElement(SetProducesMethodsModule.class.getCanonicalName());
    for (ExecutableElement producesMethod
        : ElementFilter.methodsIn(moduleElement.getEnclosedElements())) {
      assertThat(keyFactory.forProducesMethod(
          (ExecutableType) producesMethod.asType(), producesMethod))
          .isEqualTo(new AutoValue_Key(
                  Optional.<Equivalence.Wrapper<AnnotationMirror>>absent(),
                  MoreTypes.equivalence().wrap(setOfStringsType)));
    }
  }

  @ProducerModule
  static final class SetProducesMethodsModule {
    @Produces(type = Produces.Type.SET) String produceString() {
      return null;
    }

    @Produces(type = Produces.Type.SET) ListenableFuture<String> produceFutureString() {
      return null;
    }

    @Produces(type = Produces.Type.SET_VALUES) Set<String> produceStrings() {
      return null;
    }

    @Produces(type = Produces.Type.SET_VALUES)
    ListenableFuture<Set<String>> produceFutureStrings() {
      return null;
    }
  }
}