/*
 * Copyright (C) 2020 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.hilt.processor.internal.aggregateddeps;

import static com.google.common.truth.Truth.assertThat;

import androidx.room.compiler.processing.util.Source;
import com.google.common.collect.ImmutableList;
import dagger.hilt.android.testing.compile.HiltCompilerTests;
import dagger.hilt.processor.internal.GeneratedImport;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for errors generated by {@link AggregatedDepsProcessor} */
@RunWith(JUnit4.class)
public class AggregatedDepsProcessorErrorsTest {

  @Rule public TemporaryFolder tempFolderRule = new TemporaryFolder();

  @Test
  public void reportMultipleAnnotationTypeKindErrors() {
    Source source =
        HiltCompilerTests.javaSource(
            "foo.bar.AnnotationsOnWrongTypeKind",
            "package foo.bar;",
            "",
            "import dagger.hilt.EntryPoint;",
            "import dagger.hilt.InstallIn;",
            "import dagger.Module;",
            "import dagger.hilt.components.SingletonComponent;",
            "import dagger.hilt.internal.ComponentEntryPoint;",
            "import dagger.hilt.internal.GeneratedEntryPoint;",
            "",
            "@InstallIn(SingletonComponent.class)",
            "@Module",
            "enum FooModule { VALUE }",
            "",
            "@InstallIn(SingletonComponent.class)",
            "@EntryPoint",
            "final class BarEntryPoint {}",
            "",
            "@InstallIn(SingletonComponent.class)",
            "@ComponentEntryPoint",
            "final class BazComponentEntryPoint {}",
            "",
            "@EntryPoint",
            "interface QuxEntryPoint {}",
            "",
            "@EntryPoint",
            "@Module",
            "interface DontMix{}",
            "");

    HiltCompilerTests.hiltCompiler(source)
        .compile(
            subject -> {
              subject.compilationDidFail();
              subject
                  .hasErrorContaining("Only classes and interfaces can be annotated with @Module")
                  .onSource(source)
                  .onLine(12);
              subject
                  .hasErrorContaining("Only interfaces can be annotated with @EntryPoint")
                  .onSource(source)
                  .onLine(16);
              subject
                  .hasErrorContaining("Only interfaces can be annotated with @ComponentEntryPoint")
                  .onSource(source)
                  .onLine(20);
              subject
                  .hasErrorContaining(
                      "@EntryPoint foo.bar.QuxEntryPoint must also be annotated with @InstallIn")
                  .onSource(source)
                  .onLine(23);
              subject
                  .hasErrorContaining(
                      "@Module and @EntryPoint cannot be used on the same interface")
                  .onSource(source)
                  .onLine(27);
            });
  }

  @Test
  public void testInvalidComponentInInstallInAnnotation() {
    Source module =
        HiltCompilerTests.javaSource(
            "test.FooModule",
            "package test;",
            "",
            "import dagger.Module;",
            "import dagger.hilt.InstallIn;",
            "import dagger.hilt.android.qualifiers.ApplicationContext;",
            "",
            "@InstallIn(ApplicationContext.class)", // Error: Not a Hilt component
            "@Module",
            "final class FooModule {}");

    HiltCompilerTests.hiltCompiler(module)
        .compile(
            subject -> {
              subject.compilationDidFail();
              subject
                  .hasErrorContaining(
                      "@InstallIn, can only be used with @DefineComponent-annotated classes, but"
                          + " found: [dagger.hilt.android.qualifiers.ApplicationContext]")
                  .onSource(module)
                  .onLine(9);
            });
  }

  @Test
  public void testMissingInstallInAnnotation() {
    Source source =
        HiltCompilerTests.javaSource(
            "foo.bar.AnnotationsOnWrongTypeKind",
            "package foo.bar;",
            "",
            "import dagger.Module;",
            "",
            "@Module", // Error: Doesn't have InstallIn annotation
            "final class FooModule {}");

    HiltCompilerTests.hiltCompiler(source)
        .compile(
            subject -> {
              subject.compilationDidFail();
              subject
                  .hasErrorContaining("foo.bar.FooModule is missing an @InstallIn annotation")
                  .onSource(source)
                  .onLine(6);
            });
  }

  @Test
  public void testNoErrorOnDaggerGeneratedModules() {
    Source source =
        HiltCompilerTests.javaSource(
            "foo.bar.BarModule",
            "package foo.bar;",
            "",
            GeneratedImport.IMPORT_GENERATED_ANNOTATION,
            "import dagger.Module;",
            "",
            "@Module",
            "@Generated(value = \"something\")", // Error: Isn't Dagger-generated but missing
            // InstallIn
            "final class FooModule {}",
            "",
            "@Module",
            "@Generated(value = \"dagger\")", // No error because the module is dagger generated
            "final class BarModule {}");

    HiltCompilerTests.hiltCompiler(source)
        .compile(
            subject -> {
              subject.compilationDidFail();
              subject.hasErrorCount(1);
              subject
                  .hasErrorContaining("foo.bar.FooModule is missing an @InstallIn annotation")
                  .onSource(source)
                  .onLine(8);
            });
  }

  @Test
  public void testModuleWithOnlyParamConstructor_fails() {
    Source source =
        HiltCompilerTests.javaSource(
            "foo.bar.FooModule",
            "package foo.bar;",
            "",
            "import dagger.Module;",
            "import dagger.Provides;",
            "import dagger.hilt.InstallIn;",
            "import dagger.hilt.components.SingletonComponent;",
            "",
            "@Module",
            "@InstallIn(SingletonComponent.class)",
            "final class FooModule {",
            "  FooModule(String arg) {}",
            "",
            "  @Provides",
            "  String provideString() {",
            "    return \"\";",
            "  }",
            "}");

    HiltCompilerTests.hiltCompiler(source)
        .compile(
            subject -> {
              subject.compilationDidFail();
              subject.hasErrorCount(1);
              subject.hasErrorContaining(
                  "Modules that need to be instantiated by Hilt must have a visible, empty"
                      + " constructor.");
            });
  }

  @Test
  public void testInnerModule() {
    Source source =
        HiltCompilerTests.javaSource(
            "foo.bar.Outer",
            "package foo.bar;",
            "",
            "import dagger.Module;",
            "import dagger.hilt.InstallIn;",
            "import dagger.hilt.components.SingletonComponent;",
            "",
            "final class Outer {",
            "  @Module",
            "  @InstallIn(SingletonComponent.class)",
            "  final class InnerModule {}",
            "}");

    HiltCompilerTests.hiltCompiler(source)
        .compile(
            subject -> {
              subject.compilationDidFail();
              subject.hasErrorCount(1);
              subject.hasErrorContaining(
                  "Nested @InstallIn modules must be static unless they are directly nested within"
                      + " a test. Found: foo.bar.Outer.InnerModule");
            });
  }

  @Test
  public void testInnerModuleInTest() {
    Source source =
        HiltCompilerTests.javaSource(
            "foo.bar.Outer",
            "package foo.bar;",
            "",
            "import dagger.Module;",
            "import dagger.hilt.InstallIn;",
            "import dagger.hilt.components.SingletonComponent;",
            "import dagger.hilt.android.testing.HiltAndroidTest;",
            "",
            "@HiltAndroidTest",
            "final class Outer {",
            "  static class Nested {",
            "    @Module",
            "    @InstallIn(SingletonComponent.class)",
            "    final class InnerModule {}",
            "  }",
            "}");

    HiltCompilerTests.hiltCompiler(source)
        .compile(
            subject -> {
              subject.compilationDidFail();
              subject.hasErrorCount(1);
              subject.hasErrorContaining(
                  "Nested @InstallIn modules must be static unless they are directly nested within"
                      + " a test. Found: foo.bar.Outer.Nested.InnerModule");
            });
  }

  @Test
  public void testInnerModuleInTest_succeeds() {
    Source source =
        HiltCompilerTests.javaSource(
            "foo.bar.Outer",
            "package foo.bar;",
            "",
            "import dagger.Module;",
            "import dagger.hilt.InstallIn;",
            "import dagger.hilt.components.SingletonComponent;",
            "import dagger.hilt.android.testing.HiltAndroidTest;",
            "",
            "@HiltAndroidTest",
            "public final class Outer {",
            "  @Module",
            "  @InstallIn(SingletonComponent.class)",
            "  static final class InnerModule {}",
            "}");

    // TODO(danysantiago): Add KSP test once b/288966076 is resolved.
    HiltCompilerTests.compileWithKapt(
        ImmutableList.of(source),
        tempFolderRule,
        result -> assertThat(result.getSuccess()).isTrue());
  }
}
