/*
 * 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 com.google.testing.compile.CompilationSubject.assertThat;
import static dagger.internal.codegen.Compilers.daggerCompiler;
import static dagger.internal.codegen.DaggerModuleMethodSubject.Factory.assertThatModuleMethod;

import com.google.testing.compile.Compilation;
import com.google.testing.compile.JavaFileObjects;
import dagger.Module;
import dagger.producers.ProducerModule;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collection;
import javax.tools.JavaFileObject;
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() {
    JavaFileObject module =
        JavaFileObjects.forSourceLines(
            "test.TestModule",
            "package test;",
            "",
            moduleType.importStatement(),
            "",
            moduleType.annotationWithSubcomponent("NotASubcomponent.class"),
            "class TestModule {}");
    JavaFileObject notASubcomponent =
        JavaFileObjects.forSourceLines(
            "test.NotASubcomponent", "package test;", "", "class NotASubcomponent {}");
    Compilation compilation = daggerCompiler().compile(module, notASubcomponent);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            "test.NotASubcomponent is not a @Subcomponent or @ProductionSubcomponent")
        .inFile(module)
        .onLine(5);
  }

  @Test
  public void moduleSubcomponents_listsSubcomponentBuilder() {
    JavaFileObject module =
        JavaFileObjects.forSourceLines(
            "test.TestModule",
            "package test;",
            "",
            moduleType.importStatement(),
            "",
            moduleType.annotationWithSubcomponent("Sub.Builder.class"),
            "class TestModule {}");
    JavaFileObject subcomponent =
        JavaFileObjects.forSourceLines(
            "test.Sub",
            "package test;",
            "",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent",
            "interface Sub {",
            "  @Subcomponent.Builder",
            "  interface Builder {",
            "    Sub build();",
            "  }",
            "}");
    Compilation compilation = daggerCompiler().compile(module, subcomponent);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            "test.Sub.Builder is a @Subcomponent.Builder. Did you mean to use test.Sub?")
        .inFile(module)
        .onLine(5);
  }

  @Test
  public void moduleSubcomponents_listsSubcomponentFactory() {
    JavaFileObject module =
        JavaFileObjects.forSourceLines(
            "test.TestModule",
            "package test;",
            "",
            moduleType.importStatement(),
            "",
            moduleType.annotationWithSubcomponent("Sub.Factory.class"),
            "class TestModule {}");
    JavaFileObject subcomponent =
        JavaFileObjects.forSourceLines(
            "test.Sub",
            "package test;",
            "",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent",
            "interface Sub {",
            "  @Subcomponent.Factory",
            "  interface Factory {",
            "    Sub creator();",
            "  }",
            "}");
    Compilation compilation = daggerCompiler().compile(module, subcomponent);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            "test.Sub.Factory is a @Subcomponent.Factory. Did you mean to use test.Sub?")
        .inFile(module)
        .onLine(5);
  }

  @Test
  public void moduleSubcomponents_listsProductionSubcomponentBuilder() {
    JavaFileObject module =
        JavaFileObjects.forSourceLines(
            "test.TestModule",
            "package test;",
            "",
            moduleType.importStatement(),
            "",
            moduleType.annotationWithSubcomponent("Sub.Builder.class"),
            "class TestModule {}");
    JavaFileObject subcomponent =
        JavaFileObjects.forSourceLines(
            "test.Sub",
            "package test;",
            "",
            "import dagger.producers.ProductionSubcomponent;",
            "",
            "@ProductionSubcomponent",
            "interface Sub {",
            "  @ProductionSubcomponent.Builder",
            "  interface Builder {",
            "    Sub build();",
            "  }",
            "}");
    Compilation compilation = daggerCompiler().compile(module, subcomponent);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            "test.Sub.Builder is a @ProductionSubcomponent.Builder. Did you mean to use test.Sub?")
        .inFile(module)
        .onLine(5);
  }

  @Test
  public void moduleSubcomponents_listsProductionSubcomponentFactory() {
    JavaFileObject module =
        JavaFileObjects.forSourceLines(
            "test.TestModule",
            "package test;",
            "",
            moduleType.importStatement(),
            "",
            moduleType.annotationWithSubcomponent("Sub.Factory.class"),
            "class TestModule {}");
    JavaFileObject subcomponent =
        JavaFileObjects.forSourceLines(
            "test.Sub",
            "package test;",
            "",
            "import dagger.producers.ProductionSubcomponent;",
            "",
            "@ProductionSubcomponent",
            "interface Sub {",
            "  @ProductionSubcomponent.Factory",
            "  interface Factory {",
            "    Sub create();",
            "  }",
            "}");
    Compilation compilation = daggerCompiler().compile(module, subcomponent);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            "test.Sub.Factory is a @ProductionSubcomponent.Factory. Did you mean to use test.Sub?")
        .inFile(module)
        .onLine(5);
  }

  @Test
  public void moduleSubcomponents_noSubcomponentCreator() {
    JavaFileObject module =
        JavaFileObjects.forSourceLines(
            "test.TestModule",
            "package test;",
            "",
            moduleType.importStatement(),
            "",
            moduleType.annotationWithSubcomponent("NoBuilder.class"),
            "class TestModule {}");
    JavaFileObject subcomponent =
        JavaFileObjects.forSourceLines(
            "test.NoBuilder",
            "package test;",
            "",
            "import dagger.Subcomponent;",
            "",
            "@Subcomponent",
            "interface NoBuilder {}");
    Compilation compilation = daggerCompiler().compile(module, subcomponent);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            "test.NoBuilder doesn't have a @Subcomponent.Builder or @Subcomponent.Factory, which "
                + "is required when used with @"
                + moduleType.simpleName()
                + ".subcomponents")
        .inFile(module)
        .onLine(5);
  }

  @Test
  public void moduleSubcomponents_noProductionSubcomponentCreator() {
    JavaFileObject module =
        JavaFileObjects.forSourceLines(
            "test.TestModule",
            "package test;",
            "",
            moduleType.importStatement(),
            "",
            moduleType.annotationWithSubcomponent("NoBuilder.class"),
            "class TestModule {}");
    JavaFileObject subcomponent =
        JavaFileObjects.forSourceLines(
            "test.NoBuilder",
            "package test;",
            "",
            "import dagger.producers.ProductionSubcomponent;",
            "",
            "@ProductionSubcomponent",
            "interface NoBuilder {}");
    Compilation compilation = daggerCompiler().compile(module, subcomponent);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining(
            "test.NoBuilder doesn't have a @ProductionSubcomponent.Builder or "
                + "@ProductionSubcomponent.Factory, which is required when used with @"
                + moduleType.simpleName()
                + ".subcomponents")
        .inFile(module)
        .onLine(5);
  }

  @Test
  public void moduleSubcomponentsAreTypes() {
    JavaFileObject module =
        JavaFileObjects.forSourceLines(
            "test.TestModule",
            "package test;",
            "",
            "import dagger.Module;",
            "",
            "@Module(subcomponents = int.class)",
            "class TestModule {}");
    Compilation compilation = daggerCompiler().compile(module);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining("int is not a valid subcomponent type")
        .inFile(module)
        .onLine(5);
  }

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

  @Test
  public void invalidIncludedModule() {
    JavaFileObject badModule =
        JavaFileObjects.forSourceLines(
            "test.BadModule",
            "package test;",
            "",
            "import dagger.Binds;",
            "import dagger.Module;",
            "",
            "@Module",
            "abstract class BadModule {",
            "  @Binds abstract Object noParameters();",
            "}");
    JavaFileObject module =
        JavaFileObjects.forSourceLines(
            "test.IncludesBadModule",
            "package test;",
            "",
            "import dagger.Module;",
            "",
            "@Module(includes = BadModule.class)",
            "abstract class IncludesBadModule {}");
    Compilation compilation = daggerCompiler().compile(badModule, module);
    assertThat(compilation).hadErrorCount(2);
    assertThat(compilation)
        .hadErrorContaining("test.BadModule has errors")
        .inFile(module)
        .onLine(5);
    assertThat(compilation)
        .hadErrorContaining(
            "@Binds methods must have exactly one parameter, whose type is assignable to the "
                + "return type")
        .inFile(badModule)
        .onLine(8);
  }

  @Test
  public void scopeOnModule() {
    JavaFileObject badModule =
        JavaFileObjects.forSourceLines(
            "test.BadModule",
            "package test;",
            "",
            "import dagger.Module;",
            "import javax.inject.Singleton;",
            "",
            "@Singleton",
            "@Module",
            "interface BadModule {}");
    Compilation compilation = daggerCompiler().compile(badModule);
    assertThat(compilation).failed();
    assertThat(compilation)
        .hadErrorContaining("@Modules cannot be scoped")
        .inFile(badModule)
        .onLineContaining("@Singleton");
  }

  @Test
  public void moduleIncludesSelfCycle() {
    JavaFileObject module =
        JavaFileObjects.forSourceLines(
            "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; }",
            "}");

    JavaFileObject otherModule =
        JavaFileObjects.forSourceLines(
            "test.OtherModule",
            "package test;",
            "",
            "import dagger.Module;",
            "",
            "@Module",
            "class OtherModule {}");

    Compilation compilation = daggerCompiler().compile(module, otherModule);
    assertThat(compilation).failed();
    String error = String.format("@%s cannot include themselves", moduleType.simpleName());
    assertThat(compilation).hadErrorContaining(error).inFile(module).onLineContaining("Module(");
  }
}
