/*
 * Copyright (C) 2016 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 dagger.internal.codegen.DaggerModuleMethodSubject.Factory.assertThatModuleMethod;

import androidx.room.compiler.processing.XProcessingEnv;
import androidx.room.compiler.processing.util.Source;
import dagger.Module;
import dagger.producers.ProducerModule;
import dagger.testing.compile.CompilerTests;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

@RunWith(Parameterized.class)
public final class ModuleValidationTest {

  @Parameterized.Parameters(name = "{0}")
  public static Collection<Object[]> parameters() {
    return Arrays.asList(new Object[][] {{ModuleType.MODULE}, {ModuleType.PRODUCER_MODULE}});
  }

  private enum ModuleType {
    MODULE(Module.class),
    PRODUCER_MODULE(ProducerModule.class),
    ;

    private final Class<? extends Annotation> annotation;

    ModuleType(Class<? extends Annotation> annotation) {
      this.annotation = annotation;
    }

    String annotationWithSubcomponent(String subcomponent) {
      return String.format("@%s(subcomponents = %s)", annotation.getSimpleName(), subcomponent);
    }

    String importStatement() {
      return String.format("import %s;", annotation.getName());
    }

    String simpleName() {
      return annotation.getSimpleName();
    }
  }

  private final ModuleType moduleType;

  public ModuleValidationTest(ModuleType moduleType) {
    this.moduleType = moduleType;
  }

  @Test
  public void moduleSubcomponents_notASubcomponent() {
    Source module =
        CompilerTests.javaSource(
            "test.TestModule",
            "package test;",
            "",
            moduleType.importStatement(),
            "",
            moduleType.annotationWithSubcomponent("NotASubcomponent.class"),
            "class TestModule {}");
    Source notASubcomponent =
        CompilerTests.javaSource(
            "test.NotASubcomponent",
            "package test;",
            "",
            "class NotASubcomponent {}");
    CompilerTests.daggerCompiler(module, notASubcomponent)
        .compile(
            subject -> {
              subject.hasErrorCount(1);
              subject.hasErrorContaining(
                      "test.NotASubcomponent is not a @Subcomponent or @ProductionSubcomponent")
                  .onSource(module)
                  .onLine(5);
            });
  }

  @Test
  public void moduleSubcomponents_listsSubcomponentBuilder() {
    Source module =
        CompilerTests.javaSource(
            "test.TestModule",
            "package test;",
            "",
            moduleType.importStatement(),
            "",
            moduleType.annotationWithSubcomponent("Sub.Builder.class"),
            "class TestModule {}");
    Source subcomponent =
        CompilerTests.javaSource(
            "test.Sub",
            "package test;",
            "",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent",
            "interface Sub {",
            "  @Subcomponent.Builder",
            "  interface Builder {",
            "    Sub build();",
            "  }",
            "}");
    CompilerTests.daggerCompiler(module, subcomponent)
        .compile(
            subject -> {
              subject.hasErrorCount(1);
              subject.hasErrorContaining(
                      "test.Sub.Builder is a @Subcomponent.Builder. Did you mean to use test.Sub?")
                  .onSource(module)
                  .onLine(5);
            });
  }

  @Test
  public void moduleSubcomponents_listsSubcomponentFactory() {
    Source module =
        CompilerTests.javaSource(
            "test.TestModule",
            "package test;",
            "",
            moduleType.importStatement(),
            "",
            moduleType.annotationWithSubcomponent("Sub.Factory.class"),
            "class TestModule {}");
    Source subcomponent =
        CompilerTests.javaSource(
            "test.Sub",
            "package test;",
            "",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent",
            "interface Sub {",
            "  @Subcomponent.Factory",
            "  interface Factory {",
            "    Sub creator();",
            "  }",
            "}");
    CompilerTests.daggerCompiler(module, subcomponent)
        .compile(
            subject -> {
              subject.hasErrorCount(1);
              subject.hasErrorContaining(
                      "test.Sub.Factory is a @Subcomponent.Factory. Did you mean to use test.Sub?")
                  .onSource(module)
                  .onLine(5);
            });
  }

  @Test
  public void moduleSubcomponents_listsProductionSubcomponentBuilder() {
    Source module =
        CompilerTests.javaSource(
            "test.TestModule",
            "package test;",
            "",
            moduleType.importStatement(),
            "",
            moduleType.annotationWithSubcomponent("Sub.Builder.class"),
            "class TestModule {}");
    Source subcomponent =
        CompilerTests.javaSource(
            "test.Sub",
            "package test;",
            "",
            "import dagger.producers.ProductionSubcomponent;",
            "",
            "@ProductionSubcomponent",
            "interface Sub {",
            "  @ProductionSubcomponent.Builder",
            "  interface Builder {",
            "    Sub build();",
            "  }",
            "}");
    CompilerTests.daggerCompiler(module, subcomponent)
        .compile(
            subject -> {
              subject.hasErrorCount(1);
              subject.hasErrorContaining(
                      "test.Sub.Builder is a @ProductionSubcomponent.Builder. "
                          + "Did you mean to use test.Sub?")
                  .onSource(module)
                  .onLine(5);
            });
  }

  @Test
  public void moduleSubcomponents_listsProductionSubcomponentFactory() {
    Source module =
        CompilerTests.javaSource(
            "test.TestModule",
            "package test;",
            "",
            moduleType.importStatement(),
            "",
            moduleType.annotationWithSubcomponent("Sub.Factory.class"),
            "class TestModule {}");
    Source subcomponent =
        CompilerTests.javaSource(
            "test.Sub",
            "package test;",
            "",
            "import dagger.producers.ProductionSubcomponent;",
            "",
            "@ProductionSubcomponent",
            "interface Sub {",
            "  @ProductionSubcomponent.Factory",
            "  interface Factory {",
            "    Sub create();",
            "  }",
            "}");
    CompilerTests.daggerCompiler(module, subcomponent)
        .compile(
            subject -> {
              subject.hasErrorCount(1);
              subject.hasErrorContaining(
                      "test.Sub.Factory is a @ProductionSubcomponent.Factory. "
                          + "Did you mean to use test.Sub?")
                  .onSource(module)
                  .onLine(5);
            });
  }

  @Test
  public void moduleSubcomponents_noSubcomponentCreator() {
    Source module =
        CompilerTests.javaSource(
            "test.TestModule",
            "package test;",
            "",
            moduleType.importStatement(),
            "",
            moduleType.annotationWithSubcomponent("NoBuilder.class"),
            "class TestModule {}");
    Source subcomponent =
        CompilerTests.javaSource(
            "test.NoBuilder",
            "package test;",
            "",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent",
            "interface NoBuilder {}");
    CompilerTests.daggerCompiler(module, subcomponent)
        .compile(
            subject -> {
              subject.hasErrorCount(1);
              subject.hasErrorContaining(
                      "test.NoBuilder doesn't have a @Subcomponent.Builder or "
                          + "@Subcomponent.Factory, which is required when used with @"
                          + moduleType.simpleName()
                          + ".subcomponents")
                  .onSource(module)
                  .onLine(5);
            });
  }

  @Test
  public void moduleSubcomponents_noProductionSubcomponentCreator() {
    Source module =
        CompilerTests.javaSource(
            "test.TestModule",
            "package test;",
            "",
            moduleType.importStatement(),
            "",
            moduleType.annotationWithSubcomponent("NoBuilder.class"),
            "class TestModule {}");
    Source subcomponent =
        CompilerTests.javaSource(
            "test.NoBuilder",
            "package test;",
            "",
            "import dagger.producers.ProductionSubcomponent;",
            "",
            "@ProductionSubcomponent",
            "interface NoBuilder {}");
    CompilerTests.daggerCompiler(module, subcomponent)
        .compile(
            subject -> {
              subject.hasErrorCount(1);
              subject.hasErrorContaining(
                      "test.NoBuilder doesn't have a @ProductionSubcomponent.Builder or "
                          + "@ProductionSubcomponent.Factory, which is required when used with @"
                          + moduleType.simpleName()
                          + ".subcomponents")
                  .onSource(module)
                  .onLine(5);
            });
  }

  @Test
  public void moduleSubcomponentsAreTypes() {
    Source module =
        CompilerTests.javaSource(
            "test.TestModule",
            "package test;",
            "",
            "import dagger.Module;",
            "",
            "@Module(subcomponents = int.class)",
            "class TestModule {}");
    CompilerTests.daggerCompiler(module)
        .compile(
            subject -> {
              subject.hasErrorCount(1);
              switch (CompilerTests.backend(subject)) {
                case JAVAC:
                  subject.hasErrorContaining("int is not a valid subcomponent type")
                      .onSource(module)
                      .onLine(5);
                  break;
                case KSP:
                  // TODO(b/245954367): Remove this pathway once this bug is fixed.
                  // KSP interprets the int.class type as a boxed type so we get a slightly
                  // different error message for now.
                  subject.hasErrorContaining(
                          "java.lang.Integer is not a @Subcomponent or @ProductionSubcomponent")
                      .onSource(module)
                      .onLine(5);
                  break;
              }
            });
  }

  @Test
  public void tooManyAnnotations() {
    assertThatModuleMethod(
            "@BindsOptionalOf @Multibinds abstract Set<Object> tooManyAnnotations();")
        .hasError("is annotated with more than one of");
  }

  @Test
  public void invalidIncludedModule() {
    Source badModule =
        CompilerTests.javaSource(
            "test.BadModule",
            "package test;",
            "",
            "import dagger.Binds;",
            "import dagger.Module;",
            "",
            "@Module",
            "abstract class BadModule {",
            "  @Binds abstract Object noParameters();",
            "}");
    Source module =
        CompilerTests.javaSource(
            "test.IncludesBadModule",
            "package test;",
            "",
            "import dagger.Module;",
            "",
            "@Module(includes = BadModule.class)",
            "abstract class IncludesBadModule {}");
    CompilerTests.daggerCompiler(badModule, module)
        .compile(
            subject -> {
              subject.hasErrorCount(2);
              subject.hasErrorContaining("test.BadModule has errors")
                  .onSource(module)
                  .onLine(5);
              subject.hasErrorContaining(
                      "@Binds methods must have exactly one parameter, whose type is "
                          + "assignable to the return type")
                  .onSource(badModule)
                  .onLine(8);
            });
  }

  @Test
  public void scopeOnModule() {
    Source badModule =
        CompilerTests.javaSource(
            "test.BadModule",
            "package test;",
            "",
            "import dagger.Module;",
            "import javax.inject.Singleton;",
            "",
            "@Singleton",
            "@Module",
            "interface BadModule {}");
    CompilerTests.daggerCompiler(badModule)
        .compile(
            subject -> {
              subject.hasErrorCount(1);
              subject.hasErrorContaining("@Modules cannot be scoped")
                  .onSource(badModule)
                  .onLineContaining("@Singleton");
            });
  }

  @Test
  public void moduleIncludesSelfCycle() {
    Source module =
        CompilerTests.javaSource(
            "test.TestModule",
            "package test;",
            "",
            moduleType.importStatement(),
            "import dagger.Provides;",
            "",
            String.format("@%s(", moduleType.simpleName()),
            "  includes = {",
            "      TestModule.class, // first",
            "      OtherModule.class,",
            "      TestModule.class, // second",
            "  }",
            ")",
            "class TestModule {",
            "  @Provides int i() { return 0; }",
            "}");

    Source otherModule =
        CompilerTests.javaSource(
            "test.OtherModule",
            "package test;",
            "",
            "import dagger.Module;",
            "",
            "@Module",
            "class OtherModule {}");

    CompilerTests.daggerCompiler(module, otherModule)
        .compile(
            subject -> {
              subject.hasErrorCount(2);
              String error =
                  String.format("@%s cannot include themselves", moduleType.simpleName());
              switch (CompilerTests.backend(subject)) {
                case JAVAC:
                  subject.hasErrorContaining(error).onSource(module).onLineContaining("// first");
                  subject.hasErrorContaining(error).onSource(module).onLineContaining("// second");
                  break;
                case KSP:
                  // KSP doesn't support reporting errors on individual annotation values, so both
                  // errors will be reported on the annotation itself.
                  subject.hasErrorContaining(error)
                      .onSource(module)
                      .onLineContaining("@" + moduleType.simpleName());
                  break;
              }
            });
  }

  // Regression test for b/264618194.
  @Test
  public void objectModuleInheritsInstanceBindingFails() {
    Source objectModule =
        CompilerTests.kotlinSource(
            "test.ObjectModule.kt",
            "package test",
            "",
            "import dagger.Module",
            "import dagger.Provides",
            "",
            "@Module",
            "object ObjectModule : ClassModule() {",
            "  @Provides fun provideString(): String = \"\"",
            "}");
    Source classModule =
        CompilerTests.kotlinSource(
            "test.ClassModule.kt",
            "package test",
            "",
            "import dagger.Module",
            "import dagger.Provides",
            "",
            "@Module",
            "abstract class ClassModule {",
            "  @Provides fun provideInt(): Int = 1",
            "}");
    Source component =
        CompilerTests.kotlinSource(
            "test.TestComponent.kt",
            "package test",
            "",
            "import dagger.Component",
            "",
            "@Component(modules = [ObjectModule::class])",
            "interface TestComponent {",
            "  fun getInt(): Int",
            "  fun getString(): String",
            "}");

    CompilerTests.daggerCompiler(component, objectModule, classModule)
        .compile(
            subject -> {
              subject.hasErrorCount(2);
              subject.hasErrorContaining("test.ObjectModule has errors")
                  .onSource(component)
                  .onLineContaining("ObjectModule::class");
              subject.hasErrorContaining(
                      "@Module-annotated Kotlin object cannot inherit instance "
                          + "(i.e. non-abstract, non-JVM static) binding method: "
                          + "@Provides int test.ClassModule.provideInt()")
                  .onSource(objectModule)
                  .onLineContaining(
                      // TODO(b/267223703): KAPT incorrectly reports the error on the annotation.
                      CompilerTests.backend(subject) == XProcessingEnv.Backend.JAVAC
                          ? "@Module"
                          : "object ObjectModule");
            });
  }

  // Regression test for b/264618194.
  @Test
  public void objectModuleInheritsNonInstanceBindingSucceeds() {
    Source objectModule =
        CompilerTests.kotlinSource(
            "test.ObjectModule.kt",
            "package test",
            "",
            "import dagger.Module",
            "import dagger.Provides",
            "",
            "@Module",
            "object ObjectModule : ClassModule() {",
            "  @Provides fun provideString(): String = \"\"",
            "}");
    Source classModule =
        CompilerTests.javaSource(
            "test.ClassModule",
            "package test;",
            "",
            "import dagger.Binds;",
            "import dagger.Module;",
            "import dagger.Provides;",
            "",
            "@Module",
            "public abstract class ClassModule {",
            "  // A non-binding instance method is okay.",
            "  public int nonBindingMethod() {",
            "    return 1;",
            "  }",
            "",
            "  // A static binding method is also okay.",
            "  @Provides",
            "  public static int provideInt() {",
            "    return 1;",
            "  }",
            "}");
    Source component =
        CompilerTests.kotlinSource(
            "test.TestComponent.kt",
            "package test",
            "",
            "import dagger.Component",
            "",
            "@Component(modules = [ObjectModule::class])",
            "interface TestComponent {",
            "  fun getInt(): Int",
            "  fun getString(): String",
            "}");
    CompilerTests.daggerCompiler(component, objectModule, classModule)
        .compile(subject -> subject.hasErrorCount(0));
  }
}
