/*
 * Copyright (C) 2014 The Dagger Authors.
 *
 * 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 static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static dagger.internal.codegen.xprocessing.XElements.getSimpleName;
import static dagger.internal.codegen.xprocessing.XTypes.isPrimitive;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import androidx.room.compiler.processing.XConstructorElement;
import androidx.room.compiler.processing.XMethodElement;
import androidx.room.compiler.processing.XProcessingEnv;
import androidx.room.compiler.processing.XType;
import androidx.room.compiler.processing.XTypeElement;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.testing.compile.CompilationRule;
import dagger.Component;
import dagger.Module;
import dagger.Provides;
import dagger.internal.codegen.binding.KeyFactory;
import dagger.internal.codegen.javac.JavacPluginModule;
import dagger.internal.codegen.model.Key;
import dagger.multibindings.ElementsIntoSet;
import dagger.multibindings.IntoSet;
import dagger.producers.ProducerModule;
import dagger.producers.Produces;
import java.lang.annotation.Retention;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Qualifier;
import javax.inject.Singleton;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

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

  @Inject XProcessingEnv processingEnv;
  @Inject KeyFactory keyFactory;

  @Before public void setUp() {
    DaggerKeyFactoryTest_TestComponent.builder()
        .javacPluginModule(
            new JavacPluginModule(compilationRule.getElements(), compilationRule.getTypes()))
        .build()
        .inject(this);
  }

  @Test
  public void forInjectConstructorWithResolvedType() {
    XTypeElement typeElement =
        processingEnv.requireTypeElement(InjectedClass.class.getCanonicalName());
    XConstructorElement constructor = getOnlyElement(typeElement.getConstructors());
    Key key =
        keyFactory.forInjectConstructorWithResolvedType(
            constructor.getEnclosingElement().getType());
    assertThat(key.toString()).isEqualTo("dagger.internal.codegen.KeyFactoryTest.InjectedClass");
  }

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

  @Test
  public void forProvidesMethod() {
    XTypeElement moduleElement =
        processingEnv.requireTypeElement(ProvidesMethodModule.class.getCanonicalName());
    XMethodElement providesMethod = getOnlyElement(moduleElement.getDeclaredMethods());
    Key key = keyFactory.forProvidesMethod(providesMethod, moduleElement);
    assertThat(key.toString()).isEqualTo("java.lang.String");
  }

  @Module
  static final class ProvidesMethodModule {
    @Provides String provideString() {
      throw new UnsupportedOperationException();
    }
  }

  @Test
  public void forProvidesMethod_qualified() {
    XType stringType = processingEnv.requireType(String.class.getCanonicalName());
    XTypeElement qualifierElement =
        processingEnv.requireTypeElement(TestQualifier.class.getCanonicalName());
    XTypeElement moduleElement =
        processingEnv.requireTypeElement(QualifiedProvidesMethodModule.class.getCanonicalName());
    XMethodElement providesMethod = getOnlyElement(moduleElement.getDeclaredMethods());
    Key key = keyFactory.forProvidesMethod(providesMethod, moduleElement);
    assertThat(key.qualifier().get().className()).isEqualTo(qualifierElement.getClassName());
    assertThat(key.type().xprocessing().getTypeName()).isEqualTo(stringType.getTypeName());
    assertThat(key.toString())
        .isEqualTo(
            "@dagger.internal.codegen.KeyFactoryTest.TestQualifier({"
                + "@dagger.internal.codegen.KeyFactoryTest.InnerAnnotation("
                + "param1=1, value=\"value a\"), "
                + "@dagger.internal.codegen.KeyFactoryTest.InnerAnnotation("
                + "param1=2, value=\"value b\"), "
                + "@dagger.internal.codegen.KeyFactoryTest.InnerAnnotation("
                + "param1=3145, value=\"default\")"
                + "}) java.lang.String");
  }

  @Test
  public void qualifiedKeyEquivalents() {
    XTypeElement moduleElement =
        processingEnv.requireTypeElement(QualifiedProvidesMethodModule.class.getCanonicalName());
    XMethodElement providesMethod = getOnlyElement(moduleElement.getDeclaredMethods());
    Key provisionKey = keyFactory.forProvidesMethod(providesMethod, moduleElement);
    assertThat(provisionKey.toString())
        .isEqualTo(
            "@dagger.internal.codegen.KeyFactoryTest.TestQualifier({"
                + "@dagger.internal.codegen.KeyFactoryTest.InnerAnnotation("
                + "param1=1, value=\"value a\"), "
                + "@dagger.internal.codegen.KeyFactoryTest.InnerAnnotation("
                + "param1=2, value=\"value b\"), "
                + "@dagger.internal.codegen.KeyFactoryTest.InnerAnnotation("
                + "param1=3145, value=\"default\")"
                + "}) java.lang.String");
  }

  @Module
  static final class QualifiedProvidesMethodModule {
    @Provides
    @TestQualifier({
      @InnerAnnotation(value = "value a", param1 = 1),
      // please note the order of 'param' and 'value' is inverse
      @InnerAnnotation(param1 = 2, value = "value b"),
      @InnerAnnotation()
    })
    static String provideQualifiedString() {
      throw new UnsupportedOperationException();
    }
  }

  static final class QualifiedFieldHolder {
    @TestQualifier({
      @InnerAnnotation(value = "value a", param1 = 1),
      // please note the order of 'param' and 'value' is inverse
      @InnerAnnotation(param1 = 2, value = "value b"),
      @InnerAnnotation()
    })
    String aString;
  }

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

  @interface InnerAnnotation {
    int param1() default 3145;

    String value() default "default";
  }

  @Test
  public void forProvidesMethod_sets() {
    XTypeElement moduleElement =
        processingEnv.requireTypeElement(SetProvidesMethodsModule.class.getCanonicalName());
    for (XMethodElement providesMethod : moduleElement.getDeclaredMethods()) {
      Key key = keyFactory.forProvidesMethod(providesMethod, moduleElement);
      assertThat(key.toString())
          .isEqualTo(
              String.format(
                  "java.util.Set<java.lang.String> "
                      + "dagger.internal.codegen.KeyFactoryTest.SetProvidesMethodsModule#%s",
                  getSimpleName(providesMethod)));
    }
  }

  @Module
  static final class SetProvidesMethodsModule {
    @Provides @IntoSet String provideString() {
      throw new UnsupportedOperationException();
    }

    @Provides @ElementsIntoSet Set<String> provideStrings() {
      throw new UnsupportedOperationException();
    }
  }

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

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

  @Test public void primitiveKeysMatchBoxedKeys() {
    XTypeElement primitiveHolder =
        processingEnv.requireTypeElement(PrimitiveTypes.class.getCanonicalName());
    XMethodElement intMethod = getOnlyElement(primitiveHolder.getDeclaredMethods());
    XTypeElement boxedPrimitiveHolder =
        processingEnv.requireTypeElement(BoxedPrimitiveTypes.class.getCanonicalName());
    XMethodElement integerMethod = getOnlyElement(boxedPrimitiveHolder.getDeclaredMethods());

    // TODO(cgruber): Truth subject for TypeMirror and TypeElement
    XType intType = intMethod.getReturnType();
    assertThat(isPrimitive(intType)).isTrue();
    XType integerType = integerMethod.getReturnType();
    assertThat(isPrimitive(integerType)).isFalse();
    assertWithMessage("type equality").that(intType.isSameType(integerType)).isFalse();
    Key intKey = keyFactory.forProvidesMethod(intMethod, primitiveHolder);
    Key integerKey = keyFactory.forProvidesMethod(integerMethod, boxedPrimitiveHolder);
    assertThat(intKey).isEqualTo(integerKey);
    assertThat(intKey.toString()).isEqualTo("java.lang.Integer");
    assertThat(integerKey.toString()).isEqualTo("java.lang.Integer");
  }

  @Test public void forProducesMethod() {
    XTypeElement moduleElement =
        processingEnv.requireTypeElement(ProducesMethodsModule.class.getCanonicalName());
    for (XMethodElement producesMethod : moduleElement.getDeclaredMethods()) {
      Key key = keyFactory.forProducesMethod(producesMethod, moduleElement);
      assertThat(key.toString()).isEqualTo("java.lang.String");
    }
  }

  @ProducerModule
  static final class ProducesMethodsModule {
    @Produces String produceString() {
      throw new UnsupportedOperationException();
    }

    @Produces ListenableFuture<String> produceFutureString() {
      throw new UnsupportedOperationException();
    }
  }

  @Test public void forProducesMethod_sets() {
    XTypeElement moduleElement =
        processingEnv.requireTypeElement(SetProducesMethodsModule.class.getCanonicalName());
    for (XMethodElement producesMethod : moduleElement.getDeclaredMethods()) {
      Key key = keyFactory.forProducesMethod(producesMethod, moduleElement);
      assertThat(key.toString())
          .isEqualTo(
              String.format(
                  "java.util.Set<java.lang.String> "
                      + "dagger.internal.codegen.KeyFactoryTest.SetProducesMethodsModule#%s",
                  getSimpleName(producesMethod)));
    }
  }

  @ProducerModule
  static final class SetProducesMethodsModule {
    @Produces @IntoSet String produceString() {
      throw new UnsupportedOperationException();
    }

    @Produces @IntoSet ListenableFuture<String> produceFutureString() {
      throw new UnsupportedOperationException();
    }

    @Produces @ElementsIntoSet Set<String> produceStrings() {
      throw new UnsupportedOperationException();
    }

    @Produces @ElementsIntoSet
    ListenableFuture<Set<String>> produceFutureStrings() {
      throw new UnsupportedOperationException();
    }
  }

  @Singleton
  @Component(modules = JavacPluginModule.class)
  interface TestComponent {
    void inject(KeyFactoryTest test);
  }
}
